コード例 #1
0
class DatabaseEditorFrame(wx.Frame):
    """
    Various viewing and editing tools for the SQL database. This object is called on Database toolbar click.
    """
    def __init__(self, roi_map, options):
        """
        :param roi_map: roi_map object
        :type roi_map: DatabaseROIs
        """
        wx.Frame.__init__(self, None, title='Database Administrator')
        set_frame_icon(self)

        set_msw_background_color(
            self)  # If windows, change the background color

        self.roi_map = roi_map
        self.options = options
        self.db_tree = self.get_db_tree()

        self.SetSize(get_window_size(0.792, 0.781))

        self.window_db_editor = wx.SplitterWindow(self,
                                                  wx.ID_ANY,
                                                  style=wx.SP_3D)
        self.window_pane_db_tree = wx.ScrolledWindow(self.window_db_editor,
                                                     wx.ID_ANY,
                                                     style=wx.BORDER_SUNKEN
                                                     | wx.TAB_TRAVERSAL)
        self.tree_ctrl_db = wx.TreeCtrl(self.window_pane_db_tree,
                                        wx.ID_ANY,
                                        style=wx.TR_HAS_BUTTONS
                                        | wx.TR_MULTIPLE)
        self.window_pane_query = wx.Panel(self.window_db_editor,
                                          wx.ID_ANY,
                                          style=wx.BORDER_SUNKEN
                                          | wx.TAB_TRAVERSAL)
        self.text_ctrl_condition = wx.TextCtrl(self.window_pane_query,
                                               wx.ID_ANY, "")
        self.list_ctrl_query_results = wx.ListCtrl(
            self.window_pane_query,
            wx.ID_ANY,
            style=wx.LC_HRULES | wx.LC_REPORT | wx.LC_VRULES)
        self.data_query_results = DataTable(
            self.list_ctrl_query_results,
            columns=['mrn', 'study_instance_uid'])
        self.combo_box_query_table = wx.ComboBox(self.window_pane_query,
                                                 wx.ID_ANY,
                                                 choices=list(self.db_tree),
                                                 style=wx.CB_DROPDOWN
                                                 | wx.CB_READONLY)

        self.button = {
            'delete_all_data':
            wx.Button(self, wx.ID_ANY, "Delete All Data"),
            'rebuild_db':
            wx.Button(self, wx.ID_ANY, "Rebuild Database"),
            'calculations':
            wx.Button(self, wx.ID_ANY, "Calculations"),
            'edit_db':
            wx.Button(self, wx.ID_ANY, "Edit Database"),
            'reimport':
            wx.Button(self, wx.ID_ANY, "Reimport from DICOM"),
            'delete_study':
            wx.Button(self, wx.ID_ANY, "Delete Study"),
            'change_mrn_uid':
            wx.Button(self, wx.ID_ANY, "Change MRN/UID"),
            'query':
            wx.Button(self.window_pane_query, wx.ID_ANY, "Query"),
            'clear':
            wx.Button(self.window_pane_query, wx.ID_ANY, "Clear"),
            'export_csv':
            wx.Button(self.window_pane_query, wx.ID_ANY, "Export"),
            'remap_roi_names':
            wx.Button(self, wx.ID_ANY, "Remap ROI Names"),
            'update_from_csv':
            wx.Button(self, wx.ID_ANY, "Update from CSV"),
            # 'plan_complexity': wx.Button(self, wx.ID_ANY, "Recalc Plan Complexities"),
            'auto_fit_columns':
            wx.Button(self.window_pane_query, wx.ID_ANY, "Auto-fit Columns")
        }

        self.checkbox_auto_backup = wx.CheckBox(
            self, wx.ID_ANY, "Auto Backup SQLite DB After Import")

        self.__set_properties()
        self.__do_layout()
        self.__do_bind()

        self.selected_columns = {
            table: {c: False
                    for c in list(self.db_tree[table])}
            for table in list(self.db_tree)
        }
        self.selected_tables = {table: False for table in list(self.db_tree)}

        self.window_db_editor.SetSashPosition(250)
        self.tree_ctrl_db.Expand(self.db_tree_root)

        self.allow_tree_select_change = True

        self.Show()

    def __set_properties(self):
        self.SetTitle("Database Administrator")
        self.window_pane_db_tree.SetScrollRate(10, 10)
        self.window_db_editor.SetMinimumPaneSize(20)

        self.db_tree_root = self.tree_ctrl_db.AddRoot('DVH Analytics')
        self.table_nodes = {
            table: self.tree_ctrl_db.AppendItem(self.db_tree_root, table)
            for table in self.db_tree
        }
        self.column_nodes = {table: {} for table in self.db_tree}
        for table in self.db_tree:
            for column in self.db_tree[table]:
                self.column_nodes[table][
                    column] = self.tree_ctrl_db.AppendItem(
                        self.table_nodes[table], column)
        self.combo_box_query_table.SetValue('Plans')

        self.checkbox_auto_backup.SetFont(
            wx.Font(11, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL,
                    wx.FONTWEIGHT_NORMAL, 0, ""))
        self.checkbox_auto_backup.SetValue(self.options.AUTO_SQL_DB_BACKUP)

    def __do_layout(self):
        sizer_wrapper = wx.BoxSizer(wx.VERTICAL)
        sizer_query = wx.BoxSizer(wx.VERTICAL)
        sizer_query_table = wx.StaticBoxSizer(
            wx.StaticBox(self.window_pane_query, wx.ID_ANY, "Query Results"),
            wx.VERTICAL)
        sizer_condition_buttons = wx.BoxSizer(wx.HORIZONTAL)
        sizer_download_button = wx.BoxSizer(wx.VERTICAL)
        sizer_clear_button = wx.BoxSizer(wx.VERTICAL)
        sizer_query_button = wx.BoxSizer(wx.VERTICAL)
        sizer_auto_fit_columns_button = wx.BoxSizer(wx.VERTICAL)
        sizer_condition = wx.BoxSizer(wx.VERTICAL)
        sizer_combo_box = wx.BoxSizer(wx.VERTICAL)
        sizer_db_tree = wx.BoxSizer(wx.HORIZONTAL)
        sizer_dialog_buttons = wx.BoxSizer(wx.VERTICAL)
        sizer_dialog_buttons_1 = wx.BoxSizer(wx.HORIZONTAL)

        sizer_dialog_buttons_1.Add(self.button['calculations'], 0, wx.ALL, 5)
        sizer_dialog_buttons_1.Add(self.button['edit_db'], 0, wx.ALL, 5)
        sizer_dialog_buttons_1.Add(self.button['reimport'], 0, wx.ALL, 5)
        sizer_dialog_buttons_1.Add(self.button['delete_study'], 0, wx.ALL, 5)
        sizer_dialog_buttons_1.Add(self.button['change_mrn_uid'], 0, wx.ALL, 5)
        sizer_dialog_buttons_1.Add(self.button['rebuild_db'], 0, wx.ALL, 5)
        sizer_dialog_buttons_1.Add(self.button['delete_all_data'], 0, wx.ALL,
                                   5)
        sizer_dialog_buttons_1.Add(self.button['remap_roi_names'], 0, wx.ALL,
                                   5)
        sizer_dialog_buttons_1.Add(self.button['update_from_csv'], 0, wx.ALL,
                                   5)
        # sizer_dialog_buttons.Add(self.button['plan_complexity'], 0, wx.ALL, 5)
        sizer_dialog_buttons.Add(sizer_dialog_buttons_1, 0, 0, 0)
        sizer_dialog_buttons.Add(self.checkbox_auto_backup, 0, wx.LEFT, 5)
        sizer_wrapper.Add(sizer_dialog_buttons, 0, wx.ALL, 5)

        sizer_db_tree.Add(self.tree_ctrl_db, 1, wx.EXPAND, 0)
        self.window_pane_db_tree.SetSizer(sizer_db_tree)

        label_table = wx.StaticText(self.window_pane_query, wx.ID_ANY,
                                    "Table:")
        sizer_combo_box.Add(label_table, 0, wx.BOTTOM | wx.TOP, 5)
        sizer_combo_box.Add(self.combo_box_query_table, 0, 0, 0)
        sizer_condition_buttons.Add(sizer_combo_box, 0, wx.ALL | wx.EXPAND, 5)

        label_condition = wx.StaticText(self.window_pane_query, wx.ID_ANY,
                                        "Condition:")
        sizer_condition.Add(label_condition, 0, wx.BOTTOM | wx.TOP, 5)
        sizer_condition.Add(self.text_ctrl_condition, 0, wx.ALL | wx.EXPAND, 0)
        sizer_condition_buttons.Add(sizer_condition, 1, wx.ALL | wx.EXPAND, 5)

        label_spacer_1 = wx.StaticText(self.window_pane_query, wx.ID_ANY, "")
        sizer_query_button.Add(label_spacer_1, 0, wx.BOTTOM, 5)
        sizer_query_button.Add(self.button['query'], 0, wx.ALL, 5)
        sizer_condition_buttons.Add(sizer_query_button, 0, wx.ALL, 5)

        label_spacer_2 = wx.StaticText(self.window_pane_query, wx.ID_ANY, "")
        sizer_download_button.Add(label_spacer_2, 0, wx.BOTTOM, 5)
        sizer_download_button.Add(self.button['export_csv'], 0,
                                  wx.TOP | wx.BOTTOM, 5)
        sizer_condition_buttons.Add(sizer_download_button, 0, wx.ALL, 5)

        label_spacer_3 = wx.StaticText(self.window_pane_query, wx.ID_ANY, "")
        sizer_clear_button.Add(label_spacer_3, 0, wx.BOTTOM, 5)
        sizer_clear_button.Add(self.button['clear'], 0, wx.TOP | wx.BOTTOM, 5)
        sizer_condition_buttons.Add(sizer_clear_button, 0, wx.ALL, 5)

        label_spacer_4 = wx.StaticText(self.window_pane_query, wx.ID_ANY, "")
        sizer_auto_fit_columns_button.Add(label_spacer_4, 0, wx.BOTTOM, 5)
        sizer_auto_fit_columns_button.Add(self.button['auto_fit_columns'], 0,
                                          wx.ALL, 5)
        sizer_condition_buttons.Add(sizer_auto_fit_columns_button, 0, wx.ALL,
                                    5)

        sizer_query.Add(sizer_condition_buttons, 0,
                        wx.BOTTOM | wx.EXPAND | wx.LEFT | wx.RIGHT, 5)

        sizer_query_table.Add(self.list_ctrl_query_results, 1, wx.EXPAND, 0)
        sizer_query.Add(sizer_query_table, 1, wx.ALL | wx.EXPAND, 5)

        self.window_pane_query.SetSizer(sizer_query)
        self.window_db_editor.SplitVertically(self.window_pane_db_tree,
                                              self.window_pane_query)
        sizer_wrapper.Add(self.window_db_editor, 1, wx.EXPAND, 0)
        self.SetSizer(sizer_wrapper)
        self.Layout()
        self.Center()

    def __do_bind(self):
        # self.Bind(wx.EVT_TREE_SEL_CHANGED, self.OnTreeAdd, self.tree_ctrl_db, id=self.tree_ctrl_db.GetId())

        # All buttons are bound to a function based on their key prepended with 'on_'
        # For example, query button calls on_query when clicked
        for key, button in self.button.items():
            self.Bind(wx.EVT_BUTTON,
                      getattr(self, 'on_' + key),
                      id=button.GetId())

        self.Bind(wx.EVT_CHECKBOX,
                  self.on_auto_backup,
                  id=self.checkbox_auto_backup.GetId())

        self.Bind(wx.EVT_LIST_COL_CLICK, self.sort_query_results,
                  self.list_ctrl_query_results)

    def on_auto_fit_columns(self, *evt):
        self.data_query_results.set_column_widths(auto=True)

    def on_tree_add(self, evt):
        self.update_selected_tree_items()

    # def UnselectOtherTables(self, selected_table):
    #     self.allow_tree_select_change = False
    #     for table, node in self.table_nodes.items():
    #         if table != selected_table:
    #             self.tree_ctrl_db.UnselectItem(node)
    #     self.allow_tree_select_change = True

    def on_query(self, evt):
        self.update_selected_tree_items()
        table = self.combo_box_query_table.GetValue()
        columns = [
            c for c, sel in self.selected_columns[table].items()
            if sel and c not in {'mrn', 'study_instance_uid'}
        ]
        columns.sort()

        if not columns:
            columns = [
                c for c in self.db_tree[table]
                if c not in {'mrn', 'study_instance_uid'}
            ]

        columns.insert(0, 'study_instance_uid')
        columns.insert(0, 'mrn')

        condition = self.text_ctrl_condition.GetValue()

        with DVH_SQL() as cnx:
            try:
                data = cnx.query(table,
                                 ','.join(columns),
                                 condition,
                                 bokeh_cds=True)
                self.data_query_results.set_data(data, columns)
            except SQLError as e:
                SQLErrorDialog(self, e)
                self.data_query_results.clear()

    def on_clear(self, evt):
        self.data_query_results.clear()

    @staticmethod
    def get_db_tree():
        tree = get_database_tree()
        for table in list(tree):
            tree[table] = [
                column for column in tree[table] if 'string' not in column
            ]
        return tree

    def update_selected_tree_items(self):
        for table, table_item in self.table_nodes.items():
            self.selected_tables[table] = self.tree_ctrl_db.IsSelected(
                table_item)
            for column, column_item in self.column_nodes[table].items():
                self.selected_columns[table][
                    column] = self.tree_ctrl_db.IsSelected(column_item)

    def on_change_mrn_uid(self, evt):
        self.change_or_delete_dlg(ChangePatientIdentifierDialog)

    def on_delete_study(self, evt):
        self.change_or_delete_dlg(DeletePatientDialog)

    def change_or_delete_dlg(self, class_type):
        selected_data = self.data_query_results.selected_row_data
        if selected_data:
            class_type(mrn=selected_data[0][0],
                       study_instance_uid=selected_data[0][1])
        else:
            class_type()

    def on_reimport(self, evt):
        selected_data = self.data_query_results.selected_row_data
        if selected_data:
            ReimportDialog(self.roi_map,
                           self.options,
                           mrn=selected_data[0][0],
                           study_instance_uid=selected_data[0][1])
        else:
            ReimportDialog(self.roi_map, self.options)

    @staticmethod
    def on_edit_db(evt):
        EditDatabaseDialog()

    @staticmethod
    def on_calculations(evt):
        CalculationsDialog()

    def on_rebuild_db(self, evt):
        RebuildDB(self, self.roi_map, self.options)

    def on_delete_all_data(self, evt):
        DeleteAllData(self, self.options)

    def on_export_csv(self, evt):
        save_data_to_file(self, "Export Data Table to CSV",
                          self.data_query_results.get_csv())

    def on_remap_roi_names(self, evt):
        RemapROIFrame(self.roi_map, remap_all=True)

    def sort_query_results(self, evt):
        self.data_query_results.sort_table(evt)

    def on_auto_backup(self, *evt):
        self.options.AUTO_SQL_DB_BACKUP = self.checkbox_auto_backup.GetValue()

    # @staticmethod
    # def on_plan_complexity(*evt):
    #     with wx.BusyCursor() as busy:
    #         recalculate_plan_complexities_from_beams()

    def on_update_from_csv(self, *evt):
        msg = "Load a CSV file and update your database. Please see manual for " \
              "more information. Formatting is critical, this can not be undone!"
        caption = "USE WITH EXTREME CAUTION"
        ErrorDialog(self,
                    msg,
                    caption,
                    flags=wx.ICON_WARNING | wx.OK | wx.OK_DEFAULT)

        with wx.FileDialog(self,
                           "Load CSV into Database",
                           style=wx.FD_FILE_MUST_EXIST
                           | wx.FD_OPEN) as fileDialog:
            if fileDialog.ShowModal() == wx.ID_CANCEL:
                return
            pathname = fileDialog.GetPath()
            with open(pathname, 'r') as fp:
                lines = fp.readlines()
                header_row = "table,column,value,condition"
                if lines and header_row in lines[0].strip():
                    params = [csv_to_list(line.strip()) for line in lines[1:]]
                    ProgressFrame(params,
                                  update_db_with_csv,
                                  title="Update Database from CSV file",
                                  action_msg="Processing row",
                                  star_map=True)
                else:
                    msg = "The selected file was not recognized. " \
                          "Be sure you have a header row of: %s" % header_row
                    caption = "CSV load failure"
                    ErrorDialog(self,
                                msg,
                                caption,
                                flags=wx.ICON_WARNING | wx.OK | wx.OK_DEFAULT)
