1
0
Fork 0
fgdata/Nasal/canvas/MFD_Generic.nas

444 lines
16 KiB
Text
Raw Normal View History

# Generic Page switching cockpit display device.
# ---------------------------
# Richard Harrison: 2015-10-17 : rjh@zaretto.com
# ---------------------------
# I'm calling this a PFD as in Programmable Function Display.
# ---------------------------
# documentation: see http://wiki.flightgear.org/Canvas_MFD_Framework
# See FGAddon/Aircraft/F-15/Nasal/MPCD/MPCD_main.nas for an example usage
# ---------------------------
# This is but a straightforwards wrapper to provide the core logic that page switching displays require.
# Examples of Page switching displays
# * MFD
# * PFD
# * FMCS
# * F-15 MPCD
#
# Menu Item. There is a list of these for each page changing button per display page
# Parameters:
2017-11-24 23:04:46 +00:00
# menu_id : page change event id for this menu item. e.g. button number
# title : Title Text (for display on the device)
# page : Instance of page usually returned from PFD.addPage
# callbackfn : Function to call when menu item is selected
# displayfn : Function to call when the menu item is displayed. Used to enable
# highlighting of menu items, for example.
var PFD_MenuItem =
{
2017-11-24 23:04:46 +00:00
new : func (menu_id, title, page, callbackfn=nil, displayfn=nil)
{
var obj = {parents : [PFD_MenuItem] };
obj.page = page;
obj.menu_id = menu_id;
2017-11-24 23:04:46 +00:00
obj.callbackfn = callbackfn;
obj.displayfn = displayfn;
obj.title = title;
return obj;
},
};
#
#
# Create a new PFD Page
# - related svg
# - Title: Page title
# - SVG element for the page
# - Device to attach the page to
var PFD_Page =
{
new : func (svg, title, layer_id, device)
{
var obj = {parents : [PFD_Page] };
obj.title = title;
obj.device = device;
obj.layer_id = layer_id;
obj.menus = [];
obj.svg = svg.getElementById(layer_id);
if(obj.svg == nil)
printf("PFD_Device: Error loading %s: svg layer %s ",title, layer_id);
return obj;
},
#
# Makes a page visible.
# It is the responsibility of the caller to manage the visibility of pages - i.e. to
# make a page that is currenty visible not visible before making a new page visible,
# however more than one page could be visible - but only one set of menu buttons can be active
# so if two pages are visible (e.g. an overlay) then when the overlay removed it would be necessary
2017-11-24 23:04:46 +00:00
# to call setVisible on the base page to ensure that the menus are setup
setVisible : func(vis)
{
if(me.svg != nil)
me.svg.setVisible(vis);
if (vis)
me.ondisplay();
else
me.offdisplay();
},
2017-11-24 23:04:46 +00:00
# Standard callback for buttons, causing the appropriate page to be displayed
std_callbackfn : func (device, me, mi)
{
device.selectPage(mi.page);
},
# Standard display function for buttons, displaying the text and making visible
std_displayfn : func(svg_element, menuitem)
{
svg_element.setText(menuitem.title);
svg_element.setVisible(1);
#me.buttons[mi.menu_id].setText(mi.title);
#me.buttons[mi.menu_id].setVisible(1);
},
#
# Perform action when button is pushed
notifyButton : func(button_id)
{ foreach(var mi; me.menus)
{
if (mi.menu_id == button_id)
{
2017-11-24 23:04:46 +00:00
if (mi.callbackfn != nil) mi.callbackfn(me.device, me, mi);
break;
}
}
},
#
# Add an item to a menu
# Params:
# menu button id (that is set in controls/PFD/button-pressed by the model)
# title of the menu for the label
# page that will be selected when pressed
#
# The corresponding menu for the selected page will automatically be loaded
2017-11-24 23:04:46 +00:00
addMenuItem : func(menu_id, title, page, callbackfn=nil, displayfn=nil)
{
2017-11-24 23:04:46 +00:00
if (callbackfn == nil) callbackfn = me.std_callbackfn;
if (displayfn == nil) displayfn = me.std_displayfn;
var nm = PFD_MenuItem.new(menu_id, title, page, callbackfn, displayfn);
append(me.menus, nm);
return nm;
},
2017-11-24 23:04:46 +00:00
#
# Clear all items from the menu. Use-case is where they may be a hierarchy
# of menus within the same page.
#
clearMenu : func()
{
me.menus = [];
},
# base method for update; this can be overridden per page instance to provide update of the
# elements on display (e.g. to display updated properties)
update : func(notification=nil)
{
},
#
# notify the page that it is being displayed. use to load any static framework or perform one
# time initialisation
ondisplay : func
{
},
#
# notify the page that it is going off display; use to clean up any created elements or perform
# any other required functions
offdisplay : func
{
},
};
#
# Container device for pages.
var PFD_Device =
{
# - svg is the page elements from the svg.
# - num_menu_buttons is the Number of menu buttons; starting from the bottom left then right, then top, then left.
# - button prefix (e.g MI_) is the prefix of the labels in the SVG for the menu boxes.
# - _canvas is the canvas group.
2017-11-24 23:04:46 +00:00
# - designation (optional) is used for Emesary designation
#NOTE:
# This does not actually create the canvas elements, or parse the SVG, that would typically be done in
# a higher level class that contains an instance of this class.
# see: http://wiki.flightgear.org/Canvas_MFD_Framework
new : func(svg, num_menu_buttons, button_prefix, _canvas, designation="MFD")
{
var obj = {parents : [PFD_Device] };
obj.svg = svg;
obj.canvas = _canvas;
obj.current_page = nil;
obj.pages = [];
obj.page_index = {};
obj.buttons = setsize([], num_menu_buttons);
obj.transmitter = nil;
# change after creation if required
obj.device_id = 1;
obj.designation = designation;
for(var idx = 0; idx < num_menu_buttons; idx += 1)
{
var label_name = sprintf(button_prefix~"%d",idx);
var msvg = obj.svg.getElementById(label_name);
if (msvg == nil)
printf("PFD_Device: Failed to load %s",label_name);
else
{
obj.buttons[idx] = msvg;
2017-11-24 23:04:46 +00:00
obj.buttons[idx].setText(sprintf("M%d",idx));
}
}
obj.Recipient = nil;
return obj;
},
#
# instead of using the direct call method this allows the use of Emesary (via a specified or default global transmitter)
# example to notify that a softkey has been used. The "1" in the line below is the device ID
# var notification = notifications.PFDEventNotification.new(me.designation, me.DeviceId, notifications.PFDEventNotification.SoftKeyPushed, me.mpcd_button_pushed);
# emesary.GlobalTransmitter.NotifyAll(notification);
# - currently supported is
# 1. setting menu text directly (after page has been loaded)
# notifications.PFDEventNotification.new(me.designation, 1, notifications.PFDEventNotification.ChangeMenuText, [{ Id: 1, Text: "NNN"}]);
# 2. SoftKey selection.
#
# the device ID must match this device ID (to allow for multiple devices).
RegisterWithEmesary : func(transmitter = nil){
if (transmitter == nil)
transmitter = emesary.GlobalTransmitter;
if (me.Recipient == nil){
me.Recipient = emesary.Recipient.new("PFD_"~me.designation);
var pfd_obj = me;
me.Recipient.Receive = func(notification)
{
2017-11-24 23:04:46 +00:00
if (notification.Device_Id == pfd_obj.device_id
and notification.NotificationType == notifications.PFDEventNotification.DefaultType) {
if (notification.Event_Id == notifications.PFDEventNotification.SoftKeyPushed
and notification.EventParameter != nil)
{
2017-11-24 23:04:46 +00:00
#printf("Button pressed " ~ notification.EventParameter);
pfd_obj.notifyButton(notification.EventParameter);
}
else if (notification.Event_Id == notifications.PFDEventNotification.ChangeMenuText
and notification.EventParameter != nil)
{
foreach(var eventMenu; notification.EventParameter) {
2017-11-24 23:04:46 +00:00
#printf("Menu Text changed : " ~ eventMenu.Text);
foreach (var mi ; pfd_obj.current_page.menus) {
if (pfd_obj.buttons[eventMenu.Id] != nil) {
pfd_obj.buttons[eventMenu.Id].setText(eventMenu.Text);
}
else
printf("PFD_device: Menu for button not found. Menu ID '%s'",mi.menu_id);
}
}
}
return emesary.Transmitter.ReceiptStatus_OK;
}
return emesary.Transmitter.ReceiptStatus_NotProcessed;
};
transmitter.Register(me.Recipient);
me.transmitter = transmitter;
}
},
DeRegisterWithEmesary : func(transmitter = nil){
# remove registration from transmitter; but keep the recipient once it is created.
if (me.transmitter != nil)
me.transmitter.DeRegister(me.Recipient);
me.transmitter = nil;
},
#
# called when a button is pushed - connecting the property to this method is implemented in the outer class
notifyButton : func(button_id)
{
#
# by convention the buttons we have are 0 based; however externally 0 is used
# to indicate no button pushed.
if (button_id > 0)
{
button_id = button_id - 1;
if (me.current_page != nil)
{
me.current_page.notifyButton(button_id);
}
else
printf("PFD_Device: Could not locate page for button ",button_id);
}
},
#
#
# add a page to the device.
# - page title.
# - svg element id
addPage : func(title, layer_id)
{
var np = PFD_Page.new(me.svg, title, layer_id, me);
append(me.pages, np);
me.page_index[layer_id] = np;
np.setVisible(0);
return np;
},
#
2017-11-24 23:04:46 +00:00
# Get a named page
#
getPage : func(title)
{
foreach(var p; me.pages) {
if (p.title == title) return p;
}
return nil;
},
#
# manage the update of the currently selected page
update : func(notification=nil)
{
if (me.current_page != nil)
me.current_page.update(notification);
},
#
# Change to display the selected page.
# - the page object method controls the visibility
selectPage : func(p)
{
2017-11-24 23:04:46 +00:00
if (me.current_page == p) return;
if (me.current_page != nil)
me.current_page.setVisible(0);
if (me.buttons != nil)
{
foreach(var mb ; me.buttons)
if (mb != nil)
mb.setVisible(0);
foreach(var mi ; p.menus)
{
if (me.buttons[mi.menu_id] != nil)
{
2017-11-24 23:04:46 +00:00
mi.displayfn(me.buttons[mi.menu_id], mi);
}
else
printf("PFD_device: Menu for button not found. Menu ID '%s'",mi.menu_id);
}
}
p.setVisible(1);
me.current_page = p;
},
# Return the current selected page.
getCurrentPage : func()
{
return me.current_page;
},
#
# ensure that the menus are display correctly for the current page.
updateMenus : func
{
2017-11-24 23:04:46 +00:00
foreach(var mb ; me.buttons)
if (mb != nil)
mb.setVisible(0);
if (me.current_page == nil) return;
foreach(var mi ; me.current_page.menus)
{
if (me.buttons[mi.menu_id] != nil)
{
2017-11-24 23:04:46 +00:00
mi.displayfn(me.buttons[mi.menu_id], mi);
}
else
printf("No corresponding item '%s'",mi.menu_id);
}
},
};
var PFD_NavDisplay =
{
#
# Instantiate parameters:
# 1. pfd_device (instance of PFD_Device)
# 2. instrument display ident (e.g. mfd-map, or mfd-map-left mfd-map-right for multiple displays)
# (this is used to map to the property tree)
# 3. layer_id: main layer in the SVG
# 4. nd_group_ident : group (usually within the main layer) to place the NavDisplay
# 5. switches - used to connect the property tree to the nav display. see the canvas nav display
# documentation
new : func (pfd_device, title, instrument_ident, layer_id, nd_group_ident, switches=nil, map_style="Boeing")
{
var obj = pfd_device.addPage(title, layer_id);
# if no switches given then use a default set.
if (switches != nil)
obj.switches = switches;
else
obj.switches = {
'toggle_range': { path: '/inputs/range-nm', value: 50, type: 'INT' },
'toggle_weather': { path: '/inputs/wxr', value: 0, type: 'BOOL' },
'toggle_airports': { path: '/inputs/arpt', value: 1, type: 'BOOL' },
'toggle_stations': { path: '/inputs/sta', value: 1, type: 'BOOL' },
'toggle_waypoints': { path: '/inputs/wpt', value: 1, 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/tfc', value: 1, type: 'BOOL' },
'toggle_centered': { path: '/inputs/nd-centered', value: 1, type: 'BOOL' },
'toggle_lh_vor_adf': { path: '/inputs/lh-vor-adf', value: 1, type: 'INT' },
'toggle_rh_vor_adf': { path: '/inputs/rh-vor-adf', value: 1, type: 'INT' },
'toggle_display_mode': { path: '/mfd/display-mode', value: 'MAP', type: 'STRING' },
'toggle_display_type': { path: '/mfd/display-type', value: 'LCD', type: 'STRING' },
'toggle_true_north': { path: '/mfd/true-north', value: 0, type: 'BOOL' },
'toggle_rangearc': { path: '/mfd/rangearc', value: 0, type: 'BOOL' },
'toggle_track_heading': { path: '/hdg-trk-selected', value: 1, type: 'BOOL' },
};
obj.nd_initialised = 0;
obj.nd_placeholder_ident = nd_group_ident;
obj.nd_ident = instrument_ident;
obj.pfd_device = pfd_device;
obj.nd_init = func
{
me.ND = canvas.NavDisplay;
if (!me.nd_initialised)
{
me.nd_initialised = 1;
me.NDCpt = me.ND.new("instrumentation/"~me.nd_ident, me.switches,map_style);
me.group = me.pfd_device.svg.getElementById(me.nd_placeholder_ident);
me.group.setScale(0.39,0.45);
me.group.setTranslation(45,0);
me.NDCpt.newMFD(me.group, pfd_device.canvas);
}
me.NDCpt.update();
};
#
# Method overrides
#-----------------------------------------------
# Called when the page goes on display - need to delay initialization of the NavDisplay until later (it fails
# if done too early).
# NOTE: This causes a display "wobble" the first time on display as resizing happens. I've seen similar things
# happen on real avionics (when switched on) so it's not necessarily unrealistic -)
obj.ondisplay = func
{
if (!me.nd_initialised)
me.nd_init();
};
#
# most updates performed by the canvas nav display directly.
obj.update = func
{
};
return obj;
},
};