# ============================================================================== # Boeing Navigation Display by Gijs de Rooy # See: http://wiki.flightgear.org/Canvas_ND_Framework # ============================================================================== # This file makes use of the MapStructure framework, see: http://wiki.flightgear.org/Canvas_MapStructure # # Sooner or later, some parts will be revamped by coming up with a simple animation framework: http://wiki.flightgear.org/NavDisplay#mapping_vs._SVG_animation ## # pseudo DSL-ish: use these as placeholders in the config hash below var ALWAYS = func 1; var NOTHING = func nil; ## # 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 (maybe supporting SGCondition and/or SGStateMachine markup for the logic?) # 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/Images/boeingND.svg", ## ## this loads and configures existing layers (currently, *.layer files in Nasal/canvas/map) ## # TODO: phase out isMapStructure flag once map.nas & *.draw files are killed layers: [ # TODO: take z-indices from *.draw files -- now handled by MapStructure in the addLayer method. { 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 fix 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 'z-index': 3, }, # end of FIX layer # Should redraw every 10 seconds TODO: use new MapStructure/WXR here once that works properly (Gijs should check first!) { name:'WXR', isMapStructure:1, update_on:[ {rate_hz: 0.1}, 'toggle_range','toggle_weather','toggle_display_mode'], predicate: func(nd, layer) { #print("Running storms predicate"); var visible=nd.get_switch('toggle_weather') and nd.get_switch('toggle_display_mode') != "PLAN"; layer.group.setVisible(visible); if (visible) { print("storms update requested! (timer issue when closing the dialog?)"); layer.update(); } }, # end of layer update predicate }, # end of storms/WXR layer { name:'APS', isMapStructure:1, update_on:['toggle_display_mode'], predicate: func(nd, layer) { var visible = nd.get_switch('toggle_display_mode') == "PLAN"; layer.group.setVisible( visible ); if (visible) { layer.update(); } }, }, { name:'APT', isMapStructure:1, update_on:['toggle_range','toggle_airports','toggle_display_mode'], predicate: func(nd, layer) { # toggle visibility here var visible=nd.get_switch('toggle_airports') and nd.in_mode('toggle_display_mode', ['MAP']); layer.group.setVisible( visible ); if (visible) { #print("Updating MapStructure ND layer: APT"); layer.update(); } }, # end of layer update predicate 'z-index': 1, }, # end of APT layer # Should distinct between low and high altitude navaids. Hiding above 40 NM for now, to prevent clutter/lag. { 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) { # 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"); layer.update(); } }, # end of layer update predicate 'z-index': 3, }, # end of VOR layer { name:'DME', isMapStructure:1, update_on:['toggle_range','toggle_stations'], # FIXME: this is a really ugly place for controller code predicate: func(nd, layer) { var visible = nd.get_switch('toggle_stations') and nd.in_mode('toggle_display_mode', ['MAP']) and (nd.rangeNm() <= 40); # toggle visibility here layer.group.setVisible( visible ); if (visible) { #print("Updating MapStructure ND layer: DME"); layer.update(); } }, # end of layer update predicate 'z-index': 3, }, # end of DME layer { name:'TFC', isMapStructure:1, update_on:['toggle_range','toggle_traffic'], predicate: func(nd, layer) { var visible = nd.get_switch('toggle_traffic'); layer.group.setVisible( visible ); if (visible) { #print("Updating MapStructure ND layer: TFC"); layer.update(); } }, # end of layer update predicate 'z-index': 1, }, # end of traffic layer { name:'runway-nd', update_on:['toggle_range','toggle_display_mode'], predicate: func(nd, layer) { #print("runway-nd wants to be ported to MapStructure"); var visible = (nd.rangeNm() <= 40) and getprop("autopilot/route-manager/active") and nd.in_mode('toggle_display_mode', ['MAP','PLAN']) ; if (visible) layer._model.init(); # clear & redraw layer._view.setVisible( visible ); }, # end of layer update predicate }, # end of airports-nd layer { name:'RTE', isMapStructure:1, update_on:['toggle_range','toggle_display_mode'], predicate: func(nd, layer) { var visible= (nd.in_mode('toggle_display_mode', ['MAP','PLAN'])); layer.group.setVisible( visible ); if (visible) layer.update(); }, # end of layer update predicate 'z-index': 2, # apparently route.draw doesn't have a z-index? }, # end of route layer { name:'WPT', isMapStructure:1, update_on:['toggle_range','toggle_display_mode'], predicate: func(nd, layer) { var visible= (nd.in_mode('toggle_display_mode', ['MAP','PLAN'])); layer.group.setVisible( visible ); if (visible) layer.update(); }, # end of layer update predicate 'z-index': 4, }, # end of waypoint layer { name:'ALT-profile', isMapStructure:1, update_on:['toggle_range','toggle_display_mode'], predicate: func(nd, layer) { var visible= (nd.in_mode('toggle_display_mode', ['MAP','PLAN'])); layer.group.setVisible( visible ); if (visible) layer.update(); }, # end of layer update predicate 'z-index': 4, }, # end of altitude profile 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) nd.aircraft_source.get_spd() > 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(), }, }, { id: 'tasLbl', impl: { init: func(nd,symbol), predicate: func(nd) nd.aircraft_source.get_spd() > 100, is_true: func(nd) nd.symbols.tasLbl.show(), is_false: func(nd) nd.symbols.tasLbl.hide(), }, }, { id: 'ilsFreq', impl: { init: func(nd,symbol), predicate: func(nd) nd.in_mode('toggle_display_mode', ['APP']), is_true: func(nd) { nd.symbols.ilsFreq.show(); if(getprop("instrumentation/nav/in-range")) nd.symbols.ilsFreq.setText(getprop("instrumentation/nav/nav-id")); else nd.symbols.ilsFreq.setText(getprop("instrumentation/nav/frequencies/selected-mhz-fmt")); }, is_false: func(nd) nd.symbols.ilsFreq.hide(), }, }, { id: 'ilsLbl', impl: { init: func(nd,symbol), predicate: func(nd) nd.in_mode('toggle_display_mode', ['APP']), is_true: func(nd) { nd.symbols.ilsLbl.show(); }, is_false: func(nd) nd.symbols.ilsLbl.hide(), }, }, { id: 'wpActiveId', impl: { init: func(nd,symbol), predicate: func(nd) getprop("/autopilot/route-manager/wp/id") != nil and getprop("autopilot/route-manager/active") and nd.in_mode('toggle_display_mode', ['MAP','PLAN']), 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") and nd.in_mode('toggle_display_mode', ['MAP','PLAN']), is_true: func(nd) { nd.symbols.wpActiveDist.setText(sprintf("%3.01f",getprop("/autopilot/route-manager/wp/dist"))); nd.symbols.wpActiveDist.show(); }, is_false: func(nd) nd.symbols.wpActiveDist.hide(), }, }, { id: 'wpActiveDistLbl', impl: { init: func(nd,symbol), predicate: func(nd) getprop("/autopilot/route-manager/wp/dist") != nil and getprop("autopilot/route-manager/active") and nd.in_mode('toggle_display_mode', ['MAP','PLAN']), is_true: func(nd) { nd.symbols.wpActiveDistLbl.show(); if(getprop("/autopilot/route-manager/wp/dist") > 1000) nd.symbols.wpActiveDistLbl.setText(" NM"); }, is_false: func(nd) nd.symbols.wpActiveDistLbl.hide(), }, }, { id: 'eta', impl: { init: func(nd,symbol), predicate: func(nd) getprop("autopilot/route-manager/wp/eta") != nil and getprop("autopilot/route-manager/active") and nd.in_mode('toggle_display_mode', ['MAP','PLAN']), 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); etaSec=etaSec-3600*h; var m = math.floor(etaSec/60); etaSec=etaSec-60*m; var s = etaSec/10; if (h>24) h=h-24; nd.symbols.eta.setText(sprintf("%02.0f%02.0f.%01.0fz",h,m,s)); nd.symbols.eta.show(); }, is_false: func(nd) nd.symbols.eta.hide(), }, # of eta.impl }, # of eta { id: 'gsGroup', impl: { init: func(nd,symbol), predicate: func(nd) nd.in_mode('toggle_display_mode', ['APP']), is_true: func(nd) { if(nd.get_switch('toggle_centered')) nd.symbols.gsGroup.setTranslation(0,0); else nd.symbols.gsGroup.setTranslation(0,150); nd.symbols.gsGroup.show(); }, is_false: func(nd) nd.symbols.gsGroup.hide(), }, }, { id:'hdg', impl: { init: func(nd,symbol), predicate: func(nd) nd.in_mode('toggle_display_mode', ['APP','MAP','VOR']), is_true: func(nd) { var hdgText = ""; if((nd.in_mode('toggle_display_mode', ['MAP']) and nd.get_switch('toggle_display_type') == "CRT") or (nd.get_switch('toggle_track_heading') and nd.get_switch('toggle_display_type') == "LCD")) { if(nd.get_switch('toggle_true_north')) hdgText = nd.aircraft_source.get_trk_tru(); else hdgText = nd.aircraft_source.get_trk_mag(); } else { if(nd.get_switch('toggle_true_north')) hdgText = nd.aircraft_source.get_hdg_tru(); else hdgText = nd.aircraft_source.get_hdg_mag(); } if(hdgText < 0.5) hdgText = 360 + hdgText; elsif(hdgText >= 360.5) hdgText = hdgText - 360; nd.symbols.hdg.setText(sprintf("%03.0f", hdgText)); }, is_false: NOTHING, }, }, { id:'hdgGroup', impl: { init: func(nd,symbol), predicate: func(nd) nd.in_mode('toggle_display_mode', ['APP','MAP','VOR']), is_true: func(nd) { nd.symbols.hdgGroup.show(); if(nd.get_switch('toggle_centered')) nd.symbols.hdgGroup.setTranslation(0,100); else nd.symbols.hdgGroup.setTranslation(0,0); }, is_false: func(nd) nd.symbols.hdgGroup.hide(), }, }, { id:'gs', impl: { init: func(nd,symbol), common: func(nd) nd.symbols.gs.setText(sprintf("%3.0f",nd.aircraft_source.get_gnd_spd() )), predicate: func(nd) nd.aircraft_source.get_gnd_spd() >= 30, is_true: func(nd) { nd.symbols.gs.setFontSize(36); }, is_false: func(nd) nd.symbols.gs.setFontSize(52), }, }, { id:'rangeArcs', impl: { init: func(nd,symbol), predicate: func(nd) !nd.get_switch('toggle_centered') and nd.get_switch('toggle_rangearc'), is_true: func(nd) nd.symbols.rangeArcs.show(), is_false: func(nd) nd.symbols.rangeArcs.hide(), }, # of rangeArcs.impl }, # of rangeArcs { id:'rangePln1', impl: { init: func(nd,symbol), predicate: func(nd) nd.get_switch('toggle_display_mode') == "PLAN", is_true: func(nd) { nd.symbols.rangePln1.show(); nd.symbols.rangePln1.setText(sprintf("%3.0f",nd.rangeNm())); }, is_false: func(nd) nd.symbols.rangePln1.hide(), }, }, { id:'rangePln2', impl: { init: func(nd,symbol), predicate: func(nd) nd.get_switch('toggle_display_mode') == "PLAN", is_true: func(nd) { nd.symbols.rangePln2.show(); nd.symbols.rangePln2.setText(sprintf("%3.0f",nd.rangeNm()/2)); }, is_false: func(nd) nd.symbols.rangePln2.hide(), }, }, { id:'rangePln3', impl: { init: func(nd,symbol), predicate: func(nd) nd.get_switch('toggle_display_mode') == "PLAN", is_true: func(nd) { nd.symbols.rangePln3.show(); nd.symbols.rangePln3.setText(sprintf("%3.0f",nd.rangeNm()/2)); }, is_false: func(nd) nd.symbols.rangePln3.hide(), }, }, { id:'rangePln4', impl: { init: func(nd,symbol), predicate: func(nd) nd.get_switch('toggle_display_mode') == "PLAN", is_true: func(nd) { nd.symbols.rangePln4.show(); nd.symbols.rangePln4.setText(sprintf("%3.0f",nd.rangeNm())); }, is_false: func(nd) nd.symbols.rangePln4.hide(), }, }, { id:'crsLbl', impl: { init: func(nd,symbol), predicate: func(nd) nd.in_mode('toggle_display_mode', ['APP','VOR']), is_true: func(nd) nd.symbols.crsLbl.show(), is_false: func(nd) nd.symbols.crsLbl.hide(), }, }, { id:'crs', impl: { init: func(nd,symbol), predicate: func(nd) nd.in_mode('toggle_display_mode', ['APP','VOR']), is_true: func(nd) { nd.symbols.crs.show(); if(getprop("instrumentation/nav/radials/selected-deg") != nil) nd.symbols.crs.setText(sprintf("%03.0f",getprop("instrumentation/nav/radials/selected-deg"))); }, is_false: func(nd) nd.symbols.crs.hide(), }, }, { id:'dmeLbl', impl: { init: func(nd,symbol), predicate: func(nd) nd.in_mode('toggle_display_mode', ['APP','VOR']), is_true: func(nd) nd.symbols.dmeLbl.show(), is_false: func(nd) nd.symbols.dmeLbl.hide(), }, }, { id:'dme', impl: { init: func(nd,symbol), predicate: func(nd) nd.in_mode('toggle_display_mode', ['APP','VOR']), is_true: func(nd) { nd.symbols.dme.show(); if(getprop("instrumentation/dme/in-range")) nd.symbols.dme.setText(sprintf("%3.1f",getprop("instrumentation/dme/indicated-distance-nm"))); }, is_false: func(nd) nd.symbols.dme.hide(), }, }, { id:'trkInd2', impl: { init: func(nd,symbol), predicate: func(nd) (nd.in_mode('toggle_display_mode', ['MAP','APP','VOR']) and nd.get_switch('toggle_centered')), is_true: func(nd) { nd.symbols.trkInd2.show(); }, is_false: func(nd) nd.symbols.trkInd2.hide(), }, }, { id:'vorCrsPtr', impl: { init: func(nd,symbol), predicate: func(nd) (nd.in_mode('toggle_display_mode', ['APP','VOR']) and !nd.get_switch('toggle_centered')), is_true: func(nd) { nd.symbols.vorCrsPtr.show(); if((nd.in_mode('toggle_display_mode', ['MAP']) and nd.get_switch('toggle_display_type') == "CRT") or (nd.get_switch('toggle_track_heading') and nd.get_switch('toggle_display_type') == "LCD")) nd.symbols.vorCrsPtr.setRotation((getprop("instrumentation/nav/radials/selected-deg")-nd.aircraft_source.get_trk_mag())*D2R); else nd.symbols.vorCrsPtr.setRotation((getprop("instrumentation/nav/radials/selected-deg")-nd.aircraft_source.get_hdg_mag())*D2R); }, is_false: func(nd) nd.symbols.vorCrsPtr.hide(), }, }, { id:'vorCrsPtr2', impl: { init: func(nd,symbol), predicate: func(nd) (nd.in_mode('toggle_display_mode', ['APP','VOR']) and nd.get_switch('toggle_centered')), is_true: func(nd) { nd.symbols.vorCrsPtr2.show(); if((nd.in_mode('toggle_display_mode', ['MAP']) and nd.get_switch('toggle_display_type') == "CRT") or (nd.get_switch('toggle_track_heading') and nd.get_switch('toggle_display_type') == "LCD")) nd.symbols.vorCrsPtr2.setRotation((getprop("instrumentation/nav/radials/selected-deg")-nd.aircraft_source.get_trk_mag())*D2R); else nd.symbols.vorCrsPtr2.setRotation((getprop("instrumentation/nav/radials/selected-deg")-nd.aircraft_source.get_hdg_mag())*D2R); }, is_false: func(nd) nd.symbols.vorCrsPtr2.hide(), }, }, { id: 'gsDiamond', impl: { init: func(nd,symbol), predicate: func(nd) nd.in_mode('toggle_display_mode', ['APP']) and getprop("instrumentation/nav/gs-in-range"), is_true: func(nd) { var gs_deflection = getprop("instrumentation/nav/gs-needle-deflection-norm"); if(gs_deflection != nil) nd.symbols.gsDiamond.setTranslation(gs_deflection*150,0); if(abs(gs_deflection) < 0.99) nd.symbols.gsDiamond.setColorFill(1,0,1,1); else nd.symbols.gsDiamond.setColorFill(0,0,0,1); }, is_false: func(nd) nd.symbols.gsGroup.hide(), }, }, { id:'locPtr', impl: { init: func(nd,symbol), predicate: func(nd) (nd.in_mode('toggle_display_mode', ['APP','VOR']) and !nd.get_switch('toggle_centered') and getprop("instrumentation/nav/in-range")), is_true: func(nd) { nd.symbols.locPtr.show(); var deflection = getprop("instrumentation/nav/heading-needle-deflection-norm"); nd.symbols.locPtr.setTranslation(deflection*150,0); if(abs(deflection) < 0.99) nd.symbols.locPtr.setColorFill(1,0,1,1); else nd.symbols.locPtr.setColorFill(0,0,0,1); }, is_false: func(nd) nd.symbols.locPtr.hide(), }, }, { id:'locPtr2', impl: { init: func(nd,symbol), predicate: func(nd) (nd.in_mode('toggle_display_mode', ['APP','VOR']) and nd.get_switch('toggle_centered') and getprop("instrumentation/nav/in-range")), is_true: func(nd) { nd.symbols.locPtr2.show(); var deflection = getprop("instrumentation/nav/heading-needle-deflection-norm"); nd.symbols.locPtr2.setTranslation(deflection*150,0); if(abs(deflection) < 0.99) nd.symbols.locPtr2.setColorFill(1,0,1,1); else nd.symbols.locPtr2.setColorFill(0,0,0,1); }, is_false: func(nd) nd.symbols.locPtr2.hide(), }, }, { id:'wind', impl: { init: func(nd,symbol), predicate: ALWAYS, is_true: func(nd) { var windDir = getprop("environment/wind-from-heading-deg"); if(!nd.get_switch('toggle_true_north')) windDir = windDir - getprop("environment/magnetic-variation-deg"); if(windDir < 0.5) windDir = 360 + windDir; elsif(windDir >= 360.5) windDir = windDir - 360; nd.symbols.wind.setText(sprintf("%03.0f / %02.0f",windDir,getprop("environment/wind-speed-kt"))); }, is_false: NOTHING, }, }, { id:'windArrow', impl: { init: func(nd,symbol), predicate: func(nd) (!(nd.in_mode('toggle_display_mode', ['PLAN']) and (nd.get_switch('toggle_display_type') == "LCD")) and nd.aircraft_source.get_spd() > 100), is_true: func(nd) { nd.symbols.windArrow.show(); var windArrowRot = getprop("environment/wind-from-heading-deg"); if((nd.in_mode('toggle_display_mode', ['MAP','PLAN']) and nd.get_switch('toggle_display_type') == "CRT") or (nd.get_switch('toggle_track_heading') and nd.get_switch('toggle_display_type') == "LCD")) windArrowRot = windArrowRot - nd.aircraft_source.get_trk_mag(); else windArrowRot = windArrowRot - nd.aircraft_source.get_hdg_mag(); nd.symbols.windArrow.setRotation(windArrowRot*D2R); }, is_false: func(nd) nd.symbols.windArrow.hide(), }, }, ], # end of vector with features }, # end of Boeing style ##### ## ## add support for other aircraft/ND types and styles here (Airbus etc) ## or move to other files. ## ## see: http://wiki.flightgear.org/NavDisplay#Custom_ND_Styles ## and: http://wiki.flightgear.org/NavDisplay#Adding_new_features }; # end of NDStyles