コード例 #2
0
class ROIMapFrame(wx.Frame):
    """
    Class to view and edit roi map
    """
    def __init__(self, roi_map):
        """
        :param roi_map: roi map object
        :type roi_map: DatabaseROIs
        """
        wx.Frame.__init__(self, None, title='ROI Map')
        set_frame_icon(self)

        self.roi_map = roi_map

        self.window_size = get_window_size(0.893, 0.762)
        self.SetSize(self.window_size)
        self.window = wx.SplitterWindow(self, wx.ID_ANY)
        self.window_tree = wx.Panel(self.window,
                                    wx.ID_ANY,
                                    style=wx.BORDER_SUNKEN)

        self.combo_box_tree_plot_data = wx.ComboBox(
            self.window_tree,
            wx.ID_ANY,
            choices=['All', 'Linked', 'Unlinked', 'Branched'],
            style=wx.CB_DROPDOWN | wx.CB_READONLY)

        self.plot = PlotROIMap(self.window_tree, roi_map)
        self.window_editor = wx.Panel(self.window,
                                      wx.ID_ANY,
                                      style=wx.BORDER_SUNKEN)

        self.combo_box_physician = wx.ComboBox(
            self.window_editor,
            wx.ID_ANY,
            choices=self.roi_map.get_physicians(),
            style=wx.CB_DROPDOWN | wx.CB_READONLY)
        self.combo_box_physician_roi = wx.ComboBox(self.window_editor,
                                                   wx.ID_ANY,
                                                   choices=[],
                                                   style=wx.CB_DROPDOWN
                                                   | wx.CB_READONLY)
        self.list_ctrl_variations = wx.ListCtrl(
            self.window_editor,
            wx.ID_ANY,
            style=wx.LC_NO_HEADER | wx.LC_REPORT | wx.BORDER_SUNKEN)
        self.button_variation_select_all = wx.Button(self.window_editor,
                                                     wx.ID_ANY, "Select All")
        self.button_variation_deselect_all = wx.Button(self.window_editor,
                                                       wx.ID_ANY,
                                                       "Deselect All")
        self.button_variation_add = wx.Button(self.window_editor, wx.ID_ANY,
                                              "Add")
        self.button_variation_delete = wx.Button(self.window_editor, wx.ID_ANY,
                                                 "Delete")
        self.button_variation_move = wx.Button(self.window_editor, wx.ID_ANY,
                                               "Move")

        self.button_variation_move.Disable()
        self.button_variation_delete.Disable()
        self.button_variation_deselect_all.Disable()

        self.button_physician = {
            'add': wx.Button(self.window_editor, wx.ID_ANY, "+"),
            'del': wx.Button(self.window_editor, wx.ID_ANY, "-"),
            'edit': wx.Button(self.window_editor, wx.ID_ANY, "Δ")
        }
        self.button_physician_roi = {
            'add': wx.Button(self.window_editor, wx.ID_ANY, "+"),
            'del': wx.Button(self.window_editor, wx.ID_ANY, "-"),
            'edit': wx.Button(self.window_editor, wx.ID_ANY, "Δ")
        }

        self.button_link_physician_roi = wx.Button(self.window_editor,
                                                   wx.ID_ANY, "Link")
        self.button_link_physician_roi.Disable()

        self.combo_box_uncategorized_ignored = wx.ComboBox(
            self.window_editor,
            wx.ID_ANY,
            choices=["Uncategorized", "Ignored"],
            style=wx.CB_DROPDOWN | wx.CB_READONLY)
        self.combo_box_uncategorized_ignored_roi = wx.ComboBox(
            self.window_editor, wx.ID_ANY, choices=[], style=wx.CB_DROPDOWN)
        self.button_uncategorized_ignored_delete = wx.Button(
            self.window_editor, wx.ID_ANY, "Delete DVH")
        self.button_uncategorized_ignored_ignore = wx.Button(
            self.window_editor, wx.ID_ANY, "Ignore DVH")
        self.combo_box_physician_roi_merge = {
            'a':
            wx.ComboBox(self.window_editor,
                        wx.ID_ANY,
                        style=wx.CB_DROPDOWN | wx.CB_READONLY),
            'b':
            wx.ComboBox(self.window_editor,
                        wx.ID_ANY,
                        style=wx.CB_DROPDOWN | wx.CB_READONLY)
        }
        self.button_merge = wx.Button(self.window_editor, wx.ID_ANY, "Merge")

        self.list_ctrl_tg263 = wx.ListCtrl(self.window_editor,
                                           wx.ID_ANY,
                                           style=wx.LC_REPORT
                                           | wx.BORDER_SUNKEN)
        self.roi_map_gen = ROIMapGenerator()
        self.roi_map_gen.prep_data_for_roi_map_gui()
        self.data_table_tg263 = DataTable(self.list_ctrl_tg263,
                                          columns=self.roi_map_gen.keys)
        combo_map = {
            'anatomy': self.roi_map_gen.anatomic_groups,
            'target': self.roi_map_gen.target_types,
            'major': self.roi_map_gen.major_categories,
            'minor': self.roi_map_gen.minor_categories
        }
        self.combo_box_tg263 = {
            key: wx.ComboBox(self.window_editor,
                             wx.ID_ANY,
                             choices=['All'] + choices,
                             style=wx.CB_DROPDOWN | wx.CB_READONLY)
            for key, choices in combo_map.items()
        }

        self.button_save_and_update = wx.Button(self.window_editor, wx.ID_ANY,
                                                "Save and Update Database")
        self.button_cancel = wx.Button(self.window_editor, wx.ID_ANY,
                                       "Cancel Changes and Reload")

        self.uncategorized_variations = {}

        self.columns = ['Variations']
        self.data_table = DataTable(self.list_ctrl_variations,
                                    columns=self.columns,
                                    widths=[490])

        self.__set_properties()
        self.__do_bind()
        self.__do_layout()

        self.plot.update_roi_map_source_data(self.physician)

        self.physicians_to_delete = []

        self.run()

    def __set_properties(self):
        self.combo_box_uncategorized_ignored.SetSelection(0)
        self.button_uncategorized_ignored_ignore.SetMinSize((110, 20))

        self.combo_box_physician.SetValue('DEFAULT')
        self.update_physician_rois()
        self.update_variations()

        self.combo_box_physician.SetValue('DEFAULT')
        self.combo_box_tree_plot_data.SetValue('ALL')

        self.update_uncategorized_ignored_choices()

        self.window_tree.SetBackgroundColour('white')

        for button in self.button_physician.values():
            button.SetMaxSize((25, 25))
        for button in self.button_physician_roi.values():
            button.SetMaxSize((25, 25))

        self.update_physician_enable()
        self.update_merge_physician_rois()

        if is_windows(
        ):  # combo_boxes here display a doubled bottom border on MSW
            combo_boxes = [
                self.combo_box_physician, self.combo_box_physician_roi,
                self.combo_box_uncategorized_ignored,
                self.combo_box_uncategorized_ignored_roi,
                self.combo_box_physician_roi_merge['a'],
                self.combo_box_physician_roi_merge['b']
            ]
            for combo_box in combo_boxes:
                combo_box.SetMinSize((combo_box.GetSize()[0], 26))
            self.button_uncategorized_ignored_ignore.SetMinSize(
                (self.button_uncategorized_ignored_ignore.GetSize()[0],
                 self.button_uncategorized_ignored_delete.GetSize()[1]))

        self.data_table_tg263.set_data(self.roi_map_gen.tg_263,
                                       columns=self.roi_map_gen.keys)
        self.data_table_tg263.set_column_widths(auto=True)
        for combo_box in self.combo_box_tg263.values():
            combo_box.SetValue('All')

        # These combo_boxes get a height of 30 since adding TG263 combo_boxes?
        for combo_box in self.combo_box_physician_roi_merge.values():
            combo_box.SetMaxSize((1000, 26))

    def __do_bind(self):
        self.window_tree.Bind(wx.EVT_COMBOBOX,
                              self.on_plot_data_type_change,
                              id=self.combo_box_tree_plot_data.GetId())

        self.window_editor.Bind(
            wx.EVT_COMBOBOX,
            self.update_uncategorized_ignored_choices,
            id=self.combo_box_uncategorized_ignored.GetId())

        self.window_editor.Bind(wx.EVT_COMBOBOX,
                                self.physician_ticker,
                                id=self.combo_box_physician.GetId())
        self.window_editor.Bind(wx.EVT_COMBOBOX,
                                self.physician_roi_ticker,
                                id=self.combo_box_physician_roi.GetId())
        self.window_editor.Bind(
            wx.EVT_COMBOBOX,
            self.uncategorized_ticker,
            id=self.combo_box_uncategorized_ignored.GetId())

        self.window_editor.Bind(wx.EVT_BUTTON,
                                self.add_physician,
                                id=self.button_physician['add'].GetId())
        self.window_editor.Bind(wx.EVT_BUTTON,
                                self.on_delete_physician,
                                id=self.button_physician['del'].GetId())
        self.window_editor.Bind(wx.EVT_BUTTON,
                                self.on_edit_physician,
                                id=self.button_physician['edit'].GetId())
        self.window_editor.Bind(wx.EVT_BUTTON,
                                self.on_link_physician_roi,
                                id=self.button_link_physician_roi.GetId())
        self.window_editor.Bind(wx.EVT_BUTTON,
                                self.add_physician_roi,
                                id=self.button_physician_roi['add'].GetId())
        self.window_editor.Bind(wx.EVT_BUTTON,
                                self.on_delete_physician_roi,
                                id=self.button_physician_roi['del'].GetId())
        self.window_editor.Bind(wx.EVT_BUTTON,
                                self.on_edit_physician_roi,
                                id=self.button_physician_roi['edit'].GetId())
        self.window_editor.Bind(wx.EVT_BUTTON,
                                self.select_all_variations,
                                id=self.button_variation_select_all.GetId())
        self.window_editor.Bind(wx.EVT_BUTTON,
                                self.deselect_all_variations,
                                id=self.button_variation_deselect_all.GetId())
        self.window_editor.Bind(wx.EVT_BUTTON,
                                self.add_variation,
                                id=self.button_variation_add.GetId())
        self.window_editor.Bind(wx.EVT_BUTTON,
                                self.move_variations,
                                id=self.button_variation_move.GetId())
        self.window_editor.Bind(wx.EVT_BUTTON,
                                self.delete_variations,
                                id=self.button_variation_delete.GetId())
        self.window_editor.Bind(
            wx.EVT_BUTTON,
            self.on_delete_dvh,
            id=self.button_uncategorized_ignored_delete.GetId())
        self.window_editor.Bind(
            wx.EVT_BUTTON,
            self.on_ignore_dvh,
            id=self.button_uncategorized_ignored_ignore.GetId())
        self.window_editor.Bind(wx.EVT_BUTTON,
                                self.on_merge,
                                id=self.button_merge.GetId())

        for combo_box in self.combo_box_tg263.values():
            self.window_editor.Bind(wx.EVT_COMBOBOX,
                                    self.update_tg263_table,
                                    id=combo_box.GetId())
        self.window_editor.Bind(wx.EVT_LIST_COL_CLICK, self.sort_tg263_table,
                                self.list_ctrl_tg263)

        self.window_editor.Bind(wx.EVT_BUTTON,
                                self.save_and_update,
                                id=self.button_save_and_update.GetId())
        self.window_editor.Bind(wx.EVT_BUTTON,
                                self.on_cancel,
                                id=self.button_cancel.GetId())
        self.Bind(wx.EVT_CLOSE, self.on_close)

        self.window_editor.Bind(
            wx.EVT_COMBOBOX,
            self.update_merge_enable,
            id=self.combo_box_physician_roi_merge['a'].GetId())
        self.window_editor.Bind(
            wx.EVT_COMBOBOX,
            self.update_merge_enable,
            id=self.combo_box_physician_roi_merge['b'].GetId())
        self.window_editor.Bind(wx.EVT_LIST_ITEM_SELECTED,
                                self.update_button_variation_enable,
                                id=self.list_ctrl_variations.GetId())
        self.window_editor.Bind(wx.EVT_LIST_ITEM_DESELECTED,
                                self.update_button_variation_enable,
                                id=self.list_ctrl_variations.GetId())

    def __do_layout(self):

        sizer_wrapper = wx.BoxSizer(wx.HORIZONTAL)
        sizer_editor = wx.BoxSizer(wx.VERTICAL)
        sizer_physician_roi_merger = wx.BoxSizer(wx.HORIZONTAL)
        sizer_physician_roi_merger_merge = wx.BoxSizer(wx.VERTICAL)
        sizer_physician_roi_b = wx.BoxSizer(wx.VERTICAL)
        sizer_physician_roi_a = wx.BoxSizer(wx.VERTICAL)
        sizer_uncategorized_ignored = wx.StaticBoxSizer(
            wx.StaticBox(self.window_editor, wx.ID_ANY,
                         "Uncategorized / Ignored"), wx.HORIZONTAL)
        sizer_uncategorized_ignored_ignore = wx.BoxSizer(wx.VERTICAL)
        sizer_uncategorized_ignored_delete = wx.BoxSizer(wx.VERTICAL)
        sizer_uncategorized_ignored_roi = wx.BoxSizer(wx.VERTICAL)
        sizer_uncategorized_ignored_type = wx.BoxSizer(wx.VERTICAL)
        sizer_tree = wx.BoxSizer(wx.VERTICAL)
        sizer_tree_input = wx.BoxSizer(wx.HORIZONTAL)
        sizer_tree_plot_data = wx.BoxSizer(wx.VERTICAL)
        sizer_roi_manager = wx.BoxSizer(wx.VERTICAL)
        sizer_variation_buttons = wx.BoxSizer(wx.VERTICAL)
        sizer_variation_table = wx.BoxSizer(wx.VERTICAL)
        sizer_map_editor = wx.StaticBoxSizer(
            wx.StaticBox(self.window_editor, wx.ID_ANY, "ROI Map Editor"),
            wx.VERTICAL)
        sizer_variations = wx.BoxSizer(wx.HORIZONTAL)
        sizer_physician_roi = wx.BoxSizer(wx.VERTICAL)
        sizer_physician = wx.BoxSizer(wx.VERTICAL)
        sizer_physician_row = wx.BoxSizer(wx.HORIZONTAL)
        sizer_physician_roi_row = wx.BoxSizer(wx.HORIZONTAL)
        sizer_tg263 = wx.StaticBoxSizer(
            wx.StaticBox(self.window_editor, wx.ID_ANY,
                         "TG-263 (for reference)"), wx.VERTICAL)
        sizer_tg263_filters = wx.BoxSizer(wx.HORIZONTAL)
        sizer_tg263_col = {
            key: wx.BoxSizer(wx.VERTICAL)
            for key in list(self.combo_box_tg263)
        }
        sizer_tg263_table = wx.BoxSizer(wx.VERTICAL)
        sizer_save_cancel_buttons = wx.BoxSizer(wx.HORIZONTAL)

        label_physician = wx.StaticText(self.window_editor, wx.ID_ANY,
                                        "Physician:")
        sizer_physician.Add(label_physician, 0, 0, 0)
        sizer_physician_row.Add(self.combo_box_physician, 1,
                                wx.EXPAND | wx.RIGHT, 5)
        sizer_physician_row.Add(self.button_physician['add'], 0,
                                wx.LEFT | wx.RIGHT, 5)
        sizer_physician_row.Add(self.button_physician['del'], 0, wx.RIGHT, 5)
        sizer_physician_row.Add(self.button_physician['edit'], 0, wx.RIGHT, 10)
        sizer_physician.Add(sizer_physician_row, 1, wx.EXPAND, 0)

        self.label_physician_roi = wx.StaticText(self.window_editor, wx.ID_ANY,
                                                 "Institutional ROI:")
        sizer_physician_roi.Add(self.label_physician_roi, 0, 0, 0)
        sizer_physician_roi_row.Add(self.combo_box_physician_roi, 1,
                                    wx.EXPAND | wx.RIGHT, 5)
        sizer_physician_roi_row.Add(self.button_link_physician_roi, 0,
                                    wx.EXPAND | wx.LEFT | wx.RIGHT, 5)
        sizer_physician_roi_row.Add(self.button_physician_roi['add'], 0,
                                    wx.LEFT | wx.RIGHT, 5)
        sizer_physician_roi_row.Add(self.button_physician_roi['del'], 0,
                                    wx.RIGHT, 5)
        sizer_physician_roi_row.Add(self.button_physician_roi['edit'], 0,
                                    wx.RIGHT, 10)
        sizer_physician_roi.Add(sizer_physician_roi_row, 0, wx.EXPAND, 0)

        sizer_map_editor.Add(sizer_physician, 0, wx.ALL | wx.EXPAND, 5)
        sizer_map_editor.Add(sizer_physician_roi, 0, wx.ALL | wx.EXPAND, 5)

        label_variations = wx.StaticText(self.window_editor, wx.ID_ANY,
                                         "Variations:")
        label_variations_buttons = wx.StaticText(self.window_editor, wx.ID_ANY,
                                                 " ")
        sizer_variation_table.Add(label_variations, 0, 0, 0)
        sizer_variation_table.Add(self.list_ctrl_variations, 1,
                                  wx.BOTTOM | wx.EXPAND, 15)
        sizer_variations.Add(sizer_variation_table, 1, wx.EXPAND | wx.ALL, 5)
        sizer_variation_buttons.Add(label_variations_buttons, 0, 0, 0)
        sizer_variation_buttons.Add(self.button_variation_add, 0,
                                    wx.EXPAND | wx.LEFT | wx.RIGHT | wx.BOTTOM,
                                    5)
        sizer_variation_buttons.Add(self.button_variation_delete, 0,
                                    wx.EXPAND | wx.ALL, 5)
        sizer_variation_buttons.Add(self.button_variation_move, 0,
                                    wx.EXPAND | wx.ALL, 5)
        sizer_variation_buttons.Add(self.button_variation_select_all, 0,
                                    wx.EXPAND | wx.ALL, 5)
        sizer_variation_buttons.Add(self.button_variation_deselect_all, 0,
                                    wx.EXPAND | wx.ALL, 5)
        sizer_variations.Add(sizer_variation_buttons, 0, wx.EXPAND, 0)

        sizer_map_editor.Add(sizer_variations, 0, wx.EXPAND, 0)

        label_physician_roi_a = wx.StaticText(self.window_editor, wx.ID_ANY,
                                              "Merge Physician ROI A:")
        sizer_physician_roi_a.Add(label_physician_roi_a, 0,
                                  wx.EXPAND | wx.LEFT, 5)
        sizer_physician_roi_a.Add(self.combo_box_physician_roi_merge['a'], 1,
                                  wx.EXPAND | wx.LEFT | wx.RIGHT, 5)
        sizer_physician_roi_merger.Add(sizer_physician_roi_a, 1, wx.EXPAND, 0)
        label_physician_roi_b = wx.StaticText(self.window_editor, wx.ID_ANY,
                                              "Into Physician ROI B:")
        sizer_physician_roi_b.Add(label_physician_roi_b, 0, wx.EXPAND, 0)
        sizer_physician_roi_b.Add(self.combo_box_physician_roi_merge['b'], 1,
                                  wx.EXPAND | wx.LEFT | wx.RIGHT, 5)
        sizer_physician_roi_merger.Add(sizer_physician_roi_b, 1, wx.EXPAND, 0)
        sizer_physician_roi_merger_merge.Add((20, 16), 0, 0, 0)
        sizer_physician_roi_merger_merge.Add(self.button_merge, 0, wx.ALL, 5)
        sizer_physician_roi_merger.Add(sizer_physician_roi_merger_merge, 0,
                                       wx.ALL | wx.EXPAND, 0)
        sizer_map_editor.Add(sizer_physician_roi_merger, 0, wx.EXPAND, 0)

        sizer_save_cancel_buttons.Add(self.button_save_and_update, 1,
                                      wx.EXPAND | wx.LEFT | wx.RIGHT, 40)
        sizer_save_cancel_buttons.Add(self.button_cancel, 1,
                                      wx.EXPAND | wx.LEFT | wx.RIGHT, 40)
        sizer_map_editor.Add((10, 10), 0, 0, 0)
        sizer_map_editor.Add(sizer_save_cancel_buttons, 0, wx.EXPAND | wx.ALL,
                             5)

        sizer_roi_manager.Add(sizer_map_editor, 1, wx.EXPAND, 0)
        sizer_editor.Add(sizer_roi_manager, 0, wx.EXPAND | wx.ALL, 5)

        label_tree_plot_data = wx.StaticText(self.window_tree, wx.ID_ANY,
                                             'Institutional Data to Display:')
        sizer_tree_plot_data.Add(label_tree_plot_data, 0,
                                 wx.LEFT | wx.RIGHT | wx.TOP, 5)
        sizer_tree_plot_data.Add(self.combo_box_tree_plot_data, 0,
                                 wx.EXPAND | wx.LEFT | wx.BOTTOM | wx.RIGHT, 5)

        sizer_tree_input.Add(sizer_tree_plot_data, 0,
                             wx.EXPAND | wx.LEFT | wx.TOP, 5)

        sizer_tree.Add(sizer_tree_input, 0, wx.EXPAND, 0)
        sizer_tree.Add(self.plot.layout, 1, wx.EXPAND, 0)
        self.window_tree.SetSizer(sizer_tree)

        label_uncategorized_ignored = wx.StaticText(self.window_editor,
                                                    wx.ID_ANY, "Type:")
        label_uncategorized_ignored.SetMinSize((38, 16))
        sizer_uncategorized_ignored_type.Add(
            label_uncategorized_ignored, 0,
            wx.EXPAND | wx.LEFT | wx.RIGHT | wx.TOP, 5)
        sizer_uncategorized_ignored_type.Add(
            self.combo_box_uncategorized_ignored, 1,
            wx.EXPAND | wx.BOTTOM | wx.LEFT | wx.RIGHT, 5)
        sizer_uncategorized_ignored.Add(sizer_uncategorized_ignored_type, 1,
                                        wx.EXPAND, 0)
        label_uncategorized_ignored_roi = wx.StaticText(
            self.window_editor, wx.ID_ANY, "ROI:")
        label_uncategorized_ignored_roi.SetMinSize((30, 16))
        sizer_uncategorized_ignored_roi.Add(
            label_uncategorized_ignored_roi, 0,
            wx.EXPAND | wx.LEFT | wx.RIGHT | wx.TOP, 5)
        sizer_uncategorized_ignored_roi.Add(
            self.combo_box_uncategorized_ignored_roi, 1,
            wx.EXPAND | wx.BOTTOM | wx.LEFT | wx.RIGHT, 5)
        sizer_uncategorized_ignored.Add(sizer_uncategorized_ignored_roi, 1,
                                        wx.EXPAND, 0)
        sizer_uncategorized_ignored_delete.Add((20, 16), 0, 0, 0)
        sizer_uncategorized_ignored_delete.Add(
            self.button_uncategorized_ignored_delete, 0, wx.ALL, 5)
        sizer_uncategorized_ignored.Add(sizer_uncategorized_ignored_delete, 0,
                                        wx.EXPAND, 0)
        sizer_uncategorized_ignored_ignore.Add((20, 16), 0, 0, 0)
        sizer_uncategorized_ignored_ignore.Add(
            self.button_uncategorized_ignored_ignore, 0, wx.ALL, 5)
        sizer_uncategorized_ignored.Add(sizer_uncategorized_ignored_ignore, 0,
                                        wx.EXPAND, 0)
        sizer_editor.Add(sizer_uncategorized_ignored, 0, wx.ALL | wx.EXPAND, 5)

        sizer_tg263_table.Add(self.list_ctrl_tg263, 1, wx.EXPAND, 0)

        label_tg263 = {
            key: wx.StaticText(self.window_editor, wx.ID_ANY,
                               key.capitalize() + ':')
            for key in list(self.combo_box_tg263)
        }
        for key in ['major', 'minor', 'anatomy', 'target']:
            sizer_tg263_col[key].Add(label_tg263[key])
            sizer_tg263_col[key].Add(self.combo_box_tg263[key], 0, wx.EXPAND,
                                     0)
            sizer_tg263_filters.Add(sizer_tg263_col[key], 0, wx.EXPAND, 0)

        sizer_tg263.Add(sizer_tg263_filters, 0, 0, 0)
        sizer_tg263.Add(sizer_tg263_table, 1, wx.EXPAND, 0)
        sizer_editor.Add(sizer_tg263, 1, wx.EXPAND | wx.ALL, 5)

        self.window_editor.SetSizer(sizer_editor)
        self.window.SplitVertically(self.window_tree, self.window_editor)
        self.window.SetSashPosition(int(self.window_size[0] * 0.55))
        sizer_wrapper.Add(self.window, 1, wx.EXPAND, 0)

        self.SetSizer(sizer_wrapper)
        self.Layout()
        self.Centre()

    def run(self):
        self.Show()

    @staticmethod
    def update_combo_box_choices(combo_box, choices, value):
        if not value:
            value = combo_box.GetValue()
        combo_box.Clear()
        combo_box.AppendItems(sorted(choices))
        combo_box.SetValue(value)

    def update_roi_map(self):
        self.plot.update_roi_map_source_data(
            self.physician,
            plot_type=self.plot_data_type,
            y_shift=self.combo_box_physician_roi.GetValue())

    @property
    def physician(self):
        return self.combo_box_physician.GetValue()

    @property
    def physician_roi(self):
        return self.combo_box_physician_roi.GetValue()

    @property
    def plot_data_type(self):
        return self.combo_box_tree_plot_data.GetValue()

    def physician_ticker(self, evt):
        self.update_physician_roi_label()
        self.update_physician_enable()
        self.update_all(skip_physicians=True)

    def on_plot_data_type_change(self, evt):
        self.update_roi_map()

    def update_uncategorized_ignored_choices(self, *args):
        ignored_variations = self.combo_box_uncategorized_ignored.GetValue(
        ) == 'Ignored'
        self.uncategorized_variations = self.get_uncategorized_variations(
            self.physician, ignored_variations=ignored_variations)
        choices = list(self.uncategorized_variations)
        choices.sort()
        if not choices:
            choices = ['None']
            self.button_uncategorized_ignored_delete.Disable()
            self.button_uncategorized_ignored_ignore.Disable()
        else:
            self.button_uncategorized_ignored_delete.Enable()
            self.button_uncategorized_ignored_ignore.Enable()
        self.combo_box_uncategorized_ignored_roi.Clear()
        self.combo_box_uncategorized_ignored_roi.Append(choices)
        self.combo_box_uncategorized_ignored_roi.SetValue(choices[0])

    @staticmethod
    def get_uncategorized_variations(physician, ignored_variations=False):
        if echo_sql_db():
            with DVH_SQL() as cnx:
                physician = clean_name(physician).upper()
                condition = "physician_roi = '%s'" % [
                    'uncategorized', 'ignored'
                ][ignored_variations]
                cursor_rtn = cnx.query('dvhs', 'roi_name, study_instance_uid',
                                       condition)
                new_variations = {}
                for row in cursor_rtn:
                    variation = str(row[0])
                    study_instance_uid = str(row[1])
                    physician_db = cnx.get_unique_values(
                        'Plans', 'physician',
                        "study_instance_uid = '%s'" % study_instance_uid)
                    if physician_db and physician_db[0] == physician:
                        if variation not in list(new_variations):
                            new_variations[variation] = {
                                'roi_name': variation,
                                'study_instance_uid': [study_instance_uid]
                            }
                        else:
                            new_variations[variation][
                                'study_instance_uid'].append(
                                    study_instance_uid)
                return new_variations

    @property
    def selected_indices(self):
        return get_selected_listctrl_items(self.list_ctrl_variations)

    def update_button_variation_enable(self, *args):
        if self.selected_indices:
            self.button_variation_move.Enable()
            self.button_variation_delete.Enable()
            self.button_variation_deselect_all.Enable()
        else:
            self.button_variation_move.Disable()
            self.button_variation_delete.Disable()
            self.button_variation_deselect_all.Disable()

        self.button_variation_select_all.Enable(self.variation_count > 0)

    def update_variations(self):
        self.data_table.set_data(self.variation_table_data, self.columns)
        self.update_button_variation_enable()

    def physician_roi_ticker(self, evt):
        self.update_variations()
        self.update_roi_map()

    def update_physicians(self, old_physicians=None):

        choices = list(self.roi_map.physicians)
        new = choices[0]
        if old_physicians:
            new = list(set(choices) - set(old_physicians))
            if new:
                new = clean_name(new[0]).upper()

        self.update_combo_box_choices(self.combo_box_physician, choices, new)
        self.update_physician_roi_label()
        self.update_physician_enable()

    def update_physician_rois(self, old_physician_rois=None):
        choices = self.roi_map.get_physician_rois(self.physician)
        new = choices[0]
        if old_physician_rois:
            new = list(set(choices) - set(old_physician_rois))
            if new:
                new = new[0]
        self.update_combo_box_choices(self.combo_box_physician_roi, choices,
                                      new)

    @property
    def variations(self):
        variations = self.roi_map.get_variations(self.physician,
                                                 self.physician_roi)
        variations = list(set(variations) -
                          {self.physician_roi})  # remove physician roi
        variations.sort()
        return variations

    @property
    def variation_table_data(self):
        return {'Variations': self.variations}

    def add_physician_roi(self, evt):
        old_physician_rois = self.roi_map.get_physician_rois(self.physician)
        dlg = AddPhysicianROI(self,
                              self.physician,
                              self.roi_map,
                              institutional_mode=self.physician == 'DEFAULT')
        if dlg.res == wx.ID_OK:
            self.update_all(old_physician_rois=old_physician_rois,
                            skip_physicians=True)

    def add_physician(self, evt):
        old_physicians = list(self.roi_map.physicians)
        dlg = AddPhysician(self.roi_map)
        if dlg.res == wx.ID_OK:
            self.update_all(old_physicians=old_physicians)

    @property
    def variation_count(self):
        return len(self.variations)

    @property
    def selected_values(self):
        return [
            self.list_ctrl_variations.GetItem(i, 0).GetText()
            for i in self.selected_indices
        ]

    def select_all_variations(self, evt):
        self.apply_global_selection()

    def deselect_all_variations(self, evt):
        self.apply_global_selection(on=0)

    def apply_global_selection(self, on=1):
        for i in range(self.variation_count):
            self.list_ctrl_variations.Select(i, on=on)

    def delete_variations(self, evt):
        self.roi_map.delete_variations(self.physician, self.physician_roi,
                                       self.selected_values)
        self.update_variations()
        self.update_roi_map()

    def add_variation(self, evt):
        AddVariation(self, self.physician, self.roi_map, self.physician_roi)
        self.update_variations()
        self.update_roi_map()

    def move_variations(self, evt):
        choices = [
            roi for roi in self.roi_map.get_physician_rois(self.physician)
            if roi != self.physician_roi
        ]
        MoveVariationDialog(self, self.selected_values, self.physician,
                            self.physician_roi, choices, self.roi_map)
        self.update_variations()
        self.update_roi_map()

    def on_delete_physician(self, evt):
        MessageDialog(self,
                      'Delete Physician %s?' % self.physician,
                      action_yes_func=self.delete_physician)

    def delete_physician(self):
        self.roi_map.delete_physician(self.physician)
        self.physicians_to_delete.append(self.physician)
        self.update_all()

    def on_delete_physician_roi(self, evt):
        if self.physician == 'DEFAULT':
            MessageDialog(self,
                          "Delete Institutional ROI %s?" % self.physician_roi,
                          action_yes_func=self.delete_institutional_roi)
        else:
            MessageDialog(self,
                          "Delete Physician ROI %s?" % self.physician_roi,
                          action_yes_func=self.delete_physician_roi)

    def delete_physician_roi(self):
        self.roi_map.delete_physician_roi(self.physician, self.physician_roi)
        self.update_all(skip_physicians=True)

    def delete_institutional_roi(self):
        self.roi_map.delete_institutional_roi(self.physician_roi)
        self.update_all(skip_physicians=True)

    def on_delete_dvh(self, evt):
        MessageDialog(self,
                      "Delete all DVHs named %s for %s?" %
                      (self.dvh, self.physician),
                      message="Are you sure? This cannot be undone!",
                      action_yes_func=self.delete_dvh)

    def delete_dvh(self):
        with DVH_SQL() as cnx:
            for uid in self.dvh_uids:
                cnx.delete_dvh(self.dvh, uid)
        self.update_uncategorized_ignored_choices()

    def on_ignore_dvh(self, evt):
        msg_type = ['Unignore', 'Ignore'
                    ][self.button_uncategorized_ignored_ignore.GetLabelText()
                      == 'Ignore DVH']
        MessageDialog(self,
                      "%s all DVHs named %s for %s?" %
                      (msg_type, self.dvh, self.physician),
                      action_yes_func=self.ignore_dvh)

    def ignore_dvh(self):
        unignore = self.button_uncategorized_ignored_ignore.GetLabelText(
        ) == 'Unignore DVH'
        with DVH_SQL() as cnx:
            for uid in self.dvh_uids:
                cnx.ignore_dvh(self.dvh, uid, unignore=unignore)
        self.update_uncategorized_ignored_choices()

    @property
    def dvh(self):
        return self.combo_box_uncategorized_ignored_roi.GetValue()

    @property
    def dvh_uids(self):
        return self.uncategorized_variations[self.dvh]['study_instance_uid']

    def update_all(self,
                   old_physicians=None,
                   old_physician_rois=None,
                   skip_physicians=False):
        if not skip_physicians:
            self.update_physicians(old_physicians=old_physicians)
        self.update_physician_rois(old_physician_rois=old_physician_rois)
        self.update_variations()
        self.update_uncategorized_ignored_choices()
        self.update_merge_physician_rois()
        self.update_roi_map()

    def on_edit_physician(self, evt):
        current_physicians = list(self.roi_map.get_physicians())
        dlg = RenamePhysicianDialog(self.physician, self.roi_map)
        if dlg.res == wx.ID_OK:
            self.update_all(old_physicians=current_physicians)

    def on_edit_physician_roi(self, evt):
        current_physician_rois = self.roi_map.get_physician_rois(
            self.physician)
        if self.physician == 'DEFAULT':
            dlg = RenameInstitutionalROIDialog(self.physician_roi,
                                               self.roi_map)
        else:
            dlg = RenamePhysicianROIDialog(self.physician, self.physician_roi,
                                           self.roi_map)
        if dlg.res == wx.ID_OK:
            self.update_all(old_physician_rois=current_physician_rois,
                            skip_physicians=True)

    def update_physician_enable(self):
        self.button_physician['del'].Enable(self.physician != 'DEFAULT')
        self.button_physician['edit'].Enable(self.physician != 'DEFAULT')
        self.button_variation_add.Enable(self.physician != 'DEFAULT')

    def update_physician_roi_label(self):
        label_text = ['Physician ROI:',
                      'Institutional ROI:'][self.physician == 'DEFAULT']
        self.label_physician_roi.SetLabelText(label_text)
        self.button_link_physician_roi.Enable(self.physician != 'DEFAULT')

    def uncategorized_ticker(self, evt):
        if self.combo_box_uncategorized_ignored.GetValue() == 'Uncategorized':
            self.button_uncategorized_ignored_ignore.SetLabelText('Ignore DVH')
        else:
            self.button_uncategorized_ignored_ignore.SetLabelText(
                'Unignore DVH')
        self.update_uncategorized_ignored_choices()

    def update_merge_physician_rois(self):
        options = []
        if self.physician != 'DEFAULT':
            options = self.roi_map.get_physician_rois(self.physician)
        if not options:
            options = ['']
        for combo_box in self.combo_box_physician_roi_merge.values():
            combo_box.Clear()
            combo_box.Append(options)
            combo_box.SetValue(options[0])
        self.update_merge_enable()

    @property
    def merge_a(self):
        return self.combo_box_physician_roi_merge['a'].GetValue()

    @property
    def merge_b(self):
        return self.combo_box_physician_roi_merge['b'].GetValue()

    def on_merge(self, evt):
        self.roi_map.merge_physician_rois(self.physician,
                                          [self.merge_a, self.merge_b],
                                          self.merge_b)
        self.update_all(skip_physicians=True)

    def update_merge_enable(self, *args):  # *args to catch wx.EVT_BUTTON
        self.combo_box_physician_roi_merge['a'].Enable(
            bool(self.merge_a and self.merge_b))
        self.combo_box_physician_roi_merge['b'].Enable(
            bool(self.merge_a and self.merge_b))
        self.button_merge.Enable(
            bool(self.merge_a) and bool(self.merge_b)
            and self.merge_a != self.merge_b)

    def on_link_physician_roi(self, evt):
        dlg = LinkPhysicianROI(self, self.physician, self.physician_roi,
                               self.roi_map)
        if dlg.res == wx.ID_OK:
            self.update_roi_map()

    def update_tg263_table(self, *evt):
        filter_key_map = {
            'major': 'Major Cat.',
            'minor': 'Minor Cat.',
            'anatomy': 'Anat. Group',
            'target': 'Target Type'
        }
        data_filter = {
            col: self.combo_box_tg263[key].GetValue()
            for key, col in filter_key_map.items()
        }
        data = self.roi_map_gen.get_filtered_data(data_filter)
        self.data_table_tg263.set_data(data, columns=self.roi_map_gen.keys)
        self.data_table_tg263.set_column_widths(auto=True)

        self.update_tg263_combo_choices()

    def update_tg263_combo_choices(self):
        filter_key_map = {
            'major': 'Major Cat.',
            'minor': 'Minor Cat.',
            'anatomy': 'Anat. Group',
            'target': 'Target Type'
        }
        for key, col in filter_key_map.items():
            current_choice = self.combo_box_tg263[key].GetValue()
            new_choices = ['All'
                           ] + self.data_table_tg263.get_unique_values(col)
            self.combo_box_tg263[key].SetItems(new_choices)
            self.combo_box_tg263[key].SetValue(current_choice)

    def sort_tg263_table(self, evt):
        self.data_table_tg263.sort_table(evt)
        self.data_table_tg263.set_column_widths(auto=True)

    def save_and_update(self, evt):
        for physician in self.physicians_to_delete:
            rel_path = 'physician_%s.roi' % physician
            abs_file_path = join(PREF_DIR, rel_path)
            delete_file(abs_file_path)

        RemapROIFrame(self.roi_map)

    def on_cancel(self, *args):
        self.roi_map.import_from_file()
        self.physicians_to_delete = []
        self.update_roi_map()
        self.update_physicians()

    def on_close(self, *args):
        MessageDialog(self,
                      'Close without saving ROI Map?',
                      action_yes_func=self.do_close)

    def do_close(self):
        self.Destroy()
        self.roi_map.import_from_file()
        self.physicians_to_delete = []