a9576e8c8d
- get rid of global variables and use instance variables - identified all important drawing routines and move them into *.draw files - changed to dynamic loading of *.draw *.model and *.layer files - implemented poor-man's controller hash to move use-case specific conditionals out of the draw files, and back into the instantiation, i.e. Gijs' EFIS class - started identifying stuff that is not specific to drawing, but to what is to be drawn, i.e. Model stuff - such as positioned queries, moved those out into *.model files - some more work on supporting more than a single ND MFD instance per aircraft - renamed a handful of SVG identifiers to avoid naming conflicts and to simplify usage of SVG IDs as member fields - moved all of the setlistener setup out of the fdm-initialized stub right into the ctor of the Efis class (actually that's controller stuff...) - initial MapStructure framework - aircraft-agnostic NavDisplay class - preparations for deprecating map.nas - additions to canvas.map - preparations for making NDStyles configurable via XML
648 lines
25 KiB
Text
648 lines
25 KiB
Text
# ==============================================================================
|
|
# Boeing Navigation Display by Gijs de Rooy (currently specific to the 744)
|
|
# ==============================================================================
|
|
|
|
|
|
##
|
|
# 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
|
|
#
|
|
# Note to Gijs: this may look weird and confusing, but it' actually requires
|
|
# less coding now, and it is now even possible to configure things via a little
|
|
# XML wrapper
|
|
# TODO: move this to an XML config file
|
|
#
|
|
var NDStyles = {
|
|
##
|
|
# this configures the 744 ND to help generalize the NavDisplay class itself
|
|
'B747-400': {
|
|
font_mapper: func(family, weight) {
|
|
if( family == "Liberation Sans" and weight == "normal" )
|
|
return "LiberationFonts/LiberationSans-Regular.ttf";
|
|
},
|
|
|
|
# where all the symbols are stored
|
|
# TODO: the SVG image should be moved to the canvas folder
|
|
# so that it can be shared by other aircraft, i.e. no 744 dependencies
|
|
# also 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: "Aircraft/747-400/Models/Cockpit/Instruments/ND/ND.svg",
|
|
|
|
##
|
|
## this loads and configures existing layers (currently, *.layer files in Nasal/canvas/map)
|
|
##
|
|
|
|
layers: [
|
|
{ name:'fixes', update_on:['toggle_range','toggle_waypoints'], predicate: func(nd, layer) {
|
|
# print("Running fixes predicate");
|
|
var visible=nd.get_switch('toggle_waypoints');
|
|
if(nd.rangeNm() <= 40 and visible and
|
|
nd.get_switch('toggle_display_mode') == "MAP") {
|
|
# print("fixes update requested!");
|
|
trigger_update( layer );
|
|
} layer._view.setVisible(visible);
|
|
|
|
}, # end of layer update predicate
|
|
}, # end of fixes layer
|
|
|
|
{ name:'airports-nd', update_on:['toggle_range','toggle_airports','toggle_display_mode'], predicate: func(nd, layer) {
|
|
# print("Running airports-nd predicate");
|
|
if (nd.rangeNm() <= 80 and
|
|
nd.get_switch('toggle_display_mode') == "MAP" ) {
|
|
trigger_update( layer ); # clear & redraw
|
|
}
|
|
layer._view.setVisible( nd.get_switch('toggle_airports') );
|
|
|
|
}, # end of layer update predicate
|
|
}, # end of airports layer
|
|
|
|
{ name:'vor', update_on:['toggle_range','toggle_stations','toggle_display_mode'], predicate: func(nd, layer) {
|
|
# print("Running vor layer predicate");
|
|
if(nd.rangeNm() <= 40 and
|
|
nd.get_switch('toggle_stations') and
|
|
nd.get_switch('toggle_display_mode') == "MAP"){
|
|
trigger_update( layer ); # clear & redraw
|
|
}
|
|
layer._view.setVisible( nd.get_switch('toggle_stations') );
|
|
}, # end of layer update predicate
|
|
}, # end of VOR layer
|
|
|
|
{ name:'dme', update_on:['toggle_range','toggle_stations'], predicate: func(nd, layer) {
|
|
if(nd.rangeNm() <= 40 and
|
|
nd.get_switch('toggle_stations') and
|
|
nd.get_switch('toggle_display_mode') == "MAP"){
|
|
trigger_update( layer ); # clear & redraw
|
|
}
|
|
layer._view.setVisible( nd.get_switch('toggle_stations') );
|
|
}, # 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
|
|
|
|
{ name:'altitude-arc', not_a_map:1, update_on:['toggle_range','toggle_display_mode'], predicate: func(nd, layer) {
|
|
trigger_update( layer ); # clear & redraw
|
|
layer._view.setVisible( 1 );
|
|
}, # end of layer update predicate
|
|
}, # end of alt-arc 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: 'eta',
|
|
impl: {
|
|
init: func(nd,symbol),
|
|
predicate: func(nd) getprop("autopilot/route-manager/wp/eta") != nil,
|
|
is_true: func(nd) {
|
|
var eta=getprop("autopilot/route-manager/wp/eta");
|
|
var etaWp = split(":",eta);
|
|
var h = getprop("/sim/time/utc/hour");
|
|
var m = getprop("/sim/time/utc/minute")+sprintf("%02f",etaWp[0]);
|
|
var s = getprop("/sim/time/utc/second")+sprintf("%02f",etaWp[1]);
|
|
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() )),
|
|
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:'compass',
|
|
impl: {
|
|
init: func(nd,symbol) nd.getElementById(symbol.id).updateCenter(),
|
|
common: func(nd) ,
|
|
predicate: func(nd) (nd.in_mode('toggle_display_mode', ['APP','MAP','PLAN','VOR'] )),
|
|
is_true: func(nd) {
|
|
# # orientation/track-magnetic-deg is noisy
|
|
nd.symbols.compass.setRotation(-nd.aircraft_source.get_hdg() *D2R);
|
|
nd.symbols.compass.show();
|
|
},
|
|
is_false: func(nd) nd.symbols.compass.hide(),
|
|
}, # of compass.impl
|
|
}, # of compass
|
|
|
|
|
|
|
|
|
|
], # end of vector with features
|
|
|
|
|
|
}, # end of 744 ND style
|
|
#####
|
|
##
|
|
## add support for other aircraft/ND types and styles here (737, 757, 777 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= func 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");
|
|
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_display_mode': {path: '/mfd/display-mode', value:'MAP', type:'STRING'},
|
|
};
|
|
|
|
##
|
|
# 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: func source.getNode('orientation/true-heading-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='B747-400') {
|
|
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.mfd_display_mode = m.mfd.initNode("display-mode",m.mfd_mode_list[2],"STRING");
|
|
m.std_mode = m.efis.initNode("inputs/setting-std",0,"BOOL");
|
|
m.previous_set = m.efis.initNode("inhg-previos",29.92); # watch out typo here, check other files before fixing !
|
|
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.nd_centered = m.efis.initNode("inputs/nd-centered",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.rh_vor_adf = m.efis.initNode("inputs/rh-vor-adf",0,"INT"); # not yet in switches hash
|
|
m.lh_vor_adf = m.efis.initNode("inputs/lh-vor-adf",0,"INT"); # not yet in switches hash
|
|
m.nd_plan_wpt = m.efis.initNode("inputs/plan-wpt-index", 0, "INT"); # ditto
|
|
|
|
###
|
|
# 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; ["wpActiveId","wpActiveDist","wind",
|
|
"dmeLDist","dmeRDist","vorLId","vorRId",
|
|
"range","status.wxr","status.wpt",
|
|
"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; ["rotateComp","windArrow","selHdg",
|
|
"curHdgPtr","staFromL","staToL",
|
|
"staFromR","staToR"] )
|
|
me.symbols[element] = me.nd.getElementById(element).updateCenter();
|
|
|
|
# this should probably be using Philosopher's new SymbolLayer ?
|
|
me.map = me.nd.createChild("map","map")
|
|
.setTranslation(512,824)
|
|
.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,
|
|
};
|
|
|
|
###
|
|
# 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) {
|
|
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 = me.layers[layer.name] = canvas.MAP_LAYERS[layer.name].new( render_target, layer.name, controller );
|
|
|
|
# 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
|
|
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;
|
|
else continue;
|
|
print("not in checked mode");
|
|
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');
|
|
|
|
var userHdg = me.aircraft_source.get_hdg();
|
|
var userTrkMag = me.aircraft_source.get_hdg(); # getprop("orientation/heading-deg"); # orientation/track-magnetic-deg is noisy
|
|
var userLat = me.aircraft_source.get_lat();
|
|
var userLon = me.aircraft_source.get_lon();
|
|
|
|
# this should only ever happen when testing the experimental AI/MP ND driver hash (not critical)
|
|
if (!userHdg or !userTrkMag or !userLat or !userLon) {
|
|
print("aircraft source invalid, returning !");
|
|
return;
|
|
}
|
|
|
|
# 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 ((var navid0=getprop("instrumentation/nav/nav-id"))!=nil )
|
|
me.symbols.vorLId.setText(navid0);
|
|
if ((var navid1=getprop("instrumentation/nav[1]/nav-id"))!=nil )
|
|
me.symbols.vorRId.setText(navid1);
|
|
if((var nav0dist=getprop("instrumentation/nav/nav-distance"))!=nil )
|
|
me.symbols.dmeLDist.setText(sprintf("%3.1f",nav0dist*0.000539));
|
|
if((var nav1dist=getprop("instrumentation/nav[1]/nav-distance"))!=nil )
|
|
me.symbols.dmeRDist.setText(sprintf("%3.1f",nav1dist*0.000539));
|
|
|
|
me.symbols.range.setText(sprintf("%3.0f",me.rangeNm() ));
|
|
#rangeNm=rangeNm*2;
|
|
|
|
# updates two SVG symbols, should use a listener specified in the config hash
|
|
if(getprop("/autopilot/route-manager/active")) {
|
|
me.symbols.wpActiveId.setText(getprop("/autopilot/route-manager/wp/id"));
|
|
me.symbols.wpActiveDist.setText(sprintf("%3.01fNM",getprop("/autopilot/route-manager/wp/dist")));
|
|
}
|
|
|
|
# reposition the map, change heading & range:
|
|
me.map._node.getNode("ref-lat",1).setDoubleValue(userLat);
|
|
me.map._node.getNode("ref-lon",1).setDoubleValue(userLon);
|
|
me.map._node.getNode("hdg",1).setDoubleValue(userHdg); # should also be using a listener for this
|
|
me.map._node.getNode("range",1).setDoubleValue(me.rangeNm()/2); # avoid this here, use a listener instead
|
|
|
|
|
|
me.symbols.rotateComp.setRotation(-userTrkMag*D2R);
|
|
|
|
## these would require additional arguments to be moved to an external config hash currently
|
|
me.symbols.curHdgPtr.setRotation(userHdg*D2R);
|
|
me.symbols.selHdg.setRotation(getprop("autopilot/settings/true-heading-deg")*D2R);
|
|
if (var nav0hdg=getprop("instrumentation/nav/heading-deg") != nil)
|
|
me.symbols.staFromL.setRotation((nav0hdg-userHdg+180)*D2R);
|
|
if (var nav0hdg=getprop("instrumentation/nav/heading-deg") != nil)
|
|
me.symbols.staToL.setRotation((nav0hdg-userHdg)*D2R);
|
|
if (var nav1hdg=getprop("instrumentation/nav[1]/heading-deg") != nil)
|
|
me.symbols.staFromR.setRotation((nav1hdg-userHdg+180)*D2R);
|
|
if (var nav1hdg=getprop("instrumentation/nav[1]/heading-deg") != nil)
|
|
me.symbols.staToR.setRotation((nav1hdg-userHdg)*D2R);
|
|
|
|
|
|
## 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) ) {
|
|
# print("Update predicate true for ", feature.id);
|
|
feature.impl.is_true(me, result); # pass the result to the predicate
|
|
}
|
|
else {
|
|
# print("Update predicate false for ", feature.id);
|
|
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') );
|
|
me.symbols['status.wpt'].setVisible( me.get_switch('toggle_waypoints'));
|
|
me.symbols['status.arpt'].setVisible( me.get_switch('toggle_airports'));
|
|
me.symbols['status.sta'].setVisible( me.get_switch('toggle_stations') );
|
|
|
|
|
|
}
|
|
};
|