def __init__(self, project, parent_widget = None): QtGui.QDialog.__init__(self, parent_widget) self.setupUi(self) self.project = project self.submodel_node = None # the submodel that we are editing (a copy of actual submodel node) self.active_variables_node = None self.selector_table_model = VariableSelectorTableModel(project) self.tree_structure_editor.header().setStretchLastSection(True) self.tree_structure_editor.header().setMinimumWidth(50) self.frame_name_warning.setVisible(False) self.pb_remove_variable.setVisible(False) # hide the name warning when the user edit the name hide_widget_on_value_change(self.lbl_name_warning, self.le_name) S = QtCore.SIGNAL # temporarily use a shorter name for all the connections below self.connect(self.selector_table_model, S('layoutChanged()'), self._selector_model_column_resize) signal = S("currentItemChanged(QTreeWidgetItem*, QTreeWidgetItem*)") self.connect(self.tree_structure_selector, signal, self._change_structure_node) signal = S('currentIndexChanged(int)') self.connect(self.cbo_dataset_filter, signal, self._update_available_variables) # Setup Variable Selector Table self.table_selected_variables.setModel(self.selector_table_model) self.table_selected_variables.horizontalHeader().setStretchLastSection(True) self.table_selected_variables.verticalHeader().hide() self.table_selected_variables.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) signal = S("customContextMenuRequested(const QPoint &)") self.connect(self.table_selected_variables, signal, self._right_click_variables) f_create_nest = lambda x = 'nest': self.tree_structure_editor.create_structure_node(x) f_create_equation = lambda x = 'equation': self.tree_structure_editor.create_structure_node(x) self.connect(self.pb_create_nest, S('released()'), f_create_nest) self.connect(self.pb_create_equation, S('released()'), f_create_equation) self.connect(self.buttonBox, S('rejected()'), self.reject) self.connect(self.buttonBox, S('accepted()'), self.validate_submodel_and_accept) # the label "OK" can be confusing when switching between the structure # editor and the variable selector. Some users clicked "OK" to confirm the structure changes # Therefore we set a more explicit label. self.buttonBox.button(self.buttonBox.Ok).setText('Save and Close') signal = S('structure_changed') self.connect(self.tree_structure_editor, signal, self._update_submodel_structure_trees) signal = S('clicked()') self.connect(self.pb_update_model_structure, signal, self.update_model_nested_structure)
def __init__(self, project, parent_widget=None): QtGui.QDialog.__init__(self, parent_widget) self.setupUi(self) self.project = project self.submodel_node = None # the submodel that we are editing (a copy of actual submodel node) self.active_variables_node = None self.selector_table_model = VariableSelectorTableModel(project) self.tree_structure_editor.header().setStretchLastSection(True) self.tree_structure_editor.header().setMinimumWidth(50) self.frame_name_warning.setVisible(False) self.pb_remove_variable.setVisible(False) # hide the name warning when the user edit the name hide_widget_on_value_change(self.lbl_name_warning, self.le_name) S = QtCore.SIGNAL # temporarily use a shorter name for all the connections below self.connect(self.selector_table_model, S('layoutChanged()'), self._selector_model_column_resize) signal = S("currentItemChanged(QTreeWidgetItem*, QTreeWidgetItem*)") self.connect(self.tree_structure_selector, signal, self._change_structure_node) signal = S('currentIndexChanged(int)') self.connect(self.cbo_dataset_filter, signal, self._update_available_variables) # Setup Variable Selector Table self.table_selected_variables.setModel(self.selector_table_model) self.table_selected_variables.horizontalHeader().setStretchLastSection( True) self.table_selected_variables.verticalHeader().hide() self.table_selected_variables.setContextMenuPolicy( QtCore.Qt.CustomContextMenu) signal = S("customContextMenuRequested(const QPoint &)") self.connect(self.table_selected_variables, signal, self._right_click_variables) f_create_nest = lambda x='nest': self.tree_structure_editor.create_structure_node( x) f_create_equation = lambda x='equation': self.tree_structure_editor.create_structure_node( x) self.connect(self.pb_create_nest, S('released()'), f_create_nest) self.connect(self.pb_create_equation, S('released()'), f_create_equation) self.connect(self.buttonBox, S('rejected()'), self.reject) self.connect(self.buttonBox, S('accepted()'), self.validate_submodel_and_accept) # the label "OK" can be confusing when switching between the structure # editor and the variable selector. Some users clicked "OK" to confirm the structure changes # Therefore we set a more explicit label. self.buttonBox.button(self.buttonBox.Ok).setText('Save and Close') signal = S('structure_changed') self.connect(self.tree_structure_editor, signal, self._update_submodel_structure_trees) signal = S('clicked()') self.connect(self.pb_update_model_structure, signal, self.update_model_nested_structure)
class SubModelEditor(QtGui.QDialog, Ui_SubModelEditor): ''' Submodel Editing dialog. The editor support three different structures of submodels: I call these structures Plain Structures, Equation Structures and Nested Structures The Plain Structures are submodels that only have a variable list. The Equation Structures are submodels that have one or more <equation>, with each equation having it's own variable list. The Nested Structures have one or more levels of <nest>:s. Each nest can either have an <equation> (that in turn have a variable list) or another nest. Nests can not have variable lists themselves. The assignment of variables happen on either the different <equation>:s (in the case of Equation Structures and Nested Structures) or on the submodel itself if it has a Plain Structure The GUI dialog is made somewhat simpler if the submodel has a Plain Structure, as some functionality is not needed in this case. ''' def __init__(self, project, parent_widget=None): QtGui.QDialog.__init__(self, parent_widget) self.setupUi(self) self.project = project self.submodel_node = None # the submodel that we are editing (a copy of actual submodel node) self.active_variables_node = None self.selector_table_model = VariableSelectorTableModel(project) self.tree_structure_editor.header().setStretchLastSection(True) self.tree_structure_editor.header().setMinimumWidth(50) self.frame_name_warning.setVisible(False) self.pb_remove_variable.setVisible(False) # hide the name warning when the user edit the name hide_widget_on_value_change(self.lbl_name_warning, self.le_name) S = QtCore.SIGNAL # temporarily use a shorter name for all the connections below self.connect(self.selector_table_model, S('layoutChanged()'), self._selector_model_column_resize) signal = S("currentItemChanged(QTreeWidgetItem*, QTreeWidgetItem*)") self.connect(self.tree_structure_selector, signal, self._change_structure_node) signal = S('currentIndexChanged(int)') self.connect(self.cbo_dataset_filter, signal, self._update_available_variables) # Setup Variable Selector Table self.table_selected_variables.setModel(self.selector_table_model) self.table_selected_variables.horizontalHeader().setStretchLastSection( True) self.table_selected_variables.verticalHeader().hide() self.table_selected_variables.setContextMenuPolicy( QtCore.Qt.CustomContextMenu) signal = S("customContextMenuRequested(const QPoint &)") self.connect(self.table_selected_variables, signal, self._right_click_variables) f_create_nest = lambda x='nest': self.tree_structure_editor.create_structure_node( x) f_create_equation = lambda x='equation': self.tree_structure_editor.create_structure_node( x) self.connect(self.pb_create_nest, S('released()'), f_create_nest) self.connect(self.pb_create_equation, S('released()'), f_create_equation) self.connect(self.buttonBox, S('rejected()'), self.reject) self.connect(self.buttonBox, S('accepted()'), self.validate_submodel_and_accept) # the label "OK" can be confusing when switching between the structure # editor and the variable selector. Some users clicked "OK" to confirm the structure changes # Therefore we set a more explicit label. self.buttonBox.button(self.buttonBox.Ok).setText('Save and Close') signal = S('structure_changed') self.connect(self.tree_structure_editor, signal, self._update_submodel_structure_trees) signal = S('clicked()') self.connect(self.pb_update_model_structure, signal, self.update_model_nested_structure) def _lookup_model_node_for(self, node): ''' seek up the tree structure for the <model> parent of the submodel node ''' while node is not None: if node.tag == 'model': return node node = node.getparent() return None def _change_structure_node(self, new_item, old_item): self._set_variable_list_node( new_item.variable_list() if new_item else None) def _show_name_warning(self, text): self.lbl_name_warning.setText(text) self.frame_name_warning.setVisible(True) self.le_name.selectAll() self.le_name.setFocus() def _set_variable_list_node(self, variable_list_node): ''' populate the list of selected variables with the variable_spec nodes of the given variable_list_node ''' # "save' the changes to the previously edited variable_list before changing active node self._apply_selected_variables(self.active_variables_node) self.active_variables_node = variable_list_node self.selector_table_model.clear() if variable_list_node is not None: for variable_spec_node in variable_list_node: self.selector_table_model.add_variable_spec_node( variable_spec_node) self.table_selected_variables.setEnabled(True) self._selector_model_column_resize() self.pb_show_picker.setEnabled(True) else: self.table_selected_variables.setEnabled(False) self.pb_show_picker.setEnabled(False) def _apply_selected_variables(self, variables_node): if variables_node is None: return self.selector_table_model.apply_selected_variables(variables_node) def _update_submodel_structure_trees(self): ''' updates both of the tree widgets to show the structure of self.submodel_node ''' self.tree_structure_selector.clear() self.tree_structure_editor.clear() self._populate_structure_tree(self.submodel_node, False, self.tree_structure_selector) self._populate_structure_tree(self.submodel_node, True, self.tree_structure_editor) for tree_widget in [ self.tree_structure_editor, self.tree_structure_selector ]: tree_widget.resizeColumnToContents(0) tree_widget.resizeColumnToContents(1) # make the GUI a little simpler if the submodel is "plain" (i.e has no structural elements) # by automatically hiding the "structure selector" tree if self._in_simple_mode(): self.split_struct_variables.setSizes([0, 10 ]) # hide structure selector # auto select the only variable_list self._set_variable_list_node( self.submodel_node.find('variable_list')) else: # make sure that the structure widget is visible if not self.pb_show_picker.isChecked(): self.stack_struct_picker.setCurrentIndex(1) self.split_struct_variables.setSizes([10, 10]) self._set_variable_list_node(None) # auto select the first structural element item = self.tree_structure_selector.topLevelItem(0) self.tree_structure_selector.setCurrentItem(item) def _populate_structure_tree(self, parent_node, editable, parent_widget): ''' adds all <nest> nodes and <equation> nodes of parent_node to parent_widget. Recurses down added <nest> nodes. ''' nest_nodes = parent_node.findall('nest') equation_nodes = parent_node.findall('equation') # recursively add nest nodes for nest_node in nest_nodes: item = SubmodelStructureItem(nest_node, editable, parent_widget) item.setExpanded(True) self._populate_structure_tree(nest_node, editable, item) # add any equations for equation_node in equation_nodes: item = SubmodelStructureItem(equation_node, editable, parent_widget) def _selector_model_column_resize(self): ''' updates the column widths whenever there has been a layout change to the model ''' self.selector_table_model.sort_variables_by_name() self.table_selected_variables.resizeRowsToContents() for col in [0, 3, 4]: self.table_selected_variables.resizeColumnToContents(col) def _validate_names(self, show_error_message=False): ''' go through all nest and equation names and ensure that there are no collisions. Returns True if all the names are valid and False otherwise. If @param show_error_message is True an error message of the name errors is displayed.''' # Check for colliding names among the nest and equations colliding_names = set() nodes_to_inspect = self.submodel_node.findall('.//nest') nodes_to_inspect.extend(self.submodel_node.findall('.//equation')) for inspected_node in nodes_to_inspect: # get all sibling names with the same tag sibling_names = [ node.get('name') for node in inspected_node.getparent() if node is not inspected_node and node.tag == inspected_node.tag ] # if there is a name collision, add the name to the set of found colliding names if inspected_node.get('name') in sibling_names: parent_node = inspected_node.getparent() if parent_node.tag == 'nest': desc = '<%s>/%s' % (parent_node.get('name'), inspected_node.get('name')) else: desc = '%s' % inspected_node.get('name') colliding_names.add(desc) # the concept of colliding names might be confusing so we want to be clear on what is # happening and (more importantly) how to solve it if colliding_names: if not show_error_message: return False str_collide_list = ''.join( ['<li>%s</li>\n' % name for name in colliding_names]) short_msg = 'Name collisions found.' longer_msg = ''' <qt> Colliding names: <b> <ul> %s </ul> </b> <p>A name collision is when there are two items with the same name, the same type and the same level.</p> For example: <ul> <li>MY_NEST</li> <li><ul><li>MY_EQUATION</li><li>MY_EQUATION</li></ul></li> </ul> <p>will cause a name collision, while this example;</p> <ul> <li>MY_NEST</li> <li><ul><li>MY_EQUATION</li></ul></li> <li>MY_OTHER_NEST</li> <li> <ul><li>MY_EQUATION</li></ul></li> </ul> <p>is fine since the two equations with the same name are on different levels.</p> <p>To correct this error please give unique names for the above mentioned equations and/or nests.</p></qt>''' % str_collide_list MessageBox.warning(self, short_msg, longer_msg) return False return True def _create_nested_structure_xml(self, nest_node, counter): ''' create and return an XML representation of the nested_structure for the given node ''' # quickie method to get the nest_id or equiation_id from a node dep. on it's tag the_id = lambda x: str(x.get('nest_id')) if x.tag == 'nest' else str( x.get('equation_id')) # the submodel node is passed in the first time this method is called, so we create the # special containing node then if nest_node.tag == 'submodel': created_nest_node = etree.Element('argument', { 'name': 'nested_structure', 'type': 'dictionary' }) else: attrib = { 'name': the_id(nest_node), 'type': 'dictionary', 'parser_action': 'convert_key_to_integer' } created_nest_node = etree.Element('nest', attrib) if nest_node.find( 'nest') is not None: # decend into multiple levels of nests for nest_child_node in nest_node.findall('nest'): child_xml = self._create_nested_structure_xml( nest_child_node, counter) created_nest_node.append(child_xml) elif nest_node.get('number_of_samples') is not None: # auto generate a series of equation ID's num_of_samples = int(nest_node.get('number_of_samples')) id_start = counter['current count'] counter['current count'] = id_end = id_start + num_of_samples index_range = range(id_start, id_end) created_nest_node.set('type', 'list') created_nest_node.text = repr(index_range) elif nest_node.find('equation') is not None: index_range = [ int(the_id(x)) for x in nest_node.findall('equation') ] created_nest_node.set('type', 'list') created_nest_node.text = repr(index_range) else: raise ValueError( 'Found empty nest without "number_of_samples" attribute or <equation> ' 'child nodes (%s name=%s)' % (nest_node.tag, nest_node.get('name'))) return created_nest_node def _create_add_variable_menu(self): # function to display variables in the popup menu def display_node(node, selected_nodes): if node in selected_nodes: return '(already selected) %s' % get_variable_name(node) return get_variable_name(node) # call back to only add unselected variables from the popup menu def add_if_unselected(node, selected_nodes): if not node in selected_nodes: self.add_variable(node) dataset_variable_nodes = get_variable_nodes_per_dataset(self.project) selected_names = map(get_variable_name, self.selector_table_model._variable_nodes) selected_nodes = [] for variable_node_list in dataset_variable_nodes.values(): for variable_node in variable_node_list: if get_variable_name(variable_node) in selected_names: selected_nodes.append(variable_node) # sort by variable name variable_node_list.sort( lambda x, y: cmp(get_variable_name(x), get_variable_name(y))) # display selected items at the bottom for node in variable_node_list[:]: if node in selected_nodes: variable_node_list.remove(node) variable_node_list.insert(len(variable_node_list), node) display_func = lambda x, y=selected_nodes: display_node(x, y) callback = lambda x, y=selected_nodes: add_if_unselected(x, y) return dictionary_to_menu(source_dict=dataset_variable_nodes, callback=callback, display_func=display_func, parent_widget=self) def _right_click_variables(self, point): ''' construct and show an operations operations_menu when the user right clicks the variable selector ''' operations_menu = QtGui.QMenu(self) add_variable_menu = self._create_add_variable_menu() add_variable_menu.setTitle('Add a variable') add_variable_menu.setIcon(IconLibrary.icon('add')) operations_menu.addMenu(add_variable_menu) index_under_cursor = self.table_selected_variables.indexAt(point) if index_under_cursor.isValid(): row = index_under_cursor.row() variable_node = self.selector_table_model.get_variable(row) self.table_selected_variables.selectRow(row) action = create_qt_action( None, 'Remove %s' % get_variable_name(variable_node), self._remove_selected_variables, self.table_selected_variables) action.setIcon(IconLibrary.icon('delete')) operations_menu.addAction(action) else: # if the index is not valid -- assume that the user clicked the "white space" of the table pass operations_menu.exec_(QtGui.QCursor.pos()) def _in_simple_mode(self): return self.tree_structure_selector.topLevelItemCount() == 0 def _update_available_variables(self): # populate the list of available variables while considering A) what dataset filter the user # has selected, an d B) what variables that have already been selected) self.lst_available_variables.clear() if self.cbo_dataset_filter.currentIndex() > 0: dataset_filter = str(self.cbo_dataset_filter.currentText()) else: dataset_filter = None selected_variable_names = [ node.get('name') for node in self.selector_table_model._variable_nodes ] available_variable_nodes = [] variable_nodes_per_dataset = get_variable_nodes_per_dataset( self.project) if not dataset_filter: # take all variables for variable_nodes in variable_nodes_per_dataset.values(): available_variable_nodes.extend(variable_nodes) else: available_variable_nodes = variable_nodes_per_dataset[ dataset_filter] available_variable_nodes.append(get_built_in_constant_node()) # filter already selected variables and show the list of available variables not_selected_variables = [ var_node for var_node in available_variable_nodes if not var_node.get('name') in selected_variable_names ] for variable_node in not_selected_variables: item = QtGui.QListWidgetItem(self.lst_available_variables) item.setIcon(IconLibrary.icon('variable')) item.node = variable_node # monkey in the node for adding later item.setText(get_variable_name(variable_node)) self.lst_available_variables.addItem(item) self.lst_available_variables.sortItems() def _update_dataset_filter_list(self): # update the combo box list (keeping selection) variable_nodes_per_dataset = get_variable_nodes_per_dataset( self.project) pre_update_dataset_name = self.cbo_dataset_filter.currentText() self.cbo_dataset_filter.clear() self.cbo_dataset_filter.addItem('[All datasets]') for dataset_name in variable_nodes_per_dataset: if dataset_name is not None: # built ins are manually added self.cbo_dataset_filter.addItem(dataset_name) post_update_index = self.cbo_dataset_filter.findText( pre_update_dataset_name) if post_update_index > -1: self.cbo_dataset_filter.setCurrentIndex(post_update_index) def _remove_selected_variables(self): # delete the rows from highest to lowest so we don't change the row number when we delete # a row (i.e if we want to delete row 1 and 2 and start with one, then the row that was # number 2 will have become one and we will actually delete row number 3 selected_indices = self.table_selected_variables.selectedIndexes() selected_rows = list(set([idx.row() for idx in selected_indices])) selected_rows.sort(reverse=True) for row in selected_rows: self.table_selected_variables.model().removeRow(row) self._update_available_variables() def _show_advanced_parameters(self, set_advanced_visible=None): # updates the selector table to show only the basic data if set_advanced_visible is True # otherwise all available data is shown if set_advanced_visible is None: set_advanced_visible = self.cb_show_advanced_parameters.isChecked() if set_advanced_visible: for column in range(5): self.table_selected_variables.showColumn(column) else: advanced_columns = [0, 2, 3, 4] for column in range(5): if column in advanced_columns: self.table_selected_variables.hideColumn(column) else: self.table_selected_variables.showColumn(column) def _set_picker_visible(self, visible): # shows / hides the variable picker # visible = if visible: self.stack_struct_picker.setCurrentIndex(0) if self._in_simple_mode(): self.split_struct_variables.setSizes([1, 1]) # hide unnecessary information while picking variables self._show_advanced_parameters(False) else: self.stack_struct_picker.setCurrentIndex(1) if self._in_simple_mode(): self.split_struct_variables.setSizes([0, 10]) self._show_advanced_parameters() # sometimes the table gets messed up when shown again so force refresh self.selector_table_model.emit(QtCore.SIGNAL("layoutChanged()")) self.cb_show_advanced_parameters.setEnabled(not visible) self.pb_remove_variable.setVisible(visible) self.pb_show_picker.setChecked(visible) def update_model_nested_structure(self, node=None): ''' Create an XML representation to use for the argument "nested_structure" used by Nested Logit Models (NLM). The argument is used in the NLMs init() method. @param node submodel node to construct a nested structure from (default self.submodel_node) @raise RuntimeError: If the model node could not be updated @return the created nested_structure node (useful for tests) ''' node = self.submodel_node if node is None else node if node.find( 'nest' ) is None: # can't create a nested structure if there are no nests return None counter = { 'current count': 1 } # pass object ref to keep increments down the recursive chain try: new_nested_structure_node = self._create_nested_structure_xml( node, counter) except ValueError, ex: MessageBox.error(self, 'Not all nests have equations assigned to them.', str(ex)) return None model_node = self._lookup_model_node_for(node) if new_nested_structure_node is None or model_node is None: err_msg = 'Warning: Could not update the model nested node because:' if new_nested_structure_node is None: err_msg = err_msg + ' * the created "nested_structure" was empty' if model_node is None: err_msg = err_msg + ' * a parent <model> node to update could not be found' raise RuntimeError(err_msg) # replace existing nested_structure(s) with the new one init_node = model_node.find('structure/init/') for existing_nest_node in init_node.findall( "argument[@name='nested_structure']"): init_node.remove(existing_nest_node) init_node.append(new_nested_structure_node) return new_nested_structure_node
class SubModelEditor(QtGui.QDialog, Ui_SubModelEditor): ''' Submodel Editing dialog. The editor support three different structures of submodels: I call these structures Plain Structures, Equation Structures and Nested Structures The Plain Structures are submodels that only have a variable list. The Equation Structures are submodels that have one or more <equation>, with each equation having it's own variable list. The Nested Structures have one or more levels of <nest>:s. Each nest can either have an <equation> (that in turn have a variable list) or another nest. Nests can not have variable lists themselves. The assignment of variables happen on either the different <equation>:s (in the case of Equation Structures and Nested Structures) or on the submodel itself if it has a Plain Structure The GUI dialog is made somewhat simpler if the submodel has a Plain Structure, as some functionality is not needed in this case. ''' def __init__(self, project, parent_widget = None): QtGui.QDialog.__init__(self, parent_widget) self.setupUi(self) self.project = project self.submodel_node = None # the submodel that we are editing (a copy of actual submodel node) self.active_variables_node = None self.selector_table_model = VariableSelectorTableModel(project) self.tree_structure_editor.header().setStretchLastSection(True) self.tree_structure_editor.header().setMinimumWidth(50) self.frame_name_warning.setVisible(False) self.pb_remove_variable.setVisible(False) # hide the name warning when the user edit the name hide_widget_on_value_change(self.lbl_name_warning, self.le_name) S = QtCore.SIGNAL # temporarily use a shorter name for all the connections below self.connect(self.selector_table_model, S('layoutChanged()'), self._selector_model_column_resize) signal = S("currentItemChanged(QTreeWidgetItem*, QTreeWidgetItem*)") self.connect(self.tree_structure_selector, signal, self._change_structure_node) signal = S('currentIndexChanged(int)') self.connect(self.cbo_dataset_filter, signal, self._update_available_variables) # Setup Variable Selector Table self.table_selected_variables.setModel(self.selector_table_model) self.table_selected_variables.horizontalHeader().setStretchLastSection(True) self.table_selected_variables.verticalHeader().hide() self.table_selected_variables.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) signal = S("customContextMenuRequested(const QPoint &)") self.connect(self.table_selected_variables, signal, self._right_click_variables) f_create_nest = lambda x = 'nest': self.tree_structure_editor.create_structure_node(x) f_create_equation = lambda x = 'equation': self.tree_structure_editor.create_structure_node(x) self.connect(self.pb_create_nest, S('released()'), f_create_nest) self.connect(self.pb_create_equation, S('released()'), f_create_equation) self.connect(self.buttonBox, S('rejected()'), self.reject) self.connect(self.buttonBox, S('accepted()'), self.validate_submodel_and_accept) # the label "OK" can be confusing when switching between the structure # editor and the variable selector. Some users clicked "OK" to confirm the structure changes # Therefore we set a more explicit label. self.buttonBox.button(self.buttonBox.Ok).setText('Save and Close') signal = S('structure_changed') self.connect(self.tree_structure_editor, signal, self._update_submodel_structure_trees) signal = S('clicked()') self.connect(self.pb_update_model_structure, signal, self.update_model_nested_structure) def _lookup_model_node_for(self, node): ''' seek up the tree structure for the <model> parent of the submodel node ''' while node is not None: if node.tag == 'model': return node node = node.getparent() return None def _change_structure_node(self, new_item, old_item): self._set_variable_list_node(new_item.variable_list() if new_item else None) def _show_name_warning(self, text): self.lbl_name_warning.setText(text) self.frame_name_warning.setVisible(True) self.le_name.selectAll() self.le_name.setFocus() def _set_variable_list_node(self, variable_list_node): ''' populate the list of selected variables with the variable_spec nodes of the given variable_list_node ''' # "save' the changes to the previously edited variable_list before changing active node self._apply_selected_variables(self.active_variables_node) self.active_variables_node = variable_list_node self.selector_table_model.clear() if variable_list_node is not None: for variable_spec_node in variable_list_node: self.selector_table_model.add_variable_spec_node(variable_spec_node) self.table_selected_variables.setEnabled(True) self._selector_model_column_resize() self.pb_show_picker.setEnabled(True) else: self.table_selected_variables.setEnabled(False) self.pb_show_picker.setEnabled(False) def _apply_selected_variables(self, variables_node): if variables_node is None: return self.selector_table_model.apply_selected_variables(variables_node) def _update_submodel_structure_trees(self): ''' updates both of the tree widgets to show the structure of self.submodel_node ''' self.tree_structure_selector.clear() self.tree_structure_editor.clear() self._populate_structure_tree(self.submodel_node, False, self.tree_structure_selector) self._populate_structure_tree(self.submodel_node, True, self.tree_structure_editor) for tree_widget in [self.tree_structure_editor, self.tree_structure_selector]: tree_widget.resizeColumnToContents(0) tree_widget.resizeColumnToContents(1) # make the GUI a little simpler if the submodel is "plain" (i.e has no structural elements) # by automatically hiding the "structure selector" tree if self._in_simple_mode(): self.split_struct_variables.setSizes([0, 10]) # hide structure selector # auto select the only variable_list self._set_variable_list_node(self.submodel_node.find('variable_list')) else: # make sure that the structure widget is visible if not self.pb_show_picker.isChecked(): self.stack_struct_picker.setCurrentIndex(1) self.split_struct_variables.setSizes([10, 10]) self._set_variable_list_node(None) # auto select the first structural element item = self.tree_structure_selector.topLevelItem(0) self.tree_structure_selector.setCurrentItem(item) def _populate_structure_tree(self, parent_node, editable, parent_widget): ''' adds all <nest> nodes and <equation> nodes of parent_node to parent_widget. Recurses down added <nest> nodes. ''' nest_nodes = parent_node.findall('nest') equation_nodes = parent_node.findall('equation') # recursively add nest nodes for nest_node in nest_nodes: item = SubmodelStructureItem(nest_node, editable, parent_widget) item.setExpanded(True) self._populate_structure_tree(nest_node, editable, item) # add any equations for equation_node in equation_nodes: item = SubmodelStructureItem(equation_node, editable, parent_widget) def _selector_model_column_resize(self): ''' updates the column widths whenever there has been a layout change to the model ''' self.selector_table_model.sort_variables_by_name() self.table_selected_variables.resizeRowsToContents() for col in [0, 3, 4]: self.table_selected_variables.resizeColumnToContents(col) def _validate_names(self, show_error_message = False): ''' go through all nest and equation names and ensure that there are no collisions. Returns True if all the names are valid and False otherwise. If @param show_error_message is True an error message of the name errors is displayed.''' # Check for colliding names among the nest and equations colliding_names = set() nodes_to_inspect = self.submodel_node.findall('.//nest') nodes_to_inspect.extend(self.submodel_node.findall('.//equation')) for inspected_node in nodes_to_inspect: # get all sibling names with the same tag sibling_names = [node.get('name') for node in inspected_node.getparent() if node is not inspected_node and node.tag == inspected_node.tag] # if there is a name collision, add the name to the set of found colliding names if inspected_node.get('name') in sibling_names: parent_node = inspected_node.getparent() if parent_node.tag == 'nest': desc = '<%s>/%s' % (parent_node.get('name'), inspected_node.get('name')) else: desc = '%s' % inspected_node.get('name') colliding_names.add(desc) # the concept of colliding names might be confusing so we want to be clear on what is # happening and (more importantly) how to solve it if colliding_names: if not show_error_message: return False str_collide_list = ''.join(['<li>%s</li>\n' % name for name in colliding_names]) short_msg = 'Name collisions found.' longer_msg = ''' <qt> Colliding names: <b> <ul> %s </ul> </b> <p>A name collision is when there are two items with the same name, the same type and the same level.</p> For example: <ul> <li>MY_NEST</li> <li><ul><li>MY_EQUATION</li><li>MY_EQUATION</li></ul></li> </ul> <p>will cause a name collision, while this example;</p> <ul> <li>MY_NEST</li> <li><ul><li>MY_EQUATION</li></ul></li> <li>MY_OTHER_NEST</li> <li> <ul><li>MY_EQUATION</li></ul></li> </ul> <p>is fine since the two equations with the same name are on different levels.</p> <p>To correct this error please give unique names for the above mentioned equations and/or nests.</p></qt>'''% str_collide_list MessageBox.warning(self, short_msg, longer_msg) return False return True def _create_nested_structure_xml(self, nest_node, counter): ''' create and return an XML representation of the nested_structure for the given node ''' # quickie method to get the nest_id or equiation_id from a node dep. on it's tag the_id = lambda x: str(x.get('nest_id')) if x.tag == 'nest' else str(x.get('equation_id')) # the submodel node is passed in the first time this method is called, so we create the # special containing node then if nest_node.tag == 'submodel': created_nest_node = etree.Element('argument', {'name': 'nested_structure', 'type': 'dictionary'}) else: attrib = {'name': the_id(nest_node), 'type': 'dictionary', 'parser_action': 'convert_key_to_integer'} created_nest_node = etree.Element('nest', attrib) if nest_node.find('nest') is not None: # decend into multiple levels of nests for nest_child_node in nest_node.findall('nest'): child_xml = self._create_nested_structure_xml(nest_child_node, counter) created_nest_node.append(child_xml) elif nest_node.get('number_of_samples') is not None: # auto generate a series of equation ID's num_of_samples = int(nest_node.get('number_of_samples')) id_start = counter['current count'] counter['current count'] = id_end = id_start + num_of_samples index_range = range(id_start, id_end) created_nest_node.set('type', 'list') created_nest_node.text = repr(index_range) elif nest_node.find('equation') is not None: index_range = [int(the_id(x)) for x in nest_node.findall('equation')] created_nest_node.set('type', 'list') created_nest_node.text = repr(index_range) else: raise ValueError('Found empty nest without "number_of_samples" attribute or <equation> ' 'child nodes (%s name=%s)' % (nest_node.tag, nest_node.get('name'))) return created_nest_node def _create_add_variable_menu(self): # function to display variables in the popup menu def display_node(node, selected_nodes): if node in selected_nodes: return '(already selected) %s' % get_variable_name(node) return get_variable_name(node) # call back to only add unselected variables from the popup menu def add_if_unselected(node, selected_nodes): if not node in selected_nodes: self.add_variable(node) dataset_variable_nodes = get_variable_nodes_per_dataset(self.project) selected_names = map(get_variable_name, self.selector_table_model._variable_nodes) selected_nodes = [] for variable_node_list in dataset_variable_nodes.values(): for variable_node in variable_node_list: if get_variable_name(variable_node) in selected_names: selected_nodes.append(variable_node) # sort by variable name variable_node_list.sort(lambda x, y: cmp(get_variable_name(x), get_variable_name(y))) # display selected items at the bottom for node in variable_node_list[:]: if node in selected_nodes: variable_node_list.remove(node) variable_node_list.insert(len(variable_node_list), node) display_func = lambda x, y = selected_nodes: display_node(x, y) callback = lambda x, y = selected_nodes: add_if_unselected(x, y) return dictionary_to_menu(source_dict = dataset_variable_nodes, callback = callback, display_func = display_func, parent_widget = self) def _right_click_variables(self, point): ''' construct and show an operations operations_menu when the user right clicks the variable selector ''' operations_menu = QtGui.QMenu(self) add_variable_menu = self._create_add_variable_menu() add_variable_menu.setTitle('Add a variable') add_variable_menu.setIcon(IconLibrary.icon('add')) operations_menu.addMenu(add_variable_menu) index_under_cursor = self.table_selected_variables.indexAt(point) if index_under_cursor.isValid(): row = index_under_cursor.row() variable_node = self.selector_table_model.get_variable(row) self.table_selected_variables.selectRow(row) action = create_qt_action(None, 'Remove %s' % get_variable_name(variable_node), self._remove_selected_variables, self.table_selected_variables) action.setIcon(IconLibrary.icon('delete')) operations_menu.addAction(action) else: # if the index is not valid -- assume that the user clicked the "white space" of the table pass operations_menu.exec_(QtGui.QCursor.pos()) def _in_simple_mode(self): return self.tree_structure_selector.topLevelItemCount() == 0 def _update_available_variables(self): # populate the list of available variables while considering A) what dataset filter the user # has selected, an d B) what variables that have already been selected) self.lst_available_variables.clear() if self.cbo_dataset_filter.currentIndex() > 0: dataset_filter = str(self.cbo_dataset_filter.currentText()) else: dataset_filter = None selected_variable_names = [node.get('name') for node in self.selector_table_model._variable_nodes] available_variable_nodes = [] variable_nodes_per_dataset = get_variable_nodes_per_dataset(self.project) if not dataset_filter: # take all variables for variable_nodes in variable_nodes_per_dataset.values(): available_variable_nodes.extend(variable_nodes) else: available_variable_nodes = variable_nodes_per_dataset[dataset_filter] available_variable_nodes.append(get_built_in_constant_node()) # filter already selected variables and show the list of available variables not_selected_variables = [var_node for var_node in available_variable_nodes if not var_node.get('name') in selected_variable_names] for variable_node in not_selected_variables: item = QtGui.QListWidgetItem(self.lst_available_variables) item.setIcon(IconLibrary.icon('variable')) item.node = variable_node # monkey in the node for adding later item.setText(get_variable_name(variable_node)) self.lst_available_variables.addItem(item) self.lst_available_variables.sortItems() def _update_dataset_filter_list(self): # update the combo box list (keeping selection) variable_nodes_per_dataset = get_variable_nodes_per_dataset(self.project) pre_update_dataset_name = self.cbo_dataset_filter.currentText() self.cbo_dataset_filter.clear() self.cbo_dataset_filter.addItem('[All datasets]') for dataset_name in variable_nodes_per_dataset: if dataset_name is not None: # built ins are manually added self.cbo_dataset_filter.addItem(dataset_name) post_update_index = self.cbo_dataset_filter.findText(pre_update_dataset_name) if post_update_index > -1: self.cbo_dataset_filter.setCurrentIndex(post_update_index) def _remove_selected_variables(self): # delete the rows from highest to lowest so we don't change the row number when we delete # a row (i.e if we want to delete row 1 and 2 and start with one, then the row that was # number 2 will have become one and we will actually delete row number 3 selected_indices = self.table_selected_variables.selectedIndexes() selected_rows = list(set([idx.row() for idx in selected_indices])) selected_rows.sort(reverse=True) for row in selected_rows: self.table_selected_variables.model().removeRow(row) self._update_available_variables() def _show_advanced_parameters(self, set_advanced_visible = None): # updates the selector table to show only the basic data if set_advanced_visible is True # otherwise all available data is shown if set_advanced_visible is None: set_advanced_visible = self.cb_show_advanced_parameters.isChecked() if set_advanced_visible: for column in range(5): self.table_selected_variables.showColumn(column) else: advanced_columns = [0, 2, 3, 4] for column in range(5): if column in advanced_columns: self.table_selected_variables.hideColumn(column) else: self.table_selected_variables.showColumn(column) def _set_picker_visible(self, visible): # shows / hides the variable picker # visible = if visible: self.stack_struct_picker.setCurrentIndex(0) if self._in_simple_mode(): self.split_struct_variables.setSizes([1, 1]) # hide unnecessary information while picking variables self._show_advanced_parameters(False) else: self.stack_struct_picker.setCurrentIndex(1) if self._in_simple_mode(): self.split_struct_variables.setSizes([0, 10]) self._show_advanced_parameters() # sometimes the table gets messed up when shown again so force refresh self.selector_table_model.emit(QtCore.SIGNAL("layoutChanged()")) self.cb_show_advanced_parameters.setEnabled(not visible) self.pb_remove_variable.setVisible(visible) self.pb_show_picker.setChecked(visible) def update_model_nested_structure(self, node = None): ''' Create an XML representation to use for the argument "nested_structure" used by Nested Logit Models (NLM). The argument is used in the NLMs init() method. @param node submodel node to construct a nested structure from (default self.submodel_node) @raise RuntimeError: If the model node could not be updated @return the created nested_structure node (useful for tests) ''' node = self.submodel_node if node is None else node if node.find('nest') is None: # can't create a nested structure if there are no nests return None counter = {'current count': 1} # pass object ref to keep increments down the recursive chain try: new_nested_structure_node = self._create_nested_structure_xml(node, counter) except ValueError, ex: MessageBox.error(self, 'Not all nests have equations assigned to them.', str(ex)) return None model_node = self._lookup_model_node_for(node) if new_nested_structure_node is None or model_node is None: err_msg = 'Warning: Could not update the model nested node because:' if new_nested_structure_node is None: err_msg = err_msg + ' * the created "nested_structure" was empty' if model_node is None: err_msg = err_msg + ' * a parent <model> node to update could not be found' raise RuntimeError(err_msg) # replace existing nested_structure(s) with the new one init_node = model_node.find('structure/init/') for existing_nest_node in init_node.findall("argument[@name='nested_structure']"): init_node.remove(existing_nest_node) init_node.append(new_nested_structure_node) return new_nested_structure_node