# ============================================================================== # Boeing Navigation Display by Gijs de Rooy # ============================================================================== ## # do we really need to keep track of each drawable here ?? var i = 0; ## # pseudo DSL-ish: use these as placeholders in the config hash below var ALWAYS = func 1; var NOTHING = func nil; ## # so that we only need to update a single line ... # var trigger_update = func(layer) layer._model.init(); ## # TODO: move ND-specific implementation details into this lookup hash # so that other aircraft and ND types can be more easily supported # # any aircraft-specific ND behavior should be wrapped here, # to isolate/decouple things in the generic NavDisplay class # # TODO: move this to an XML config file # var NDStyles = { ## # this configures the 744 ND to help generalize the NavDisplay class itself 'Boeing': { font_mapper: func(family, weight) { if( family == "Liberation Sans" and weight == "normal" ) return "LiberationFonts/LiberationSans-Regular.ttf"; }, # where all the symbols are stored # TODO: SVG elements should be renamed to use boeing/airbus prefix # aircraft developers should all be editing the same ND.svg image # the code can deal with the differences now svg_filename: "Nasal/canvas/map/boeingND.svg", ## ## this loads and configures existing layers (currently, *.layer files in Nasal/canvas/map) ## layers: [ { name:'fixes', disabled:1, update_on:['toggle_range','toggle_waypoints'], predicate: func(nd, layer) { # print("Running fixes predicate"); var visible=nd.get_switch('toggle_waypoints') and nd.in_mode('toggle_display_mode', ['MAP']) and (nd.rangeNm() <= 40); if (visible) { # print("fixes update requested!"); trigger_update( layer ); } layer._view.setVisible(visible); }, # end of layer update predicate }, # end of fixes layer { name:'FIX', isMapStructure:1, update_on:['toggle_range','toggle_waypoints'], # FIXME: this is a really ugly place for controller code predicate: func(nd, layer) { # print("Running vor layer predicate"); # toggle visibility here var visible=nd.get_switch('toggle_waypoints') and nd.in_mode('toggle_display_mode', ['MAP']) and (nd.rangeNm() <= 40); layer.group.setVisible( nd.get_switch('toggle_waypoints') ); if (visible) { #print("Updating MapStructure ND layer: FIX"); # (Hopefully) smart update layer.update(); } }, # end of layer update predicate }, # end of FIX layer # Should redraw every 10 seconds { name:'storms', update_on:['toggle_range','toggle_weather','toggle_display_mode'], predicate: func(nd, layer) { # print("Running fixes predicate"); var visible=nd.get_switch('toggle_weather') and nd.get_switch('toggle_display_mode') != "PLAN"; if (visible) { #print("storms update requested!"); trigger_update( layer ); } layer._view.setVisible(visible); }, # end of layer update predicate }, # end of storms layer { name:'airports-nd', update_on:['toggle_range','toggle_airports','toggle_display_mode'], predicate: func(nd, layer) { # print("Running airports-nd predicate"); var visible = nd.get_switch('toggle_airports') and nd.in_mode('toggle_display_mode', ['MAP']); if (visible) { trigger_update( layer ); # clear & redraw } layer._view.setVisible( visible ); }, # end of layer update predicate }, # end of airports layer # Should distinct between low and high altitude navaids. Hiding above 40 NM for now, to prevent clutter/lag. { name:'vor', disabled:1, update_on:['toggle_range','toggle_stations','toggle_display_mode'], predicate: func(nd, layer) { # print("Running vor layer predicate"); var visible = nd.get_switch('toggle_stations') and nd.in_mode('toggle_display_mode', ['MAP']) and (nd.rangeNm() <= 40); if(visible) { trigger_update( layer ); # clear & redraw } layer._view.setVisible( nd.get_switch('toggle_stations') ); }, # end of layer update predicate }, # end of VOR layer { name:'VOR', isMapStructure:1, update_on:['toggle_range','toggle_stations','toggle_display_mode'], # FIXME: this is a really ugly place for controller code predicate: func(nd, layer) { # print("Running vor layer predicate"); # toggle visibility here var visible = nd.get_switch('toggle_stations') and nd.in_mode('toggle_display_mode', ['MAP']) and (nd.rangeNm() <= 40); layer.group.setVisible( visible ); if (visible) { #print("Updating MapStructure ND layer: VOR"); # (Hopefully) smart update layer.update(); } }, # end of layer update predicate }, # end of VOR layer # Should distinct between low and high altitude navaids. Hiding above 40 NM for now, to prevent clutter/lag. { name:'dme', disabled:1, update_on:['toggle_range','toggle_stations'], predicate: func(nd, layer) { var visible = nd.get_switch('toggle_stations') and nd.in_mode('toggle_display_mode', ['MAP']) and (nd.rangeNm() <= 40); if(visible) { trigger_update( layer ); # clear & redraw } layer._view.setVisible( nd.get_switch('toggle_stations') ); }, # end of layer update predicate }, # end of DME layers { name:'DME', isMapStructure:1, update_on:['toggle_range','toggle_stations'], # FIXME: this is a really ugly place for controller code predicate: func(nd, layer) { # print("Running vor layer predicate"); # toggle visibility here layer.group.setVisible( nd.get_switch('toggle_stations') ); if (nd.rangeNm() <= 40 and nd.get_switch('toggle_stations') and nd.get_switch('toggle_display_mode') == "MAP") { #print("Updating MapStructure ND layer: DME"); # (Hopefully) smart update layer.update(); } }, # end of layer update predicate }, # end of DME layer { name:'mp-traffic', update_on:['toggle_range','toggle_traffic'], predicate: func(nd, layer) { trigger_update( layer ); # clear & redraw layer._view.setVisible( 1 ); #nd.get_switch('toggle_traffic') }, # end of layer update predicate }, # end of traffic layer { name:'runway-nd', update_on:['toggle_range','toggle_display_mode'], predicate: func(nd, layer) { var visible = (nd.rangeNm() <= 40 and getprop("autopilot/route-manager/active") ) ; if (visible) trigger_update( layer ); # clear & redraw layer._view.setVisible( visible ); }, # end of layer update predicate }, # end of airports-nd layer { name:'route', update_on:['toggle_range','toggle_display_mode'], predicate: func(nd, layer) { trigger_update( layer ); # clear & redraw layer._view.setVisible( 1 ); #nd.get_switch('toggle_traffic') }, # end of layer update predicate }, # end of route layer ## add other layers here, layer names must match the registered names as used in *.layer files for now ## this will all change once we're using Philosopher's MapStructure framework ], # end of vector with configured layers # This is where SVG elements are configured by providing "behavior" hashes, i.e. for animations # to animate each SVG symbol, specify behavior via callbacks (predicate, and true/false implementation) # SVG identifier, callback etc # TODO: update_on([]), update_mode (update() vs. timers/listeners) # TODO: support putting symbols on specific layers features: [ { # TODO: taOnly doesn't need to use getprop polling in update(), use a listener instead! id: 'taOnly', # the SVG ID impl: { # implementation hash init: func(nd, symbol), # for updateCenter stuff, called during initialization in the ctor predicate: func(nd) getprop("instrumentation/tcas/inputs/mode") == 2, # the condition is_true: func(nd) nd.symbols.taOnly.show(), # if true, run this is_false: func(nd) nd.symbols.taOnly.hide(), # if false, run this }, # end of taOnly behavior/callbacks }, # end of taOnly { id: 'tas', impl: { init: func(nd,symbol), predicate: func(nd) getprop("/velocities/airspeed-kt") > 100, is_true: func(nd) { nd.symbols.tas.setText(sprintf("%3.0f",getprop("/velocities/airspeed-kt") )); nd.symbols.tas.show(); }, is_false: func(nd) nd.symbols.tas.hide(), }, # end of tas behavior callbacks }, # end of tas hash { id: 'wpActiveId', impl: { init: func(nd,symbol), predicate: func(nd) getprop("/autopilot/route-manager/wp/id") != nil and getprop("autopilot/route-manager/active"), is_true: func(nd) { nd.symbols.wpActiveId.setText(getprop("/autopilot/route-manager/wp/id")); nd.symbols.wpActiveId.show(); }, is_false: func(nd) nd.symbols.wpActiveId.hide(), }, # of wpActiveId.impl }, # of wpActiveId { id: 'wpActiveDist', impl: { init: func(nd,symbol), predicate: func(nd) getprop("/autopilot/route-manager/wp/dist") != nil and getprop("autopilot/route-manager/active"), is_true: func(nd) { nd.symbols.wpActiveDist.setText(sprintf("%3.01fNM",getprop("/autopilot/route-manager/wp/dist"))); nd.symbols.wpActiveDist.show(); }, is_false: func(nd) nd.symbols.wpActiveDist.hide(), }, # of wpActiveDist.impl }, # of wpActiveDist { id: 'eta', impl: { init: func(nd,symbol), predicate: func(nd) getprop("autopilot/route-manager/wp/eta") != nil and getprop("autopilot/route-manager/active"), is_true: func(nd) { var etaSec = getprop("/sim/time/utc/day-seconds")+getprop("autopilot/route-manager/wp/eta-seconds"); var h = math.floor(etaSec/3600); if (h>24) h=h-24; etaSec=etaSec-3600*h; var m = math.floor(etaSec/60); etaSec=etaSec-60*m; var s = etaSec; nd.symbols.eta.setText(sprintf("%02.0f%02.0f.%02.0fz",h,m,s)); nd.symbols.eta.show(); }, is_false: func(nd) nd.symbols.eta.hide(), }, # of eta.impl }, # of eta { id:'hdg', impl: { init: func(nd,symbol), predicate: ALWAYS, # always true is_true: func(nd) nd.symbols.hdg.setText(sprintf("%03.0f", nd.aircraft_source.get_hdg_mag() )), is_false: NOTHING, }, # of hdg.impl }, # of hdg { id:'gs', impl: { init: func(nd,symbol), common: func(nd) nd.symbols.gs.setText(sprintf("%3.0f",nd.aircraft_source.get_spd() )), predicate: func(nd) nd.aircraft_source.get_spd() >= 30, is_true: func(nd) { nd.symbols.gs.setFontSize(36); }, is_false: func(nd) nd.symbols.gs.setFontSize(52), }, # of gs.impl }, # of gs { id:'rangeArcs', impl: { init: func(nd,symbol), 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"), is_true: func(nd) nd.symbols.rangeArcs.show(), is_false: func(nd) nd.symbols.rangeArcs.hide(), }, # of rangeArcs.impl }, # of rangeArcs ], # end of vector with features }, # end of Boeing style ##### ## ## add support for other aircraft/ND types and styles here (Airbus etc) ## ## }; # end of NDStyles ## # 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) # var NDSourceDriver = {}; NDSourceDriver.new = func { var m = {parents:[NDSourceDriver]}; m.get_hdg_mag= func getprop("/orientation/heading-magnetic-deg"); m.get_hdg_tru= func getprop("/orientation/heading-deg"); m.get_hgg = func getprop("instrumentation/afds/settings/heading"); m.get_trk_mag= func { if(getprop("/velocities/groundspeed-kt") > 80) { getprop("/orientation/track-magnetic-deg"); } else { getprop("/orientation/heading-magnetic-deg"); } }; m.get_trk_tru = func { if(getprop("/velocities/groundspeed-kt") > 80) { getprop("/orientation/track-deg"); } else { getprop("/orientation/heading-deg"); } }; m.get_lat= func getprop("/position/latitude-deg"); m.get_lon= func getprop("/position/longitude-deg"); m.get_spd= func getprop("/velocities/groundspeed-kt"); m.get_vspd= func getprop("/velocities/vertical-speed-fps"); return m; } ## # configure aircraft specific cockpit switches here # these are some defaults, can be overridden when calling NavDisplay.new() - # see the 744 ND.nas file the backend code should never deal directly with # aircraft specific properties using getprop. # To get started implementing your own ND, just copy the switches hash to your # ND.nas file and map the keys to your cockpit properties - and things will just work. # TODO: switches are ND specific, so move to the NDStyle hash! var default_switches = { 'toggle_range': {path: '/inputs/range-nm', value:40, type:'INT'}, 'toggle_weather': {path: '/inputs/wxr', value:0, type:'BOOL'}, 'toggle_airports': {path: '/inputs/arpt', value:0, type:'BOOL'}, 'toggle_stations': {path: '/inputs/sta', value:0, type:'BOOL'}, 'toggle_waypoints': {path: '/inputs/wpt', value:0, type:'BOOL'}, 'toggle_position': {path: '/inputs/pos', value:0, type:'BOOL'}, 'toggle_data': {path: '/inputs/data',value:0, type:'BOOL'}, 'toggle_terrain': {path: '/inputs/terr',value:0, type:'BOOL'}, 'toggle_traffic': {path: '/inputs/tcas',value:0, type:'BOOL'}, 'toggle_centered': {path: '/inputs/nd-centered',value:0, type:'BOOL'}, 'toggle_lh_vor_adf': {path: '/inputs/lh-vor-adf',value:0, type:'INT'}, 'toggle_rh_vor_adf': {path: '/inputs/rh-vor-adf',value:0, type:'INT'}, 'toggle_display_mode': {path: '/mfd/display-mode', value:'MAP', type:'STRING'}, # valid values are: APP, MAP, PLAN or VOR 'toggle_display_type': {path: '/mfd/display-type', value:'CRT', type:'STRING'}, # valid values are: CRT or LCD 'toggle_true_north': {path: '/mfd/true-north', value:0, type:'BOOL'}, }; # Hack to update weather radar once every 10 seconds var update_weather = func { if (getprop("/instrumentation/efis/inputs/wxr") != nil) setprop("/instrumentation/efis/inputs/wxr",getprop("/instrumentation/efis/inputs/wxr")); settimer(update_weather, 10); } update_weather(); # Hack to update airplane symbol location on PLAN mode every 5 seconds var update_apl_sym = func { if (getprop("/instrumentation/efis/mfd/display-mode") == "PLAN") setprop("/instrumentation/efis/mfd/display-mode","PLAN"); settimer(update_apl_sym, 5); } update_apl_sym(); ## # TODO: # - introduce a MFD class (use it also for PFD/EICAS) # - introduce a SGSubsystem class and use it here # - introduce a Boeing NavDisplay class var NavDisplay = { # reset handler handle_reinit: func { print("Cleaning up NavDisplay listeners"); # shut down all timers and other loops here me.update_timer.stop(); foreach(var l; me.listeners) removelistener(l); }, listen: func(p,c) { append(me.listeners, setlistener(p,c)); }, # listeners for cockpit switches listen_switch: func(s,c) { # print("event setup for: ", id(c)); me.listen( me.get_full_switch_path(s), func { # print("listen_switch triggered:", s, " callback id:", id(c) ); c(); }); }, # get the full property path for a given switch get_full_switch_path: func (s) { # debug.dump( me.efis_switches[s] ); return me.efis_path ~ me.efis_switches[s].path; # FIXME: should be using props.nas instead of ~ }, # helper method for getting configurable cockpit switches (which are usually different in each aircraft) get_switch: func(s) { var switch = me.efis_switches[s]; var path = me.efis_path ~ switch.path ; #print(s,":Getting switch prop:", path); return getprop( path ); }, # for creating NDs that are driven by AI traffic instead of the main aircraft (generalization rocks!) connectAI: func(source=nil) { me.aircraft_source = { get_hdg_mag: func source.getNode('orientation/heading-magnetic-deg').getValue(), get_trk_mag: func source.getNode('orientation/track-magnetic-deg').getValue(), get_lat: func source.getNode('position/latitude-deg').getValue(), get_lon: func source.getNode('position/longitude-deg').getValue(), get_spd: func source.getNode('velocities/true-airspeed-kt').getValue(), }; }, # of connectAI # TODO: the ctor should allow customization, for different aircraft # especially properties and SVG files/handles (747, 757, 777 etc) new : func(prop1, switches=default_switches, style='Boeing') { var m = { parents : [NavDisplay]}; m.listeners=[]; # for cleanup handling m.aircraft_source = NDSourceDriver.new(); # uses the main aircraft as the driver/source (speeds, position, heading) m.nd_style = NDStyles[style]; # look up ND specific stuff (file names etc) m.radio_list=["instrumentation/comm/frequencies","instrumentation/comm[1]/frequencies", "instrumentation/nav/frequencies", "instrumentation/nav[1]/frequencies"]; m.mfd_mode_list=["APP","VOR","MAP","PLAN"]; m.efis_path = prop1; m.efis_switches = switches; # just an alias, to avoid having to rewrite the old code for now m.rangeNm = func m.get_switch('toggle_range'); m.efis = props.globals.initNode(prop1); m.mfd = m.efis.initNode("mfd"); # TODO: unify this with switch handling m.mfd_mode_num = m.mfd .initNode("mode-num",2,"INT"); m.std_mode = m.efis.initNode("inputs/setting-std",0,"BOOL"); m.previous_set = m.efis.initNode("inhg-previous",29.92); m.kpa_mode = m.efis.initNode("inputs/kpa-mode",0,"BOOL"); m.kpa_output = m.efis.initNode("inhg-kpa",29.92); m.kpa_prevoutput = m.efis.initNode("inhg-kpa-previous",29.92); m.temp = m.efis.initNode("fixed-temp",0); m.alt_meters = m.efis.initNode("inputs/alt-meters",0,"BOOL"); m.fpv = m.efis.initNode("inputs/fpv",0,"BOOL"); m.mins_mode = m.efis.initNode("inputs/minimums-mode",0,"BOOL"); m.mins_mode_txt = m.efis.initNode("minimums-mode-text","RADIO","STRING"); m.minimums = m.efis.initNode("minimums",250,"INT"); m.mk_minimums = props.globals.getNode("instrumentation/mk-viii/inputs/arinc429/decision-height"); # TODO: these are switches, can be unified with switch handling hash above (eventually): m.nd_plan_wpt = m.efis.initNode("inputs/plan-wpt-index", 0, "INT"); # not yet in switches hash ### # initialize all switches based on the defaults specified in the switch hash # foreach(var switch; keys( m.efis_switches ) ) props.globals.initNode ( m.get_full_switch_path (switch), m.efis_switches[switch].value, m.efis_switches[switch].type ); return m; }, newMFD: func(canvas_group) { me.listen("/sim/signals/reinit", func me.handle_reinit() ); me.update_timer = maketimer(0.05, func me.update() ); # TODO: make interval configurable via ctor me.nd = canvas_group; # load the specified SVG file into the me.nd group and populate all sub groups canvas.parsesvg(me.nd, me.nd_style.svg_filename, {'font-mapper': me.nd_style.font_mapper}); me.symbols = {}; # storage for SVG elements, to avoid namespace pollution (all SVG elements end up here) foreach(var feature; me.nd_style.features ) { # print("Setting up SVG feature:", feature.id); me.symbols[feature.id] = me.nd.getElementById(feature.id); if(contains(feature.impl,'init')) feature.impl.init(me.nd, feature); # call The element's init code (i.e. updateCenter) } ### this is the "old" method that's less flexible, we want to use the style hash instead (see above) # because things are much better configurable that way # now look up all required SVG elements and initialize member fields using the same name to have a convenient handle foreach(var element; ["wind","dmeLDist","dmeRDist","dmeL","dmeR","vorL","vorR","vorLId","vorRId", "range","status.wxr","status.wpt","hdgGroup","status.sta","status.arpt"]) me.symbols[element] = me.nd.getElementById(element); # load elements from vector image, and create instance variables using identical names, and call updateCenter() on each # anything that needs updatecenter called, should be added to the vector here # foreach(var element; ["windArrow","compassApp","northUp","aplSymMap","aplSymMapCtr","aplSymVor", "staFromL2","staToL2","staFromR2","staToR2", "locPtr","hdgTrk","truMag","altArc","planArcs", "trkInd","compass","HdgBugCRT","TrkBugLCD","HdgBugLCD","selHdgLine","curHdgPtr", "staFromL","staToL","staFromR","staToR"] ) me.symbols[element] = me.nd.getElementById(element).updateCenter(); foreach(var element; ["HdgBugCRT2","TrkBugLCD2","HdgBugLCD2","selHdgLine2","curHdgPtr2","vorCrsPtr2"] ) me.symbols[element] = me.nd.getElementById(element).setCenter(512,565); # this should probably be using Philosopher's new SymbolLayer ? me.map = me.nd.createChild("map","map") .set("clip", "rect(124, 1024, 1024, 0)"); # this callback will be passed onto the model via the controller hash, and used for the positioned queries, to specify max query range: var get_range = func me.get_switch('toggle_range'); # predicate for the draw controller var is_tuned = func(freq) { var nav1=getprop("instrumentation/nav[0]/frequencies/selected-mhz"); var nav2=getprop("instrumentation/nav[1]/frequencies/selected-mhz"); if (freq == nav1 or freq == nav2) return 1; return 0; } # another predicate for the draw controller var get_course_by_freq = func(freq) { if (freq == getprop("instrumentation/nav[0]/frequencies/selected-mhz")) return getprop("instrumentation/nav[0]/radials/selected-deg"); else return getprop("instrumentation/nav[1]/radials/selected-deg"); } var get_current_position = func { return [ me.aircraft_source.get_lat(), me.aircraft_source.get_lon() ]; } # a hash with controller callbacks, will be passed onto draw routines to customize behavior/appearance # the point being that draw routines don't know anything about their frontends (instrument or GUI dialog) # so we need some simple way to communicate between frontend<->backend until we have real controllers # for now, a single controller hash is shared by most layers - unsupported callbacks are simply ignored by the draw files # var controller = { query_range: func get_range(), is_tuned:is_tuned, get_tuned_course:get_course_by_freq, get_position: get_current_position, }; # FIXME: MapStructure: big hack canvas.Symbol.Controller.get("VOR").query_range = controller.query_range; canvas.Symbol.Controller.get("VOR").get_tuned_course = controller.get_tuned_course; canvas.Symbol.Controller.get("DME").is_tuned = controller.is_tuned; ### # set up various layers, controlled via callbacks in the controller hash # revisit this code once Philosopher's "Smart MVC Symbols/Layers" work is committed and integrated # helper / closure generator var make_event_handler = func(predicate, layer) func predicate(me, layer); me.layers={}; # storage container for all ND specific layers # look up all required layers as specified per the NDStyle hash and do the initial setup for event handling foreach(var layer; me.nd_style.layers) { if(layer['disabled']) continue; # skip this layer #print("newMFD(): Setting up ND layer:", layer.name); # huge hack for the alt-arc, which is not rendered as a map group, but directly as part of the toplevel ND group var render_target = (!contains(layer,'not_a_map') or !layer.not_a_map) ? me.map : me.nd; var the_layer = nil; if(!layer['isMapStructure']) the_layer = me.layers[layer.name] = canvas.MAP_LAYERS[layer.name].new( render_target, layer.name, controller ); else { #print("Setting up MapStructure-based layer for ND, name:", layer.name); render_target.addLayer(factory: canvas.SymbolLayer, type_arg: layer.name); the_layer = me.layers[layer.name] = render_target.getLayer(layer.name); } # now register all layer specific notification listeners and their corresponding update predicate/callback # pass the ND instance and the layer handle to the predicate when it is called # so that it can directly access the ND instance and its own layer (without having to know the layer's name) var event_handler = make_event_handler(layer.predicate, the_layer); foreach(var event; layer.update_on) { # print("Setting up subscription:", event, " for ", layer.name, " handler id:", id(event_handler) ); me.listen_switch(event, event_handler); } # foreach event subscription # and now update/init each layer once by calling its update predicate for initialization event_handler(); } # foreach layer #print("navdisplay.mfd:ND layer setup completed"); # start the update timer, which makes sure that the update() will be called me.update_timer.start(); # next, radio & autopilot & listeners # TODO: move this to .init field in layers hash or to model files foreach(var n; var radios = [ "instrumentation/nav/frequencies/selected-mhz", "instrumentation/nav[1]/frequencies/selected-mhz"]) me.listen(n, func() { # me.drawvor(); # me.drawdme(); }); # TODO: move this to the route.model # Hack to draw the route on rm activation me.listen("/autopilot/route-manager/active", func(active) { if(active.getValue()) { me.drawroute(); me.drawrunways(); } else { #print("TODO: navdisplay.mfd: implement route-manager/layer clearing!"); #me.route_group.removeAllChildren(); # HACK! } }); me.listen("/autopilot/route-manager/current-wp", func(activeWp) { canvas.updatewp( activeWp.getValue() ); }); }, drawroute: func print("drawroute no longer used!"), drawrunways: func print("drawrunways no longer used!"), in_mode:func(switch, modes) { foreach(var m; modes) if(me.get_switch(switch)==m) return 1; return 0; }, # each model should keep track of when it last got updated, using current lat/lon # in update(), we can then check if the aircraft has traveled more than 0.5-1 nm (depending on selected range) # and update each model accordingly update: func() # FIXME: This stuff is still too aircraft specific, cannot easily be reused by other aircraft { ## # important constants var m1 = 111132.92; var m2 = -559.82; var m3 = 1.175; var m4 = -0.0023; var p1 = 111412.84; var p2 = -93.5; var p3 = 0.118; var latNm = 60; var lonNm = 60; # fgcommand('profiler-start'); # Heading update var userHdgMag = me.aircraft_source.get_hdg_mag(); var userHdgTru = me.aircraft_source.get_hdg_tru(); var userTrkMag = me.aircraft_source.get_trk_mag(); var userTrkTru = me.aircraft_source.get_trk_tru(); if(me.get_switch('toggle_true_north')) { me.symbols.truMag.setText("TRU"); var userHdg=userHdgTru; var userTrk=userTrkTru; } else { me.symbols.truMag.setText("MAG"); var userHdg=userHdgMag; var userTrk=userTrkMag; } if (me.aircraft_source.get_spd() < 80) userTrk = userHdg; var userLat = me.aircraft_source.get_lat(); var userLon = me.aircraft_source.get_lon(); var userSpd = me.aircraft_source.get_spd(); var userVSpd = me.aircraft_source.get_vspd(); var dispLCD = me.get_switch('toggle_display_type') == "LCD"; # this should only ever happen when testing the experimental AI/MP ND driver hash (not critical) if (!userHdg or !userTrk or !userLat or !userLon) { print("aircraft source invalid, returning !"); return; } if(me.in_mode('toggle_display_mode', ['PLAN'])) me.map.setTranslation(512,512); elsif(me.get_switch('toggle_centered')) me.map.setTranslation(512,565); else me.map.setTranslation(512,824); # Calculate length in NM of one degree at current location TODO: expose as methods, for external callbacks var userLatR = userLat*D2R; var userLonR = userLon*D2R; var latlen = m1 + (m2 * math.cos(2 * userLatR)) + (m3 * math.cos(4 * userLatR)) + (m4 * math.cos(6 * userLatR)); var lonlen = (p1 * math.cos(userLatR)) + (p2 * math.cos(3 * userLatR)) + (p3 * math.cos(5 * userLatR)); latNm = latlen*M2NM; #60 at equator lonNm = lonlen*M2NM; #60 at equator me.symbols.windArrow.setRotation((getprop("/environment/wind-from-heading-deg")-userHdg)*D2R); me.symbols.wind.setText(sprintf("%3.0f / %2.0f",getprop("/environment/wind-from-heading-deg"),getprop("/environment/wind-speed-kt"))); if(me.get_switch('toggle_lh_vor_adf') == 1) { me.symbols.vorL.setText("VOR L"); me.symbols.vorL.setColor(0.195,0.96,0.097); me.symbols.dmeL.setText("DME"); me.symbols.dmeL.setColor(0.195,0.96,0.097); if(getprop("instrumentation/nav/in-range")) me.symbols.vorLId.setText(getprop("instrumentation/nav/nav-id")); else me.symbols.vorLId.setText(getprop("instrumentation/nav/frequencies/selected-mhz-fmt")); me.symbols.vorLId.setColor(0.195,0.96,0.097); if(getprop("instrumentation/nav/dme-in-range")) me.symbols.dmeLDist.setText(sprintf("%3.1f",getprop("instrumentation/nav/nav-distance")*0.000539)); else me.symbols.dmeLDist.setText(" ---"); me.symbols.dmeLDist.setColor(0.195,0.96,0.097); } elsif(me.get_switch('toggle_lh_vor_adf') == -1) { me.symbols.vorL.setText("ADF L"); me.symbols.vorL.setColor(0,0.6,0.85); me.symbols.dmeL.setText(""); me.symbols.dmeL.setColor(0,0.6,0.85); if((var navident=getprop("instrumentation/adf/ident")) != "") me.symbols.vorLId.setText(navident); else me.symbols.vorLId.setText(sprintf("%3d",getprop("instrumentation/adf/frequencies/selected-khz"))); me.symbols.vorLId.setColor(0,0.6,0.85); me.symbols.dmeLDist.setText(""); me.symbols.dmeLDist.setColor(0,0.6,0.85); } else { me.symbols.vorL.setText(""); me.symbols.dmeL.setText(""); me.symbols.vorLId.setText(""); me.symbols.dmeLDist.setText(""); } if(me.get_switch('toggle_rh_vor_adf') == 1) { me.symbols.vorR.setText("VOR R"); me.symbols.vorR.setColor(0.195,0.96,0.097); me.symbols.dmeR.setText("DME"); me.symbols.dmeR.setColor(0.195,0.96,0.097); if(getprop("instrumentation/nav[1]/in-range")) me.symbols.vorRId.setText(getprop("instrumentation/nav[1]/nav-id")); else me.symbols.vorRId.setText(getprop("instrumentation/nav[1]/frequencies/selected-mhz-fmt")); me.symbols.vorRId.setColor(0.195,0.96,0.097); if(getprop("instrumentation/nav[1]/dme-in-range")) me.symbols.dmeRDist.setText(sprintf("%3.1f",getprop("instrumentation/nav[1]/nav-distance")*0.000539)); else me.symbols.dmeRDist.setText(" ---"); me.symbols.dmeRDist.setColor(0.195,0.96,0.097); } elsif(me.get_switch('toggle_rh_vor_adf') == -1) { me.symbols.vorR.setText("ADF R"); me.symbols.vorR.setColor(0,0.6,0.85); me.symbols.dmeR.setText(""); me.symbols.dmeR.setColor(0,0.6,0.85); if((var navident=getprop("instrumentation/adf[1]/ident")) != "") me.symbols.vorRId.setText(navident); else me.symbols.vorRId.setText(sprintf("%3d",getprop("instrumentation/adf[1]/frequencies/selected-khz"))); me.symbols.vorRId.setColor(0,0.6,0.85); me.symbols.dmeRDist.setText(""); me.symbols.dmeRDist.setColor(0,0.6,0.85); } else { me.symbols.vorR.setText(""); me.symbols.dmeR.setText(""); me.symbols.vorRId.setText(""); me.symbols.dmeRDist.setText(""); } me.symbols.range.setText(sprintf("%3.0f",me.rangeNm()/2)); # reposition the map, change heading & range: if(me.in_mode('toggle_display_mode', ['PLAN'])) { me.symbols.windArrow.setVisible(!dispLCD); me.map._node.getNode("hdg",1).setDoubleValue(0); if (getprop(me.efis_path ~ "/inputs/plan-wpt-index") >= 0) { me.map._node.getNode("ref-lat",1).setDoubleValue(getprop("/autopilot/route-manager/route/wp["~getprop(me.efis_path ~ "/inputs/plan-wpt-index")~"]/latitude-deg")); me.map._node.getNode("ref-lon",1).setDoubleValue(getprop("/autopilot/route-manager/route/wp["~getprop(me.efis_path ~ "/inputs/plan-wpt-index")~"]/longitude-deg")); } } else { me.symbols.windArrow.show(); me.map._node.getNode("ref-lat",1).setDoubleValue(userLat); me.map._node.getNode("ref-lon",1).setDoubleValue(userLon); } # The set range of the map does not correspond to what we see in-sim!! me.map._node.getNode("range",1).setDoubleValue(me.rangeNm()/3.2); # avoid this here, use a listener instead var vhdg_bug = getprop("autopilot/settings/heading-bug-deg"); if(me.in_mode('toggle_display_mode', ['MAP'])) { me.symbols.HdgBugCRT.setRotation((vhdg_bug-userTrk)*D2R); me.symbols.HdgBugLCD.setRotation((vhdg_bug-userTrk)*D2R); me.symbols.TrkBugLCD.setRotation((vhdg_bug-userTrk)*D2R); me.symbols.selHdgLine.setRotation((vhdg_bug-userTrk)*D2R); me.symbols.HdgBugCRT2.setRotation((vhdg_bug-userTrk)*D2R); me.symbols.TrkBugLCD2.setRotation((vhdg_bug-userTrk)*D2R); me.symbols.selHdgLine2.setRotation((vhdg_bug-userTrk)*D2R); me.symbols.trkInd.setRotation(0); me.symbols.curHdgPtr.setRotation((userHdg-userTrk)*D2R); me.symbols.curHdgPtr2.setRotation((userHdg-userTrk)*D2R); me.map._node.getNode("hdg",1).setDoubleValue(userTrkTru); me.symbols.compass.setRotation(-userTrk*D2R); me.symbols.compassApp.setRotation(-userTrk*D2R); me.symbols.hdgTrk.setText("TRK"); } if(me.in_mode('toggle_display_mode', ['APP','VOR'])) { me.symbols.HdgBugCRT.setRotation((vhdg_bug-userHdg)*D2R); me.symbols.HdgBugLCD.setRotation((vhdg_bug-userHdg)*D2R); me.symbols.selHdgLine.setRotation((vhdg_bug-userHdg)*D2R); me.symbols.HdgBugCRT2.setRotation((vhdg_bug-userHdg)*D2R); me.symbols.HdgBugLCD2.setRotation((vhdg_bug-userHdg)*D2R); me.symbols.selHdgLine2.setRotation((vhdg_bug-userHdg)*D2R); me.symbols.trkInd.setRotation((userTrk-userHdg)*D2R); me.symbols.curHdgPtr.setRotation(0); me.symbols.curHdgPtr2.setRotation(0); me.map._node.getNode("hdg",1).setDoubleValue(userHdgTru); me.symbols.compass.setRotation(-userHdg*D2R); me.symbols.compassApp.setRotation(-userHdg*D2R); me.symbols.hdgTrk.setText("HDG"); } if(me.get_switch('toggle_centered')) { if (me.in_mode('toggle_display_mode', ['APP','VOR'])) { me.symbols.vorCrsPtr2.show(); me.symbols.compassApp.show(); if(getprop("instrumentation/nav/in-range")) { var deflection = getprop("instrumentation/nav/heading-needle-deflection-norm"); me.symbols.locPtr.show(); me.symbols.locPtr.setTranslation(deflection*150,0); if(abs(deflection < 0.99)) me.symbols.locPtr.setColorFill(1,0,1,1); else me.symbols.locPtr.setColorFill(1,0,1,0); } else { me.symbols.locPtr.hide(); } me.symbols.vorCrsPtr2.setRotation((getprop("instrumentation/nav/radials/selected-deg")-userHdg)*D2R); me.symbols.hdgGroup.setTranslation(0,100); } else { me.symbols.vorCrsPtr2.hide(); me.symbols.hdgGroup.setTranslation(0,100*me.in_mode('toggle_display_mode', ['MAP'])); me.symbols.compassApp.setVisible(me.in_mode('toggle_display_mode', ['MAP'])); } } else { me.symbols.vorCrsPtr2.hide(); me.symbols.hdgGroup.setTranslation(0,0); me.symbols.compassApp.hide(); } if ((me.get_switch('toggle_centered') and !me.in_mode('toggle_display_mode', ['PLAN'])) or me.in_mode('toggle_display_mode', ['PLAN'])) { me.symbols.compass.hide(); } else { me.symbols.compass.show(); } var staPtrVis = !me.in_mode('toggle_display_mode', ['APP','PLAN']); var magVar = getprop("environment/magnetic-variation-deg"); if(me.in_mode('toggle_display_mode', ['APP','MAP','VOR','PLAN'])) { if(getprop("instrumentation/nav/heading-deg") != nil) var nav0hdg=getprop("instrumentation/nav/heading-deg") - userHdg - magVar; if(getprop("instrumentation/nav[1]/heading-deg") != nil) var nav1hdg=getprop("instrumentation/nav[1]/heading-deg") - userHdg - magVar; var adf0hdg=getprop("instrumentation/adf/indicated-bearing-deg"); var adf1hdg=getprop("instrumentation/adf[1]/indicated-bearing-deg"); if(!me.get_switch('toggle_centered')) { if(me.in_mode('toggle_display_mode', ['PLAN'])) me.symbols.trkInd.hide(); else me.symbols.trkInd.show(); if((getprop("instrumentation/nav/in-range") and me.get_switch('toggle_lh_vor_adf') == 1)) { me.symbols.staFromL.setVisible(staPtrVis); me.symbols.staToL.setVisible(staPtrVis); me.symbols.staFromL.setColor(0.195,0.96,0.097); me.symbols.staToL.setColor(0.195,0.96,0.097); me.symbols.staFromL.setRotation((nav0hdg+180)*D2R); me.symbols.staToL.setRotation(nav0hdg*D2R); } elsif(getprop("instrumentation/adf/in-range") and (me.get_switch('toggle_lh_vor_adf') == -1)) { me.symbols.staFromL.setVisible(staPtrVis); me.symbols.staToL.setVisible(staPtrVis); me.symbols.staFromL.setColor(0,0.6,0.85); me.symbols.staToL.setColor(0,0.6,0.85); me.symbols.staFromL.setRotation((adf0hdg+180)*D2R); me.symbols.staToL.setRotation(adf0hdg*D2R); } else { me.symbols.staFromL.hide(); me.symbols.staToL.hide(); } if((getprop("instrumentation/nav[1]/in-range") and me.get_switch('toggle_rh_vor_adf') == 1)) { me.symbols.staFromR.setVisible(staPtrVis); me.symbols.staToR.setVisible(staPtrVis); me.symbols.staFromR.setColor(0.195,0.96,0.097); me.symbols.staToR.setColor(0.195,0.96,0.097); me.symbols.staFromR.setRotation((nav1hdg+180)*D2R); me.symbols.staToR.setRotation(nav1hdg*D2R); } elsif(getprop("instrumentation/adf[1]/in-range") and (me.get_switch('toggle_rh_vor_adf') == -1)) { me.symbols.staFromR.setVisible(staPtrVis); me.symbols.staToR.setVisible(staPtrVis); me.symbols.staFromR.setColor(0,0.6,0.85); me.symbols.staToR.setColor(0,0.6,0.85); me.symbols.staFromR.setRotation((adf1hdg+180)*D2R); me.symbols.staToR.setRotation(adf1hdg*D2R); } else { me.symbols.staFromR.hide(); me.symbols.staToR.hide(); } me.symbols.staFromL2.hide(); me.symbols.staToL2.hide(); me.symbols.staFromR2.hide(); me.symbols.staToR2.hide(); me.symbols.curHdgPtr2.hide(); me.symbols.HdgBugCRT2.hide(); me.symbols.TrkBugLCD2.hide(); me.symbols.HdgBugLCD2.hide(); me.symbols.selHdgLine2.hide(); me.symbols.curHdgPtr.setVisible(staPtrVis); me.symbols.TrkBugLCD.hide(); me.symbols.HdgBugCRT.setVisible(staPtrVis and !dispLCD); me.symbols.HdgBugLCD.setVisible(staPtrVis and dispLCD); me.symbols.selHdgLine.setVisible(staPtrVis); } else { me.symbols.trkInd.hide(); if((getprop("instrumentation/nav/in-range") and me.get_switch('toggle_lh_vor_adf') == 1)) { me.symbols.staFromL2.setVisible(staPtrVis); me.symbols.staToL2.setVisible(staPtrVis); me.symbols.staFromL2.setColor(0.195,0.96,0.097); me.symbols.staToL2.setColor(0.195,0.96,0.097); me.symbols.staFromL2.setRotation((nav0hdg+180)*D2R); me.symbols.staToL2.setRotation(nav0hdg*D2R); } elsif(getprop("instrumentation/adf/in-range") and (me.get_switch('toggle_lh_vor_adf') == -1)) { me.symbols.staFromL2.setVisible(staPtrVis); me.symbols.staToL2.setVisible(staPtrVis); me.symbols.staFromL2.setColor(0,0.6,0.85); me.symbols.staToL2.setColor(0,0.6,0.85); me.symbols.staFromL2.setRotation((adf0hdg+180)*D2R); me.symbols.staToL2.setRotation(adf0hdg*D2R); } else { me.symbols.staFromL2.hide(); me.symbols.staToL2.hide(); } if((getprop("instrumentation/nav[1]/in-range") and me.get_switch('toggle_rh_vor_adf') == 1)) { me.symbols.staFromR2.setVisible(staPtrVis); me.symbols.staToR2.setVisible(staPtrVis); me.symbols.staFromR2.setColor(0.195,0.96,0.097); me.symbols.staToR2.setColor(0.195,0.96,0.097); me.symbols.staFromR2.setRotation((nav1hdg+180)*D2R); me.symbols.staToR2.setRotation(nav1hdg*D2R); } elsif(getprop("instrumentation/adf[1]/in-range") and (me.get_switch('toggle_rh_vor_adf') == -1)) { me.symbols.staFromR2.setVisible(staPtrVis); me.symbols.staToR2.setVisible(staPtrVis); me.symbols.staFromR2.setColor(0,0.6,0.85); me.symbols.staToR2.setColor(0,0.6,0.85); me.symbols.staFromR2.setRotation((adf1hdg+180)*D2R); me.symbols.staToR2.setRotation(adf1hdg*D2R); } else { me.symbols.staFromR2.hide(); me.symbols.staToR2.hide(); } me.symbols.staFromL.hide(); me.symbols.staToL.hide(); me.symbols.staFromR.hide(); me.symbols.staToR.hide(); me.symbols.curHdgPtr.hide(); me.symbols.HdgBugCRT.hide(); me.symbols.TrkBugLCD.hide(); me.symbols.HdgBugLCD.hide(); me.symbols.selHdgLine.hide(); me.symbols.curHdgPtr2.setVisible(staPtrVis); me.symbols.TrkBugLCD2.hide(); me.symbols.HdgBugCRT2.setVisible(staPtrVis and !dispLCD); me.symbols.HdgBugLCD2.setVisible(staPtrVis and dispLCD); me.symbols.selHdgLine2.setVisible(staPtrVis); } } me.symbols.hdgGroup.setVisible(!me.in_mode('toggle_display_mode', ['PLAN'])); me.symbols.northUp.setVisible(me.in_mode('toggle_display_mode', ['PLAN'])); me.symbols.aplSymMap.setVisible(me.in_mode('toggle_display_mode', ['APP','MAP','VOR']) and !me.get_switch('toggle_centered')); me.symbols.aplSymMapCtr.setVisible(me.in_mode('toggle_display_mode', ['MAP']) and me.get_switch('toggle_centered')); me.symbols.aplSymVor.setVisible(me.in_mode('toggle_display_mode', ['APP','VOR']) and me.get_switch('toggle_centered')); me.symbols.planArcs.setVisible(me.in_mode('toggle_display_mode', ['PLAN'])); if (abs(userVSpd) > 5) { var altDiff = getprop("autopilot/settings/target-altitude-ft")-getprop("instrumentation/altimeter/indicated-altitude-ft"); if (abs(altDiff) > 50 and altDiff/userVSpd > 0) { var altRangeNm = altDiff/userVSpd*userSpd*KT2MPS*M2NM; if(altRangeNm > 1) { var altRangePx = (350/me.rangeNm())*altRangeNm; if (altRangePx > 700) altRangePx = 700; me.symbols.altArc.setTranslation(0,-altRangePx); } me.symbols.altArc.show(); } else me.symbols.altArc.hide(); } else { me.symbols.altArc.hide(); } ## run all predicates in the NDStyle hash and evaluate their true/false behavior callbacks ## this is in line with the original design, but normally we don't need to getprop/poll here, ## using listeners or timers would be more canvas-friendly whenever possible ## because running setprop() on any group/canvas element at framerate means that the canvas ## will be updated at frame rate too - wasteful ... (check the performance monitor!) foreach(var feature; me.nd_style.features ) { # for stuff that always needs to be updated if (contains(feature.impl, 'common')) feature.impl.common(me); # conditional stuff if(!contains(feature.impl, 'predicate')) continue; # no conditional stuff if ( var result=feature.impl.predicate(me) ) feature.impl.is_true(me, result); # pass the result to the predicate else feature.impl.is_false( me, result ); # pass the result to the predicate } ## update the status flags shown on the ND (wxr, wpt, arpt, sta) # this could/should be using listeners instead ... me.symbols['status.wxr'].setVisible( me.get_switch('toggle_weather') and me.in_mode('toggle_display_mode', ['MAP'])); me.symbols['status.wpt'].setVisible( me.get_switch('toggle_waypoints') and me.in_mode('toggle_display_mode', ['MAP'])); me.symbols['status.arpt'].setVisible( me.get_switch('toggle_airports') and me.in_mode('toggle_display_mode', ['MAP'])); me.symbols['status.sta'].setVisible( me.get_switch('toggle_stations') and me.in_mode('toggle_display_mode', ['MAP'])); } };