1# Generic Page switching cockpit display device. 2# --------------------------- 3# Richard Harrison: 2015-10-17 : rjh@zaretto.com 4# --------------------------- 5# I'm calling this a PFD as in Programmable Function Display. 6# --------------------------- 7# documentation: see http://wiki.flightgear.org/Canvas_MFD_Framework 8# See FGAddon/Aircraft/F-15/Nasal/MPCD/MPCD_main.nas for an example usage 9# --------------------------- 10# This is but a straightforwards wrapper to provide the core logic that page switching displays require. 11# Examples of Page switching displays 12# * MFD 13# * PFD 14# * FMCS 15# * F-15 MPCD 16 17# 18# Menu Item. There is a list of these for each page changing button per display page 19# Parameters: 20# menu_id : page change event id for this menu item. e.g. button number 21# title : Title Text (for display on the device) 22# page : Instance of page usually returned from PFD.addPage 23# callbackfn : Function to call when menu item is selected 24# displayfn : Function to call when the menu item is displayed. Used to enable 25# highlighting of menu items, for example. 26var PFD_MenuItem = 27{ 28 new : func (menu_id, title, page, callbackfn=nil, displayfn=nil) 29 { 30 var obj = {parents : [PFD_MenuItem] }; 31 obj.page = page; 32 obj.menu_id = menu_id; 33 obj.callbackfn = callbackfn; 34 obj.displayfn = displayfn; 35 obj.title = title; 36 return obj; 37 }, 38 }; 39 40# 41# 42# Create a new PFD Page 43# - related svg 44# - Title: Page title 45# - SVG element for the page 46# - Device to attach the page to 47 48var PFD_Page = 49{ 50 new : func (svg, title, layer_id, device) 51 { 52 var obj = {parents : [PFD_Page] }; 53 obj.title = title; 54 obj.device = device; 55 obj.layer_id = layer_id; 56 obj.menus = []; 57 obj.svg = svg.getElementById(layer_id); 58 if(obj.svg == nil) 59 printf("PFD_Device: Error loading %s: svg layer %s ",title, layer_id); 60 61 return obj; 62 }, 63 64 # 65 # Makes a page visible. 66 # It is the responsibility of the caller to manage the visibility of pages - i.e. to 67 # make a page that is currenty visible not visible before making a new page visible, 68 # however more than one page could be visible - but only one set of menu buttons can be active 69 # so if two pages are visible (e.g. an overlay) then when the overlay removed it would be necessary 70 # to call setVisible on the base page to ensure that the menus are setup 71 setVisible : func(vis) 72 { 73 if(me.svg != nil) 74 me.svg.setVisible(vis); 75 76 if (vis) 77 me.ondisplay(); 78 else 79 me.offdisplay(); 80 }, 81 82 # Standard callback for buttons, causing the appropriate page to be displayed 83 std_callbackfn : func (device, me, mi) 84 { 85 device.selectPage(mi.page); 86 }, 87 88 # Standard display function for buttons, displaying the text and making visible 89 std_displayfn : func(svg_element, menuitem) 90 { 91 svg_element.setText(menuitem.title); 92 svg_element.setVisible(1); 93 #me.buttons[mi.menu_id].setText(mi.title); 94 #me.buttons[mi.menu_id].setVisible(1); 95 }, 96 97 # 98 # Perform action when button is pushed 99 notifyButton : func(button_id) 100 { foreach(var mi; me.menus) 101 { 102 if (mi.menu_id == button_id) 103 { 104 if (mi.callbackfn != nil) mi.callbackfn(me.device, me, mi); 105 break; 106 } 107 } 108 }, 109 110 # 111 # Add an item to a menu 112 # Params: 113 # menu button id (that is set in controls/PFD/button-pressed by the model) 114 # title of the menu for the label 115 # page that will be selected when pressed 116 # 117 # The corresponding menu for the selected page will automatically be loaded 118 addMenuItem : func(menu_id, title, page, callbackfn=nil, displayfn=nil) 119 { 120 if (callbackfn == nil) callbackfn = me.std_callbackfn; 121 if (displayfn == nil) displayfn = me.std_displayfn; 122 var nm = PFD_MenuItem.new(menu_id, title, page, callbackfn, displayfn); 123 append(me.menus, nm); 124 return nm; 125 }, 126 127 # 128 # Clear all items from the menu. Use-case is where they may be a hierarchy 129 # of menus within the same page. 130 # 131 clearMenu : func() 132 { 133 me.menus = []; 134 }, 135 136 # base method for update; this can be overridden per page instance to provide update of the 137 # elements on display (e.g. to display updated properties) 138 update : func(notification=nil) 139 { 140 }, 141 142 # 143 # notify the page that it is being displayed. use to load any static framework or perform one 144 # time initialisation 145 ondisplay : func 146 { 147 }, 148 149 # 150 # notify the page that it is going off display; use to clean up any created elements or perform 151 # any other required functions 152 offdisplay : func 153 { 154 }, 155}; 156 157 158# 159# Container device for pages. 160var PFD_Device = 161{ 162# - svg is the page elements from the svg. 163# - num_menu_buttons is the Number of menu buttons; starting from the bottom left then right, then top, then left. 164# - button prefix (e.g MI_) is the prefix of the labels in the SVG for the menu boxes. 165# - _canvas is the canvas group. 166# - designation (optional) is used for Emesary designation 167#NOTE: 168# This does not actually create the canvas elements, or parse the SVG, that would typically be done in 169# a higher level class that contains an instance of this class. 170# see: http://wiki.flightgear.org/Canvas_MFD_Framework 171 new : func(svg, num_menu_buttons, button_prefix, _canvas, designation="MFD") 172 { 173 var obj = {parents : [PFD_Device] }; 174 obj.svg = svg; 175 obj.canvas = _canvas; 176 obj.current_page = nil; 177 obj.pages = []; 178 obj.page_index = {}; 179 obj.buttons = setsize([], num_menu_buttons); 180 obj.transmitter = nil; 181 182 # change after creation if required 183 obj.device_id = 1; 184 obj.designation = designation; 185 186 for(var idx = 0; idx < num_menu_buttons; idx += 1) 187 { 188 var label_name = sprintf(button_prefix~"%d",idx); 189 var msvg = obj.svg.getElementById(label_name); 190 if (msvg == nil) 191 printf("PFD_Device: Failed to load %s",label_name); 192 else 193 { 194 obj.buttons[idx] = msvg; 195 obj.buttons[idx].setText(sprintf("M%d",idx)); 196 } 197 } 198 obj.Recipient = nil; 199 return obj; 200 }, 201 # 202 # instead of using the direct call method this allows the use of Emesary (via a specified or default global transmitter) 203 # example to notify that a softkey has been used. The "1" in the line below is the device ID 204 # var notification = notifications.PFDEventNotification.new(me.designation, me.DeviceId, notifications.PFDEventNotification.SoftKeyPushed, me.mpcd_button_pushed); 205 # emesary.GlobalTransmitter.NotifyAll(notification); 206 # - currently supported is 207 # 1. setting menu text directly (after page has been loaded) 208 # notifications.PFDEventNotification.new(me.designation, 1, notifications.PFDEventNotification.ChangeMenuText, [{ Id: 1, Text: "NNN"}]); 209 # 2. SoftKey selection. 210 # 211 # the device ID must match this device ID (to allow for multiple devices). 212 RegisterWithEmesary : func(transmitter = nil){ 213 if (transmitter == nil) 214 transmitter = emesary.GlobalTransmitter; 215 216 if (me.Recipient == nil){ 217 me.Recipient = emesary.Recipient.new("PFD_"~me.designation); 218 var pfd_obj = me; 219 me.Recipient.Receive = func(notification) 220 { 221 if (notification.Device_Id == pfd_obj.device_id 222 and notification.NotificationType == notifications.PFDEventNotification.DefaultType) { 223 if (notification.Event_Id == notifications.PFDEventNotification.SoftKeyPushed 224 and notification.EventParameter != nil) 225 { 226 #printf("Button pressed " ~ notification.EventParameter); 227 pfd_obj.notifyButton(notification.EventParameter); 228 } 229 else if (notification.Event_Id == notifications.PFDEventNotification.ChangeMenuText 230 and notification.EventParameter != nil) 231 { 232 foreach(var eventMenu; notification.EventParameter) { 233 #printf("Menu Text changed : " ~ eventMenu.Text); 234 foreach (var mi ; pfd_obj.current_page.menus) { 235 if (pfd_obj.buttons[eventMenu.Id] != nil) { 236 pfd_obj.buttons[eventMenu.Id].setText(eventMenu.Text); 237 } 238 else 239 printf("PFD_device: Menu for button not found. Menu ID '%s'",mi.menu_id); 240 } 241 } 242 } 243 return emesary.Transmitter.ReceiptStatus_OK; 244 } 245 return emesary.Transmitter.ReceiptStatus_NotProcessed; 246 }; 247 transmitter.Register(me.Recipient); 248 me.transmitter = transmitter; 249 } 250 }, 251 DeRegisterWithEmesary : func(transmitter = nil){ 252 # remove registration from transmitter; but keep the recipient once it is created. 253 if (me.transmitter != nil) 254 me.transmitter.DeRegister(me.Recipient); 255 me.transmitter = nil; 256 }, 257 # 258 # called when a button is pushed - connecting the property to this method is implemented in the outer class 259 notifyButton : func(button_id) 260 { 261 # 262 # by convention the buttons we have are 0 based; however externally 0 is used 263 # to indicate no button pushed. 264 if (button_id > 0) 265 { 266 button_id = button_id - 1; 267 if (me.current_page != nil) 268 { 269 me.current_page.notifyButton(button_id); 270 } 271 else 272 printf("PFD_Device: Could not locate page for button ",button_id); 273 } 274 }, 275 # 276 # 277 # add a page to the device. 278 # - page title. 279 # - svg element id 280 addPage : func(title, layer_id) 281 { 282 var np = PFD_Page.new(me.svg, title, layer_id, me); 283 append(me.pages, np); 284 me.page_index[layer_id] = np; 285 np.setVisible(0); 286 return np; 287 }, 288 # 289 # Get a named page 290 # 291 getPage : func(title) 292 { 293 foreach(var p; me.pages) { 294 if (p.title == title) return p; 295 } 296 297 return nil; 298 }, 299 # 300 # manage the update of the currently selected page 301 update : func(notification=nil) 302 { 303 if (me.current_page != nil) 304 me.current_page.update(notification); 305 }, 306 # 307 # Change to display the selected page. 308 # - the page object method controls the visibility 309 selectPage : func(p) 310 { 311 if (me.current_page == p) return; 312 313 if (me.current_page != nil) 314 me.current_page.setVisible(0); 315 if (me.buttons != nil) 316 { 317 foreach(var mb ; me.buttons) 318 if (mb != nil) 319 mb.setVisible(0); 320 321 foreach(var mi ; p.menus) 322 { 323 if (me.buttons[mi.menu_id] != nil) 324 { 325 mi.displayfn(me.buttons[mi.menu_id], mi); 326 } 327 else 328 printf("PFD_device: Menu for button not found. Menu ID '%s'",mi.menu_id); 329 } 330 } 331 p.setVisible(1); 332 me.current_page = p; 333 }, 334 335 # Return the current selected page. 336 getCurrentPage : func() 337 { 338 return me.current_page; 339 }, 340 341 # 342 # ensure that the menus are display correctly for the current page. 343 updateMenus : func 344 { 345 foreach(var mb ; me.buttons) 346 if (mb != nil) 347 mb.setVisible(0); 348 349 if (me.current_page == nil) return; 350 351 foreach(var mi ; me.current_page.menus) 352 { 353 if (me.buttons[mi.menu_id] != nil) 354 { 355 mi.displayfn(me.buttons[mi.menu_id], mi); 356 } 357 else 358 printf("No corresponding item '%s'",mi.menu_id); 359 } 360 }, 361}; 362 363var PFD_NavDisplay = 364{ 365# 366# Instantiate parameters: 367# 1. pfd_device (instance of PFD_Device) 368# 2. instrument display ident (e.g. mfd-map, or mfd-map-left mfd-map-right for multiple displays) 369# (this is used to map to the property tree) 370# 3. layer_id: main layer in the SVG 371# 4. nd_group_ident : group (usually within the main layer) to place the NavDisplay 372# 5. switches - used to connect the property tree to the nav display. see the canvas nav display 373# documentation 374 new : func (pfd_device, title, instrument_ident, layer_id, nd_group_ident, switches=nil, map_style="Boeing") 375 { 376 var obj = pfd_device.addPage(title, layer_id); 377 378 # if no switches given then use a default set. 379 if (switches != nil) 380 obj.switches = switches; 381 else 382 obj.switches = { 383 'toggle_range': { path: '/inputs/range-nm', value: 40, type: 'INT' }, 384 'toggle_weather': { path: '/inputs/wxr', value: 0, type: 'BOOL' }, 385 'toggle_airports': { path: '/inputs/arpt', value: 1, type: 'BOOL' }, 386 'toggle_stations': { path: '/inputs/sta', value: 0, type: 'BOOL' }, 387 'toggle_waypoints': { path: '/inputs/wpt', value: 0, type: 'BOOL' }, 388 'toggle_position': { path: '/inputs/pos', value: 0, type: 'BOOL' }, 389 'toggle_data': { path: '/inputs/data', value: 1, type: 'BOOL' }, 390 'toggle_terrain': { path: '/inputs/terr', value: 0, type: 'BOOL' }, 391 'toggle_traffic': { path: '/inputs/tfc', value: 0, type: 'BOOL' }, 392 'toggle_centered': { path: '/inputs/nd-centered', value: 1, type: 'BOOL' }, 393 'toggle_lh_vor_adf': { path: '/inputs/lh-vor-adf', value: 1, type: 'INT' }, 394 'toggle_rh_vor_adf': { path: '/inputs/rh-vor-adf', value: 1, type: 'INT' }, 395 'toggle_display_mode': { path: '/mfd/display-mode', value: 'MAP', type: 'STRING' }, 396 'toggle_display_type': { path: '/mfd/display-type', value: 'LCD', type: 'STRING' }, 397 'toggle_true_north': { path: '/mfd/true-north', value: 0, type: 'BOOL' }, 398 'toggle_rangearc': { path: '/mfd/rangearc', value: 0, type: 'BOOL' }, 399 'toggle_track_heading': { path: '/hdg-trk-selected', value: 1, type: 'BOOL' }, 400 }; 401 402 obj.nd_initialised = 0; 403 obj.nd_placeholder_ident = nd_group_ident; 404 obj.nd_ident = instrument_ident; 405 obj.pfd_device = pfd_device; 406 407 obj.nd_init = func 408 { 409 me.ND = canvas.NavDisplay; 410 if (!me.nd_initialised) 411 { 412 me.nd_initialised = 1; 413 414 me.NDCpt = me.ND.new("instrumentation/"~me.nd_ident, me.switches,map_style); 415 416 me.group = me.pfd_device.svg.getElementById(me.nd_placeholder_ident); 417 me.group.setScale(0.39,0.45); 418 me.group.setTranslation(45,0); 419 call(me.NDCpt.newMFD, [me.group, pfd_device.canvas], me.NDCpt,me.NDCpt,var err = []); 420 if (size(err)>0) { 421 print(err[0]); 422 } 423 } 424 me.NDCpt.windShown = 0; 425 me.NDCpt.update(); 426 }; 427 # 428 # Method overrides 429 #----------------------------------------------- 430 # Called when the page goes on display - need to delay initialization of the NavDisplay until later (it fails 431 # if done too early). 432 # NOTE: This causes a display "wobble" the first time on display as resizing happens. I've seen similar things 433 # happen on real avionics (when switched on) so it's not necessarily unrealistic -) 434 obj.ondisplay = func 435 { 436 if (!me.nd_initialised) 437 me.nd_init(); 438 #2018.2 - manage the timer so that the nav display is only updated when visibile 439 me.NDCpt.onDisplay(); 440 }; 441 obj.offdisplay = func 442 { 443 #2018.2 - manage the timer so that the nav display is only updated when visibile 444 if (me.nd_initialised) 445 me.NDCpt.offDisplay(); 446 }; 447 # 448 # most updates performed by the canvas nav display directly. 449 obj.update = func 450 { 451 }; 452 return obj; 453 }, 454}; 455