source: sasview/src/sas/sasgui/perspectives/fitting/fitpanel.py @ ae2f623

Last change on this file since ae2f623 was 13374be, checked in by Paul Kienzle <pkienzle@…>, 7 years ago

Merge branch 'master' into 4_1_issues

  • Property mode set to 100644
File size: 22.5 KB
RevLine 
[fa09d62]1"""
2FitPanel class contains fields allowing to fit  models and  data
3
4:note: For Fit to be performed the user should check at least one parameter
5    on fit Panel window.
[2f4b430]6
[fa09d62]7"""
8import wx
9from wx.aui import AuiNotebook as nb
10
[d85c194]11from sas.sasgui.guiframe.panel_base import PanelBase
[e6de6b8]12from sas.sasgui.guiframe.events import PanelOnFocusEvent, StatusEvent
[d85c194]13from sas.sasgui.guiframe.dataFitting import check_data_validity
[a95ae9a]14from sas.sasgui.perspectives.fitting.simfitpage import SimultaneousFitPage
[fa09d62]15
16import basepage
17import models
18_BOX_WIDTH = 80
19
[a95ae9a]20
[fa09d62]21class FitPanel(nb, PanelBase):
22    """
23    FitPanel class contains fields allowing to fit  models and  data
[2f4b430]24
[fa09d62]25    :note: For Fit to be performed the user should check at least one parameter
26        on fit Panel window.
[2f4b430]27
[fa09d62]28    """
[a95ae9a]29    # Internal name for the AUI manager
[fa09d62]30    window_name = "Fit panel"
[a95ae9a]31    # Title to appear on top of the window
[fa09d62]32    window_caption = "Fit Panel "
33    CENTER_PANE = True
[2f4b430]34
[fa09d62]35    def __init__(self, parent, manager=None, *args, **kwargs):
36        """
37        """
[6f16e25]38        nb.__init__(self, parent, wx.ID_ANY,
[2f4b430]39                    style=wx.aui.AUI_NB_WINDOWLIST_BUTTON |
40                    wx.aui.AUI_NB_DEFAULT_STYLE |
[fa09d62]41                    wx.CLIP_CHILDREN)
42        PanelBase.__init__(self, parent)
[a95ae9a]43        # self.SetWindowStyleFlag(style=nb.FNB_FANCY_TABS)
[fa09d62]44        self._manager = manager
45        self.parent = parent
46        self.event_owner = None
[a95ae9a]47        # dictionary of miodel {model class name, model class}
[fa09d62]48        self.menu_mng = models.ModelManager()
49        self.model_list_box = self.menu_mng.get_model_list()
[a95ae9a]50        # pageClosedEvent = nb.EVT_FLATNOTEBOOK_PAGE_CLOSING
[fa09d62]51        self.model_dictionary = self.menu_mng.get_model_dictionary()
52        self.pageClosedEvent = wx.aui.EVT_AUINOTEBOOK_PAGE_CLOSE
[2f4b430]53
[fa09d62]54        self.Bind(self.pageClosedEvent, self.on_close_page)
[a95ae9a]55        # save the title of the last page tab added
[fa09d62]56        self.fit_page_name = {}
[a95ae9a]57        # list of existing fit page
[fa09d62]58        self.opened_pages = {}
[a95ae9a]59        # index of fit page
[fa09d62]60        self.fit_page_index = 0
[a95ae9a]61        # index of batch page
[fa09d62]62        self.batch_page_index = 0
[a95ae9a]63        # page of simultaneous fit
[fa09d62]64        self.sim_page = None
65        self.batch_page = None
[a95ae9a]66        # get the state of a page
[fa09d62]67        self.Bind(basepage.EVT_PAGE_INFO, self._onGetstate)
68        self.Bind(basepage.EVT_PREVIOUS_STATE, self._onUndo)
69        self.Bind(basepage.EVT_NEXT_STATE, self._onRedo)
70        self.Bind(wx.aui.EVT_AUINOTEBOOK_PAGE_CHANGED, self.on_page_changing)
71        self.Bind(wx.aui.EVT_AUINOTEBOOK_PAGE_CLOSED, self.on_closed)
[2f4b430]72
[fa09d62]73    def on_closed(self, event):
74        """
75        """
76        if self.GetPageCount() == 0:
77            self.add_empty_page()
78            self.enable_close_button()
[2f4b430]79
[fa09d62]80    def save_project(self, doc=None):
81        """
82        return an xml node containing state of the panel
[a95ae9a]83        that guiframe can write to file
[fa09d62]84        """
[a95ae9a]85        # Iterate through all pages and check for batch fitting
86        batch_state = None
87        if self.sim_page is not None:
88            batch_state = self.sim_page.set_state()
89
[fa09d62]90        for uid, page in self.opened_pages.iteritems():
[a95ae9a]91            data = page.get_data()
92            # state must be cloned
93            state = page.get_state().clone()
[d79feea]94            # data_list only populated with real data
95            # Fake object in data from page.get_data() if model is selected
96            if len(page.data_list) is not 0 and page.model is not None:
[a95ae9a]97                new_doc = self._manager.state_reader.write_toXML(data,
98                                                                 state,
99                                                                 batch_state)
[d79feea]100                # Fit #2 through #n are append to first fit
[a95ae9a]101                if doc is not None and hasattr(doc, "firstChild"):
[d79feea]102                    # Only append if properly formed new_doc
103                    if new_doc is not None and hasattr(new_doc, "firstChild"):
104                        child = new_doc.firstChild.firstChild
105                        doc.firstChild.appendChild(child)
106                # First fit defines the main document
[a95ae9a]107                else:
108                    doc = new_doc
109
[fa09d62]110        return doc
[2f4b430]111
[fa09d62]112    def update_model_list(self):
113        """
114        """
115        temp = self.menu_mng.update()
116        if len(temp):
117            self.model_list_box = temp
118        return temp
[2f4b430]119
[fa09d62]120    def reset_pmodel_list(self):
121        """
122        """
[f66d9d1]123        temp = self.menu_mng.plugins_reset()
[fa09d62]124        if len(temp):
125            self.model_list_box = temp
126        return temp
[2f4b430]127
[fa09d62]128    def get_page_by_id(self, uid):
129        """
130        """
131        if uid not in self.opened_pages:
132            msg = "Fitpanel cannot find ID: %s in self.opened_pages" % str(uid)
133            raise ValueError, msg
134        else:
135            return self.opened_pages[uid]
[2f4b430]136
[fa09d62]137    def on_page_changing(self, event):
138        """
139        calls the function when the current event handler has exited. avoiding
140        to call panel on focus on a panel that is currently deleted
141        """
142        wx.CallAfter(self.helper_on_page_change)
[2f4b430]143
[fa09d62]144    def helper_on_page_change(self):
145        """
146        """
147        pos = self.GetSelection()
148        if pos != -1:
149            selected_page = self.GetPage(pos)
[2f4b430]150            wx.PostEvent(self._manager.parent,
[fa09d62]151                         PanelOnFocusEvent(panel=selected_page))
152        self.enable_close_button()
[2f4b430]153
[fa09d62]154    def on_set_focus(self, event):
155        """
156        """
157        pos = self.GetSelection()
158        if pos != -1:
159            selected_page = self.GetPage(pos)
[2f4b430]160            wx.PostEvent(self._manager.parent,
[fa09d62]161                         PanelOnFocusEvent(panel=selected_page))
[2f4b430]162
[fa09d62]163    def get_data(self):
164        """
165        get the data in the current page
166        """
167        pos = self.GetSelection()
168        if pos != -1:
169            selected_page = self.GetPage(pos)
170            return selected_page.get_data()
[2f4b430]171
[fa09d62]172    def set_model_state(self, state):
173        """
174        receive a state to reset the model in the current page
175        """
176        pos = self.GetSelection()
177        if pos != -1:
178            selected_page = self.GetPage(pos)
179            selected_page.set_model_state(state)
[2f4b430]180
[fa09d62]181    def get_state(self):
182        """
[e6de6b8]183        return the state of the current selected page
[fa09d62]184        """
185        pos = self.GetSelection()
186        if pos != -1:
187            selected_page = self.GetPage(pos)
188            return selected_page.get_state()
[2f4b430]189
[fa09d62]190    def close_all(self):
191        """
192        remove all pages, used when a svs file is opened
193        """
[2f4b430]194
[e6de6b8]195        # use while-loop, for-loop will not do the job well.
[c8e1996]196        while (self.GetPageCount() > 0):
[67b0a99]197            page = self.GetPage(self.GetPageCount() - 1)
[fa09d62]198            if self._manager.parent.panel_on_focus == page:
199                self._manager.parent.panel_on_focus = None
200            self._close_helper(selected_page=page)
[67b0a99]201            self.DeletePage(self.GetPageCount() - 1)
[e6de6b8]202        # Clear list of names
[fa09d62]203        self.fit_page_name = {}
[e6de6b8]204        # Clear list of opened pages
[fa09d62]205        self.opened_pages = {}
[998ca90]206        self.fit_page_index = 0
207        self.batch_page_index = 0
[2f4b430]208
[fa09d62]209    def set_state(self, state):
210        """
211        Restore state of the panel
212        """
213        page_is_opened = False
214        if state is not None:
215            for uid, panel in self.opened_pages.iteritems():
[e6de6b8]216                # Don't return any panel is the exact same page is created
[fa09d62]217                if uid == panel.uid and panel.data == state.data:
218                    # the page is still opened
219                    panel.reset_page(state=state)
220                    panel.save_current_state()
221                    page_is_opened = True
222            if not page_is_opened:
223                if state.data.__class__.__name__ != 'list':
[e6de6b8]224                    # To support older state file format
[fa09d62]225                    list_data = [state.data]
226                else:
[e6de6b8]227                    # Todo: need new file format for the list
[fa09d62]228                    list_data = state.data
229                panel = self._manager.add_fit_page(data=list_data)
230                # add data associated to the page created
231                if panel is not None:
232                    self._manager.store_data(uid=panel.uid,
233                                             data_list=list_data,
234                                             caption=panel.window_caption)
235                    panel.reset_page(state=state)
236                    panel.save_current_state()
[2f4b430]237
[fa09d62]238    def clear_panel(self):
239        """
240        Clear and close all panels, used by guimanager
241        """
[e6de6b8]242        # close all panels only when svs file opened
[fa09d62]243        self.close_all()
[998ca90]244        self.sim_page = None
245        self.batch_page = None
[2f4b430]246
[fa09d62]247    def on_close_page(self, event=None):
248        """
249        close page and remove all references to the closed page
250        """
251        selected_page = self.GetPage(self.GetSelection())
[c8e1996]252        if self.GetPageCount() == 1:
[e6de6b8]253            if selected_page.get_data() is not None:
[fa09d62]254                if event is not None:
255                    event.Veto()
256                return
257        self._close_helper(selected_page=selected_page)
[2f4b430]258
[fa09d62]259    def close_page_with_data(self, deleted_data):
260        """
261        close a fit page when its data is completely remove from the graph
262        """
263        if deleted_data is None:
264            return
265        for index in range(self.GetPageCount()):
266            selected_page = self.GetPage(index)
267            if hasattr(selected_page, "get_data"):
268                data = selected_page.get_data()
[2f4b430]269
[fa09d62]270                if data is None:
[e6de6b8]271                    # the fitpanel exists and only the initial fit page is open
272                    # with no selected data
[fa09d62]273                    return
274                if data.id == deleted_data.id:
275                    self._close_helper(selected_page)
276                    self.DeletePage(index)
277                    break
[2f4b430]278
[fa09d62]279    def set_manager(self, manager):
280        """
281        set panel manager
[2f4b430]282
[fa09d62]283        :param manager: instance of plugin fitting
284        """
285        self._manager = manager
286        for pos in range(self.GetPageCount()):
287            page = self.GetPage(pos)
288            if page is not None:
289                page.set_manager(self._manager)
290
291    def set_model_list(self, dict):
292        """
293        copy a dictionary of model into its own dictionary
[2f4b430]294
[c8e1996]295        :param dict: dictionnary made of model name as key and model class
[ac7be54]296            as value
[fa09d62]297        """
298        self.model_list_box = dict
[2f4b430]299
[fa09d62]300    def set_model_dict(self, m_dict):
301        """
302        copy a dictionary of model name -> model object
303
304        :param m_dict: dictionary linking model name -> model object
305        """
306
307    def get_current_page(self):
308        """
309        :return: the current page selected
[2f4b430]310
[fa09d62]311        """
312        return self.GetPage(self.GetSelection())
[2f4b430]313
[fa09d62]314    def add_sim_page(self, caption="Const & Simul Fit"):
315        """
316        Add the simultaneous fit page
317        """
318        from simfitpage import SimultaneousFitPage
319        page_finder = self._manager.get_page_finder()
320        if caption == "Const & Simul Fit":
321            self.sim_page = SimultaneousFitPage(self, page_finder=page_finder,
[c8e1996]322                                                 id=wx.ID_ANY, batch_on=False)
[fa09d62]323            self.sim_page.window_caption = caption
324            self.sim_page.window_name = caption
325            self.sim_page.uid = wx.NewId()
326            self.AddPage(self.sim_page, caption, True)
327            self.sim_page.set_manager(self._manager)
328            self.enable_close_button()
329            return self.sim_page
330        else:
331            self.batch_page = SimultaneousFitPage(self, batch_on=True,
[e6de6b8]332                                                  page_finder=page_finder)
[fa09d62]333            self.batch_page.window_caption = caption
334            self.batch_page.window_name = caption
335            self.batch_page.uid = wx.NewId()
336            self.AddPage(self.batch_page, caption, True)
337            self.batch_page.set_manager(self._manager)
338            self.enable_close_button()
339            return self.batch_page
[2f4b430]340
[fa09d62]341    def add_empty_page(self):
342        """
343        add an empty page
344        """
345        if self.batch_on:
346            from batchfitpage import BatchFitPage
347            panel = BatchFitPage(parent=self)
348            self.batch_page_index += 1
349            caption = "BatchPage" + str(self.batch_page_index)
350            panel.set_index_model(self.batch_page_index)
351        else:
[a95ae9a]352            # Increment index of fit page
[e6de6b8]353            from fitpage import FitPage
[fa09d62]354            panel = FitPage(parent=self)
355            self.fit_page_index += 1
356            caption = "FitPage" + str(self.fit_page_index)
357            panel.set_index_model(self.fit_page_index)
358        panel.batch_on = self.batch_on
359        panel._set_save_flag(not panel.batch_on)
360        panel.set_model_dictionary(self.model_dictionary)
361        panel.populate_box(model_dict=self.model_list_box)
362        panel.formfactor_combo_init()
363        panel.set_manager(self._manager)
364        panel.window_caption = caption
365        panel.window_name = caption
366        self.AddPage(panel, caption, select=True)
367        self.opened_pages[panel.uid] = panel
368        self._manager.create_fit_problem(panel.uid)
369        self._manager.page_finder[panel.uid].add_data(panel.get_data())
370        self.enable_close_button()
371        panel.on_set_focus(None)
372        return panel
[2f4b430]373
[fa09d62]374    def enable_close_button(self):
375        """
376        display the close button on tab for more than 1 tabs else remove the
377        close button
378        """
379        if self.GetPageCount() <= 1:
380            style = self.GetWindowStyleFlag()
381            flag = wx.aui.AUI_NB_CLOSE_ON_ACTIVE_TAB
382            if style & wx.aui.AUI_NB_CLOSE_ON_ACTIVE_TAB == flag:
383                style = style & ~wx.aui.AUI_NB_CLOSE_ON_ACTIVE_TAB
384                self.SetWindowStyle(style)
385        else:
386            style = self.GetWindowStyleFlag()
387            flag = wx.aui.AUI_NB_CLOSE_ON_ACTIVE_TAB
388            if style & wx.aui.AUI_NB_CLOSE_ON_ACTIVE_TAB != flag:
389                style |= wx.aui.AUI_NB_CLOSE_ON_ACTIVE_TAB
390                self.SetWindowStyle(style)
[2f4b430]391
[fa09d62]392    def delete_data(self, data):
393        """
394        Delete the given data
395        """
396        if data.__class__.__name__ != "list":
397            raise ValueError, "Fitpanel delete_data expect list of id"
398        else:
399            for page in self.opened_pages.values():
400                pos = self.GetPageIndex(page)
401                temp_data = page.get_data()
402                if temp_data is not None and temp_data.id in data:
[dab94f6]403                    self.close_page_with_data(temp_data)
[67b0a99]404            if self.sim_page is not None:
405                if len(self.sim_page.model_list) == 0:
406                    pos = self.GetPageIndex(self.sim_page)
407                    self.SetSelection(pos)
408                    self.on_close_page(event=None)
[dab94f6]409                    self.DeletePage(pos)
[67b0a99]410                    self.sim_page = None
411                    self.batch_on = False
[fa09d62]412            if self.GetPageCount() == 0:
413                self._manager.on_add_new_page(event=None)
[2f4b430]414
[fa09d62]415    def set_data_on_batch_mode(self, data_list):
416        """
417        Add all data to a single tab when the application is on Batch mode.
[2f4b430]418        However all data in the set of data must be either 1D or 2D type.
419        This method presents option to select the data type before creating a
[fa09d62]420        tab.
421        """
422        data_1d_list = []
423        data_2d_list = []
424        group_id_1d = wx.NewId()
425        # separate data into data1d and data2d list
426        for data in data_list:
427            if data.__class__.__name__ == "Data1D":
428                data.group_id = group_id_1d
429                data_1d_list.append(data)
430            if data.__class__.__name__ == "Data2D":
431                data.group_id = wx.NewId()
432                data_2d_list.append(data)
433        page = None
434        for p in self.opened_pages.values():
[e6de6b8]435            # check if there is an empty page to fill up
[fa09d62]436            if not check_data_validity(p.get_data()) and p.batch_on:
[2f4b430]437
[e6de6b8]438                # make sure data get placed in 1D empty tab if data is 1D
439                # else data get place on 2D tab empty tab
[fa09d62]440                enable2D = p.get_view_mode()
441                if (data.__class__.__name__ == "Data2D" and enable2D)\
442                or (data.__class__.__name__ == "Data1D" and not enable2D):
443                    page = p
444                    break
445        if data_1d_list and data_2d_list:
446            # need to warning the user that this batch is a special case
[c8e1996]447            from sas.sasgui.perspectives.fitting.fitting_widgets import \
448                BatchDataDialog
[fa09d62]449            dlg = BatchDataDialog(self)
450            if dlg.ShowModal() == wx.ID_OK:
451                data_type = dlg.get_data()
452                dlg.Destroy()
[e6de6b8]453                if page is None:
[fa09d62]454                    page = self.add_empty_page()
455                if data_type == 1:
[e6de6b8]456                    # user has selected only data1D
[fa09d62]457                    page.fill_data_combobox(data_1d_list)
458                elif data_type == 2:
459                    page.fill_data_combobox(data_2d_list)
460            else:
[e6de6b8]461                # the batch analysis is canceled
[fa09d62]462                dlg.Destroy()
463                return None
464        else:
465            if page is None:
466                page = self.add_empty_page()
467            if data_1d_list and not data_2d_list:
[c8e1996]468                # only on type of data
[fa09d62]469                page.fill_data_combobox(data_1d_list)
470            elif not data_1d_list and data_2d_list:
471                page.fill_data_combobox(data_2d_list)
[2f4b430]472
[fa09d62]473        pos = self.GetPageIndex(page)
474        page.batch_on = self.batch_on
475        page._set_save_flag(not page.batch_on)
476        self.SetSelection(pos)
477        self.opened_pages[page.uid] = page
478        return page
[2f4b430]479
[fa09d62]480    def set_data(self, data_list):
481        """
482        Add a fitting page on the notebook contained by fitpanel
[2f4b430]483
[c8e1996]484        :param data_list: data to fit
[2f4b430]485
[fa09d62]486        :return panel : page just added for further used.
487        is used by fitting module
[2f4b430]488
[fa09d62]489        """
490        if not data_list:
491            return None
492        if self.batch_on:
493            return self.set_data_on_batch_mode(data_list)
494        else:
495            data = None
496            try:
497                data = data_list[0]
[e6de6b8]498            except Exception:
[fa09d62]499                # for 'fitv' files
500                data_list = [data]
501                data = data_list[0]
[2f4b430]502
[fa09d62]503            if data is None:
504                return None
[6f9abd3]505        focused_page = self.GetPage(self.GetSelection())
[fa09d62]506        for page in self.opened_pages.values():
[e6de6b8]507            # check if the selected data existing in the fitpanel
[fa09d62]508            pos = self.GetPageIndex(page)
509            if not check_data_validity(page.get_data()) and not page.batch_on:
[6f9abd3]510                if page.model is not None and page != focused_page:
511                    # Page has an active theory and is in background - don't
512                    # send data here.
513                    continue
[e6de6b8]514                # make sure data get placed in 1D empty tab if data is 1D
515                # else data get place on 2D tab empty tab
[fa09d62]516                enable2D = page.get_view_mode()
517                if (data.__class__.__name__ == "Data2D" and enable2D)\
[e6de6b8]518                   or (data.__class__.__name__ == "Data1D" and not enable2D):
[fa09d62]519                    page.batch_on = self.batch_on
520                    page._set_save_flag(not page.batch_on)
521                    page.fill_data_combobox(data_list)
[e6de6b8]522                    # caption = "FitPage" + str(self.fit_page_index)
[fa09d62]523                    self.SetPageText(pos, page.window_caption)
524                    self.SetSelection(pos)
525                    return page
[a95ae9a]526        # create new page and add data
[fa09d62]527        page = self.add_empty_page()
528        pos = self.GetPageIndex(page)
529        page.fill_data_combobox(data_list)
530        self.opened_pages[page.uid] = page
531        self.SetSelection(pos)
532        return page
[2f4b430]533
[fa09d62]534    def _onGetstate(self, event):
535        """
536        copy the state of a page
537        """
538        page = event.page
539        if page.uid in self.fit_page_name:
540            self.fit_page_name[page.uid].appendItem(page.createMemento())
[2f4b430]541
[fa09d62]542    def _onUndo(self, event):
543        """
544        return the previous state of a given page is available
545        """
546        page = event.page
547        if page.uid in self.fit_page_name:
548            if self.fit_page_name[page.uid].getCurrentPosition() == 0:
549                state = None
550            else:
551                state = self.fit_page_name[page.uid].getPreviousItem()
552                page._redo.Enable(True)
553            page.reset_page(state)
[2f4b430]554
[fa09d62]555    def _onRedo(self, event):
556        """
557        return the next state available
558        """
559        page = event.page
560        if page.uid in self.fit_page_name:
561            length = len(self.fit_page_name[page.uid])
562            if self.fit_page_name[page.uid].getCurrentPosition() == length - 1:
563                state = None
564                page._redo.Enable(False)
565                page._redo.Enable(True)
566            else:
567                state = self.fit_page_name[page.uid].getNextItem()
568            page.reset_page(state)
[2f4b430]569
[fa09d62]570    def _close_helper(self, selected_page):
571        """
572        Delete the given page from the notebook
573        """
[e6de6b8]574        # remove hint page
575        # if selected_page == self.hint_page:
[fa09d62]576        #    return
[e6de6b8]577        # removing sim_page
[fa09d62]578        if selected_page == self.sim_page:
579            self._manager.sim_page = None
580            return
581        if selected_page == self.batch_page:
582            self._manager.batch_page = None
583            return
[e6de6b8]584        # closing other pages
[fa09d62]585        state = selected_page.createMemento()
586        page_finder = self._manager.get_page_finder()
[e6de6b8]587        # removing fit page
[fa09d62]588        data = selected_page.get_data()
[e6de6b8]589        # Don' t remove plot for 2D
[fa09d62]590        flag = True
591        if data.__class__.__name__ == 'Data2D':
592            flag = False
593        if selected_page in page_finder:
[e6de6b8]594            # Delete the name of the page into the list of open page
[fa09d62]595            for uid, list in self.opened_pages.iteritems():
[e6de6b8]596                # Don't return any panel is the exact same page is created
[fa09d62]597                if flag and selected_page.uid == uid:
598                    self._manager.remove_plot(uid, theory=False)
599                    break
600            del page_finder[selected_page]
[2f4b430]601
[e6de6b8]602        # Delete the name of the page into the list of open page
[fa09d62]603        for uid, list in self.opened_pages.iteritems():
[e6de6b8]604            # Don't return any panel is the exact same page is created
[fa09d62]605            if selected_page.uid == uid:
606                del self.opened_pages[selected_page.uid]
607                break
[e6de6b8]608        # remove the check box link to the model name of the selected_page
[fa09d62]609        try:
610            self.sim_page.draw_page()
611        except:
[e6de6b8]612            # that page is already deleted no need to remove check box on
613            # non existing page
[fa09d62]614            pass
615        try:
616            self.batch_page.draw_page()
617        except:
[e6de6b8]618            # that page is already deleted no need to remove check box on
619            # non existing page
[fa09d62]620            pass
Note: See TracBrowser for help on using the repository browser.