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