class TrackingWindow(QMainWindow): """ Main window of the application. This class is responsible for the global data structures too. :IVariables: undo_stack : `QUndoStack` Undo stack. All actions that can be undone should be pushed on the stack. _project : `project.Project` Project object managing the loaded data _data : `tracking_data.TrackingData` Data object keeping track of points and cells toolGroup : `QActionGroup` Group of actions to be enabled only when actions can be taken on images previousSelAct : `QActionGroup` Actions enabled when points are selected in the previous pane currentSelAct : `QActionGroup` Actions enabled when points are selected in the current pane projectAct : `QActionGroup` Actions to enable once a project is loaded _previousScene : `tracking_scene.TrackingScene` Object managing the previous pane _currentScene : `tracking_scene.LinkedTrackingScene` Object managing the current pane """ def __init__(self, *args, **kwords): QMainWindow.__init__(self, *args) self.undo_stack = QUndoStack(self) self.ui = Ui_TrackingWindow() self.ui.setupUi(self) self._project = None self._data = None self.toolGroup = QActionGroup(self) self.toolGroup.addAction(self.ui.actionAdd_point) self.toolGroup.addAction(self.ui.action_Move_point) self.toolGroup.addAction(self.ui.actionAdd_cell) self.toolGroup.addAction(self.ui.actionRemove_cell) self.toolGroup.addAction(self.ui.action_Pan) self.toolGroup.addAction(self.ui.actionZoom_out) self.toolGroup.addAction(self.ui.actionZoom_in) self.previousSelAct = QActionGroup(self) self.previousSelAct.addAction(self.ui.actionCopy_selection_from_Previous) self.previousSelAct.addAction(self.ui.actionDelete_Previous) self.previousSelAct.setEnabled(False) self.currentSelAct = QActionGroup(self) self.currentSelAct.addAction(self.ui.actionCopy_selection_from_Current) self.currentSelAct.addAction(self.ui.actionDelete_Current) self.currentSelAct.setEnabled(False) self.projectAct = QActionGroup(self) self.projectAct.addAction(self.ui.action_Next_image) self.projectAct.addAction(self.ui.action_Previous_image) self.projectAct.addAction(self.ui.actionAdd_point) self.projectAct.addAction(self.ui.action_Move_point) self.projectAct.addAction(self.ui.action_Pan) self.projectAct.addAction(self.ui.actionAdd_cell) self.projectAct.addAction(self.ui.actionRemove_cell) self.projectAct.addAction(self.ui.action_Change_data_file) self.projectAct.addAction(self.ui.actionNew_data_file) self.projectAct.addAction(self.ui.actionZoom_out) self.projectAct.addAction(self.ui.actionZoom_in) self.projectAct.addAction(self.ui.actionSave_as) self.projectAct.addAction(self.ui.action_Fit) self.projectAct.addAction(self.ui.actionZoom_100) self.projectAct.addAction(self.ui.actionMerge_points) self.projectAct.addAction(self.ui.actionCopy_from_previous) self.projectAct.addAction(self.ui.actionCopy_from_current) self.projectAct.addAction(self.ui.actionReset_alignment) self.projectAct.addAction(self.ui.actionAlign_images) self.projectAct.addAction(self.ui.actionSelectPreviousAll) self.projectAct.addAction(self.ui.actionSelectPreviousNew) self.projectAct.addAction(self.ui.actionSelectPreviousNone) self.projectAct.addAction(self.ui.actionSelectPreviousNon_associated) self.projectAct.addAction(self.ui.actionSelectPreviousAssociated) self.projectAct.addAction(self.ui.actionSelectPreviousInvert) self.projectAct.addAction(self.ui.actionSelectCurrentAll) self.projectAct.addAction(self.ui.actionSelectCurrentNew) self.projectAct.addAction(self.ui.actionSelectCurrentNone) self.projectAct.addAction(self.ui.actionSelectCurrentNon_associated) self.projectAct.addAction(self.ui.actionSelectCurrentAssociated) self.projectAct.addAction(self.ui.actionSelectCurrentInvert) self.projectAct.addAction(self.ui.actionEdit_timing) self.projectAct.addAction(self.ui.actionEdit_scales) self.projectAct.addAction(self.ui.actionCompute_growth) self.projectAct.addAction(self.ui.actionClean_cells) self.projectAct.addAction(self.ui.actionGotoCell) self.projectAct.setEnabled(False) current_sel_actions = [self.ui.actionSelectCurrentAll, self.ui.actionSelectCurrentNew, self.ui.actionSelectCurrentNone, self.ui.actionSelectCurrentInvert, '-', self.ui.actionSelectCurrentNon_associated, self.ui.actionSelectCurrentAssociated, self.ui.actionCopy_selection_from_Previous ] previous_sel_actions = [self.ui.actionSelectPreviousAll, self.ui.actionSelectPreviousNew, self.ui.actionSelectPreviousNone, self.ui.actionSelectPreviousInvert, '-', self.ui.actionSelectPreviousNon_associated, self.ui.actionSelectPreviousAssociated, self.ui.actionCopy_selection_from_Current ] self._previousScene = TrackingScene(self.undo_stack, self.ui.actionDelete_Previous, previous_sel_actions, self) self._currentScene = LinkedTrackingScene(self._previousScene, self.undo_stack, self.ui.actionDelete_Current, current_sel_actions, self) self._previousScene.hasSelectionChanged.connect(self.previousSelAct.setEnabled) self._currentScene.hasSelectionChanged.connect(self.currentSelAct.setEnabled) self._previousScene.realSceneSizeChanged.connect(self.sceneSizeChanged) self._currentScene.realSceneSizeChanged.connect(self.sceneSizeChanged) self._previousScene.zoomIn[QPointF].connect(self.zoomIn) self._currentScene.zoomIn.connect(self.zoomIn) self._previousScene.zoomOut[QPointF].connect(self.zoomOut) self._currentScene.zoomOut.connect(self.zoomOut) self.ui.previousData.setScene(self._previousScene) self.ui.currentData.setScene(self._currentScene) self.ui.previousData.setDragMode(QGraphicsView.ScrollHandDrag) self.ui.currentData.setDragMode(QGraphicsView.ScrollHandDrag) #self.ui.previousData.setCacheMode(QGraphicsView.CacheBackground) #self.ui.currentData.setCacheMode(QGraphicsView.CacheBackground) # Redefine shortcuts to standard key sequences self.ui.action_Save.setShortcut(QKeySequence.Save) self.ui.actionSave_as.setShortcut(QKeySequence.SaveAs) self.ui.action_Open_project.setShortcut(QKeySequence.Open) self.ui.action_Undo.setShortcut(QKeySequence.Undo) self.ui.action_Redo.setShortcut(QKeySequence.Redo) self.ui.action_Next_image.setShortcut(QKeySequence.Forward) self.ui.action_Previous_image.setShortcut(QKeySequence.Back) # Connecting undo stack signals self.ui.action_Undo.triggered.connect(self.undo) self.ui.action_Redo.triggered.connect(self.redo) self.undo_stack.canRedoChanged[bool].connect(self.ui.action_Redo.setEnabled) self.undo_stack.canUndoChanged[bool].connect(self.ui.action_Undo.setEnabled) self.undo_stack.redoTextChanged["const QString&"].connect(self.changeRedoText) self.undo_stack.undoTextChanged["const QString&"].connect(self.changeUndoText) self.undo_stack.cleanChanged[bool].connect(self.ui.action_Save.setDisabled) # link_icon = QIcon() # pix = QPixmap(":/icons/link.png") # link_icon.addPixmap(pix, QIcon.Normal, QIcon.On) # pix = QPixmap(":/icons/link_broken.png") # link_icon.addPixmap(pix, QIcon.Normal, QIcon.Off) # self.link_icon = link_icon # #self.ui.linkViews.setIconSize(QSize(64,32)) # self.ui.linkViews.setIcon(link_icon) self._recent_projects_menu = QMenu(self) self.ui.actionRecent_projects.setMenu(self._recent_projects_menu) self._recent_projects_act = [] self._projects_mapper = QSignalMapper(self) self._projects_mapper.mapped[int].connect(self.loadRecentProject) self.param_dlg = None # Setting up the status bar bar = self.statusBar() # Adding current directory cur_dir = QLabel("") bar.addPermanentWidget(cur_dir) self._current_dir_label = cur_dir # Adding up zoom zoom = QLabel("") bar.addPermanentWidget(zoom) self._zoom_label = zoom self.changeZoom(1) self.loadConfig() parameters.instance.renderingChanged.connect(self.changeRendering) self.changeRendering() def changeRendering(self): if parameters.instance.use_OpenGL: self.ui.previousData.setViewport(QGLWidget(QGLFormat(QGL.SampleBuffers))) self.ui.currentData.setViewport(QGLWidget(QGLFormat(QGL.SampleBuffers))) else: self.ui.previousData.setViewport(QWidget()) self.ui.currentData.setViewport(QWidget()) def undo(self): self.undo_stack.undo() def redo(self): self.undo_stack.redo() def changeRedoText(self, text): self.ui.action_Redo.setText(text) self.ui.action_Redo.setToolTip(text) self.ui.action_Redo.setStatusTip(text) def changeUndoText(self, text): self.ui.action_Undo.setText(text) self.ui.action_Undo.setToolTip(text) self.ui.action_Undo.setStatusTip(text) def closeEvent(self, event): self.saveConfig() if not self.ensure_save_data("Exiting whith unsaved data", "The last modifications you made were not saved." " Are you sure you want to exit?"): event.ignore() return QMainWindow.closeEvent(self, event) #sys.exit(0) def loadConfig(self): params = parameters.instance self.ui.action_Show_vector.setChecked(params.show_vectors) self.ui.linkViews.setChecked(params.link_views) self.ui.action_Show_template.setChecked(parameters.instance.show_template) self.ui.actionShow_id.setChecked(parameters.instance.show_id) self.ui.action_Estimate_position.setChecked(parameters.instance.estimate) self.updateRecentFiles() params.recentProjectsChange.connect(self.updateRecentFiles) def updateRecentFiles(self): for a in self._recent_projects_act: self._projects_mapper.removeMappings(a) del self._recent_projects_act[:] menu = self._recent_projects_menu menu.clear() recent_projects = parameters.instance.recent_projects for i, p in enumerate(recent_projects): act = QAction(self) act.setText("&{0:d} {1}".format(i + 1, p)) self._recent_projects_act.append(act) act.triggered.connect(self._projects_mapper.map) self._projects_mapper.setMapping(act, i) menu.addAction(act) def saveConfig(self): parameters.instance.save() def check_for_data(self): if self._project is None: QMessageBox.critical(self, "No project loaded", "You have to load a project before performing this operation") return False return True def loadRecentProject(self, i): if self.ensure_save_data("Leaving unsaved data", "The last modifications you made were not saved." " Are you sure you want to change project?"): self.loadProject(parameters.instance.recent_projects[i]) @pyqtSignature("") def on_action_Open_project_triggered(self): if self.ensure_save_data("Leaving unsaved data", "The last modifications you made were not saved." " Are you sure you want to change project?"): dir_ = QFileDialog.getExistingDirectory(self, "Select a project directory", parameters.instance._last_dir) if dir_: self.loadProject(dir_) def loadProject(self, dir_): dir_ = path(dir_) project = Project(dir_) if project.valid: self._project = project else: create = QMessageBox.question(self, "Invalid project directory", "This directory does not contain a valid project. Turn into a directory?", QMessageBox.No, QMessageBox.Yes) if create == QMessageBox.No: return project.create() self._project = project self._project.use() parameters.instance.add_recent_project(dir_) parameters.instance._last_dir = dir_ if self._data is not None: _data = self._data _data.saved.disconnect(self.undo_stack.setClean) try: #self._project.load() self.load_data() _data = self._project.data _data.saved.connect(self.undo_stack.setClean) self._project.changedDataFile.connect(self.dataFileChanged) self._data = _data self._previousScene.changeDataManager(self._data) self._currentScene.changeDataManager(self._data) self.initFromData() self.projectAct.setEnabled(True) except TrackingDataException as ex: showException(self, "Error while loaded data", ex) def dataFileChanged(self, new_file): if new_file is None: self._current_dir_label.setText("") else: self._current_dir_label.setText(new_file) def initFromData(self): """ Initialize the interface using the current data """ self.ui.previousState.clear() self.ui.currentState.clear() for name in self._data.images_name: self.ui.previousState.addItem(name) self.ui.currentState.addItem(name) self.ui.previousState.setCurrentIndex(0) self.ui.currentState.setCurrentIndex(1) self._previousScene.changeImage(self._data.image_path(self._data.images_name[0])) self._currentScene.changeImage(self._data.image_path(self._data.images_name[1])) self.dataFileChanged(self._project.data_file) @pyqtSignature("int") def on_previousState_currentIndexChanged(self, index): #print "Previous image loaded: %s" % self._data.images[index] self.changeScene(self._previousScene, index) self._currentScene.changeImage(None) @pyqtSignature("int") def on_currentState_currentIndexChanged(self, index): #print "Current image loaded: %s" % self._data.images[index] self.changeScene(self._currentScene, index) def changeScene(self, scene, index): """ Set the scene to use the image number index. """ scene.changeImage(self._data.image_path(self._data.images_name[index])) @pyqtSignature("") def on_action_Save_triggered(self): self.save_data() @pyqtSignature("") def on_actionSave_as_triggered(self): fn = QFileDialog.getSaveFileName(self, "Select a data file to save in", self._project.data_dir, "CSV Files (*.csv);;All files (*.*)") if fn: self.save_data(path(fn)) def save_data(self, data_file=None): if self._data is None: raise TrackingDataException("Trying to save data when none have been loaded") try: self._project.save(data_file) return True except TrackingDataException as ex: showException(self, "Error while saving data", ex) return False def load_data(self, **opts): if self._project is None: raise TrackingDataException("Trying to load data when no project have been loaded") try: if self._project.load(**opts): log_debug("Data file was corrected. Need saving.") self.ui.action_Save.setEnabled(True) else: log_debug("Data file is clean.") self.ui.action_Save.setEnabled(False) return True except TrackingDataException as ex: showException(self, "Error while loading data", ex) return False except RetryTrackingDataException as ex: if retryException(self, "Problem while loading data", ex): new_opts = dict(opts) new_opts.update(ex.method_args) return self.load_data(**new_opts) return False def ensure_save_data(self, title, reason): if self._data is not None and not self.undo_stack.isClean(): button = QMessageBox.warning(self, title, reason, QMessageBox.Yes | QMessageBox.Save | QMessageBox.Cancel) if button == QMessageBox.Save: return self.save_data() elif button == QMessageBox.Cancel: return False self.undo_stack.clear() return True @pyqtSignature("") def on_action_Change_data_file_triggered(self): if self.ensure_save_data("Leaving unsaved data", "The last modifications you made were not saved." " Are you sure you want to change the current data file?"): fn = QFileDialog.getOpenFileName(self, "Select a data file to load", self._project.data_dir, "CSV Files (*.csv);;All files (*.*)") if fn: self._project.data_file = str(fn) if self.load_data(): self._previousScene.resetNewPoints() self._currentScene.resetNewPoints() @pyqtSignature("bool") def on_action_Show_vector_toggled(self, value): parameters.instance.show_vector = value self._currentScene.showVector(value) @pyqtSignature("bool") def on_action_Show_template_toggled(self, value): parameters.instance.show_template = value @pyqtSignature("bool") def on_actionShow_id_toggled(self, value): parameters.instance.show_id = value @pyqtSignature("") def on_action_Next_image_triggered(self): cur = self.ui.currentState.currentIndex() pre = self.ui.previousState.currentIndex() l = len(self._data.images_name) if cur < l-1 and pre < l-1: self.ui.previousState.setCurrentIndex(pre+1) self.ui.currentState.setCurrentIndex(cur+1) @pyqtSignature("") def on_action_Previous_image_triggered(self): cur = self.ui.currentState.currentIndex() pre = self.ui.previousState.currentIndex() if cur > 0 and pre > 0: self.ui.previousState.setCurrentIndex(pre-1) self.ui.currentState.setCurrentIndex(cur-1) @pyqtSignature("") def on_copyToPrevious_clicked(self): self._currentScene.copyFromLinked(self._previousScene) @pyqtSignature("") def on_copyToCurrent_clicked(self): self._previousScene.copyToLinked(self._currentScene) @pyqtSignature("bool") def on_action_Estimate_position_toggled(self, value): parameters.instance.estimate = value # @pyqtSignature("") # def on_action_Undo_triggered(self): # print "Undo" # @pyqtSignature("") # def on_action_Redo_triggered(self): # print "Redo" @pyqtSignature("bool") def on_action_Parameters_toggled(self, value): if value: from .parametersdlg import ParametersDlg self._previousScene.showTemplates() self._currentScene.showTemplates() #tracking_scene.saveParameters() parameters.instance.save() max_size = max(self._currentScene.width(), self._currentScene.height(), self._previousScene.width(), self._previousScene.height(), 400) self.param_dlg = ParametersDlg(max_size, self) self.param_dlg.setModal(False) self.ui.action_Pan.setChecked(True) self.ui.actionAdd_point.setEnabled(False) self.ui.action_Move_point.setEnabled(False) self.ui.actionAdd_cell.setEnabled(False) self.ui.actionRemove_cell.setEnabled(False) self.ui.action_Undo.setEnabled(False) self.ui.action_Redo.setEnabled(False) self.ui.action_Open_project.setEnabled(False) self.ui.actionRecent_projects.setEnabled(False) self.ui.action_Change_data_file.setEnabled(False) self.ui.copyToCurrent.setEnabled(False) self.ui.copyToPrevious.setEnabled(False) self.param_dlg.finished[int].connect(self.closeParam) self.param_dlg.show() elif self.param_dlg: self.param_dlg.accept() def closeParam(self, value): if value == QDialog.Rejected: parameters.instance.load() self.ui.actionAdd_point.setEnabled(True) self.ui.action_Move_point.setEnabled(True) self.ui.actionAdd_cell.setEnabled(True) self.ui.actionRemove_cell.setEnabled(True) self.ui.action_Undo.setEnabled(True) self.ui.action_Redo.setEnabled(True) self.ui.action_Open_project.setEnabled(True) self.ui.actionRecent_projects.setEnabled(True) self.ui.action_Change_data_file.setEnabled(True) self.ui.copyToCurrent.setEnabled(True) self.ui.copyToPrevious.setEnabled(True) self._previousScene.showTemplates(False) self._currentScene.showTemplates(False) self._previousScene.update() self._currentScene.update() self.param_dlg = None self.ui.action_Parameters.setChecked(False) @pyqtSignature("bool") def on_actionZoom_in_toggled(self, value): if value: self._previousScene.mode = TrackingScene.ZoomIn self._currentScene.mode = TrackingScene.ZoomIn @pyqtSignature("bool") def on_actionZoom_out_toggled(self, value): if value: self._previousScene.mode = TrackingScene.ZoomOut self._currentScene.mode = TrackingScene.ZoomOut #def resizeEvent(self, event): # self.ensureZoomFit() def ensureZoomFit(self): if self._data: prev_rect = self._previousScene.sceneRect() cur_rect = self._currentScene.sceneRect() prev_wnd = QRectF(self.ui.previousData.childrenRect()) cur_wnd = QRectF(self.ui.currentData.childrenRect()) prev_matrix = self.ui.previousData.matrix() cur_matrix = self.ui.currentData.matrix() prev_mapped_rect = prev_matrix.mapRect(prev_rect) cur_mapped_rect = cur_matrix.mapRect(cur_rect) if (prev_mapped_rect.width() < prev_wnd.width() or prev_mapped_rect.height() < prev_wnd.height() or cur_mapped_rect.width() < cur_wnd.width() or cur_mapped_rect.height() < cur_wnd.height()): self.on_action_Fit_triggered() @pyqtSignature("") def on_action_Fit_triggered(self): prev_rect = self._previousScene.sceneRect() cur_rect = self._currentScene.sceneRect() prev_wnd = self.ui.previousData.childrenRect() cur_wnd = self.ui.currentData.childrenRect() prev_sw = prev_wnd.width() / prev_rect.width() prev_sh = prev_wnd.height() / prev_rect.height() cur_sw = cur_wnd.width() / cur_rect.width() cur_sh = cur_wnd.height() / cur_rect.height() s = max(prev_sw, prev_sh, cur_sw, cur_sh) self.ui.previousData.resetMatrix() self.ui.previousData.scale(s, s) self.ui.currentData.resetMatrix() self.ui.currentData.scale(s, s) self.changeZoom(s) def zoomOut(self, point=None): self.ui.currentData.scale(0.5, 0.5) self.ui.previousData.scale(0.5, 0.5) self.changeZoom(self.ui.previousData.matrix().m11()) if point is not None: self.ui.previousData.centerOn(point) self.ui.currentData.centerOn(point) #self.ensureZoomFit() def zoomIn(self, point=None): self.ui.currentData.scale(2, 2) self.ui.previousData.scale(2, 2) self.changeZoom(self.ui.previousData.matrix().m11()) if point is not None: self.ui.previousData.centerOn(point) self.ui.currentData.centerOn(point) def changeZoom(self, zoom): self._zoom_label.setText("Zoom: %.5g%%" % (100*zoom)) @pyqtSignature("") def on_actionZoom_100_triggered(self): self.ui.previousData.resetMatrix() self.ui.currentData.resetMatrix() self.changeZoom(1) @pyqtSignature("bool") def on_actionAdd_point_toggled(self, value): if value: self._previousScene.mode = TrackingScene.Add self._currentScene.mode = TrackingScene.Add @pyqtSignature("bool") def on_actionAdd_cell_toggled(self, value): if value: self._previousScene.mode = TrackingScene.AddCell self._currentScene.mode = TrackingScene.AddCell @pyqtSignature("bool") def on_actionRemove_cell_toggled(self, value): if value: self._previousScene.mode = TrackingScene.RemoveCell self._currentScene.mode = TrackingScene.RemoveCell @pyqtSignature("bool") def on_action_Move_point_toggled(self, value): if value: self._previousScene.mode = TrackingScene.Move self._currentScene.mode = TrackingScene.Move @pyqtSignature("bool") def on_action_Pan_toggled(self, value): if value: self._previousScene.mode = TrackingScene.Pan self._currentScene.mode = TrackingScene.Pan @pyqtSignature("bool") def on_linkViews_toggled(self, value): parameters.instance.link_views = value phor = self.ui.previousData.horizontalScrollBar() pver = self.ui.previousData.verticalScrollBar() chor = self.ui.currentData.horizontalScrollBar() cver = self.ui.currentData.verticalScrollBar() if value: phor.valueChanged[int].connect(chor.setValue) pver.valueChanged[int].connect(cver.setValue) chor.valueChanged[int].connect(phor.setValue) cver.valueChanged[int].connect(pver.setValue) self._previousScene.templatePosChange.connect(self._currentScene.setTemplatePos) self._currentScene.templatePosChange.connect(self._previousScene.setTemplatePos) phor.setValue(chor.value()) pver.setValue(cver.value()) else: phor.valueChanged[int].disconnect(chor.setValue) pver.valueChanged[int].disconnect(cver.setValue) chor.valueChanged[int].disconnect(phor.setValue) cver.valueChanged[int].disconnect(pver.setValue) self._previousScene.templatePosChange.disconnect(self._currentScene.setTemplatePos) self._currentScene.templatePosChange.disconnect(self._previousScene.setTemplatePos) def copyFrom(self, start, items): if parameters.instance.estimate: dlg = createForm('copy_progress.ui', None) dlg.buttonBox.clicked["QAbstractButton*"].connect(self.cancelCopy) params = parameters.instance ts = params.template_size ss = params.search_size fs = params.filter_size self.copy_thread = algo.FindInAll(self._data, start, items, ts, ss, fs, self) dlg.imageProgress.setMaximum(self.copy_thread.num_images) self.copy_thread.start() self.copy_dlg = dlg dlg.exec_() else: algo.copyFromImage(self._data, start, items, self.undo_stack) def cancelCopy(self, *args): self.copy_thread.stop = True dlg = self.copy_dlg dlg.buttonBox.clicked['QAbstractButton*)'].disconnect(self.cancelCopy) self._previousScene.changeImage(None) self._currentScene.changeImage(None) def event(self, event): if isinstance(event, algo.NextImage): dlg = self.copy_dlg if dlg is not None: dlg.imageProgress.setValue(event.currentImage) dlg.pointProgress.setMaximum(event.nbPoints) dlg.pointProgress.setValue(0) return True elif isinstance(event, algo.NextPoint): dlg = self.copy_dlg if dlg is not None: dlg.pointProgress.setValue(event.currentPoint) return True elif isinstance(event, algo.FoundAll): dlg = self.copy_dlg if dlg is not None: self.cancelCopy() dlg.accept() return True elif isinstance(event, algo.Aborted): dlg = self.copy_dlg if dlg is not None: self.cancelCopy() dlg.accept() return True return QMainWindow.event(self, event) def itemsToCopy(self, scene): items = scene.getSelectedIds() if items: answer = QMessageBox.question(self, "Copy of points", "Some points were selected in the previous data window." " Do you want to copy only these point on the successive images?", QMessageBox.Yes, QMessageBox.No) if answer == QMessageBox.Yes: return items return scene.getAllIds() @pyqtSignature("") def on_actionCopy_from_previous_triggered(self): items = self.itemsToCopy(self._previousScene) if items: self.copyFrom(self.ui.previousState.currentIndex(), items) @pyqtSignature("") def on_actionCopy_from_current_triggered(self): items = self.itemsToCopy(self._currentScene) if items: self.copyFrom(self.ui.currentState.currentIndex(), items) @pyqtSignature("") def on_actionSelectPreviousAll_triggered(self): self._previousScene.selectAll() @pyqtSignature("") def on_actionSelectPreviousNew_triggered(self): self._previousScene.selectNew() @pyqtSignature("") def on_actionSelectPreviousNone_triggered(self): self._previousScene.selectNone() @pyqtSignature("") def on_actionSelectPreviousNon_associated_triggered(self): self._previousScene.selectNonAssociated() @pyqtSignature("") def on_actionSelectPreviousAssociated_triggered(self): self._previousScene.selectAssociated() @pyqtSignature("") def on_actionSelectPreviousInvert_triggered(self): self._previousScene.selectInvert() @pyqtSignature("") def on_actionSelectCurrentAll_triggered(self): self._currentScene.selectAll() @pyqtSignature("") def on_actionSelectCurrentNew_triggered(self): self._currentScene.selectNew() @pyqtSignature("") def on_actionSelectCurrentNone_triggered(self): self._currentScene.selectNone() @pyqtSignature("") def on_actionSelectCurrentNon_associated_triggered(self): self._currentScene.selectNonAssociated() @pyqtSignature("") def on_actionSelectCurrentAssociated_triggered(self): self._currentScene.selectAssociated() @pyqtSignature("") def on_actionSelectCurrentInvert_triggered(self): self._currentScene.selectInvert() def whichDelete(self): """ Returns a function deleting what the user wants """ dlg = createForm("deletedlg.ui", None) ret = dlg.exec_() if ret: if dlg.inAllImages.isChecked(): return TrackingScene.deleteInAllImages if dlg.toImage.isChecked(): return TrackingScene.deleteToImage if dlg.fromImage.isChecked(): return TrackingScene.deleteFromImage return lambda x: None @pyqtSignature("") def on_actionDelete_Previous_triggered(self): del_fct = self.whichDelete() del_fct(self._previousScene) @pyqtSignature("") def on_actionDelete_Current_triggered(self): del_fct = self.whichDelete() del_fct(self._currentScene) @pyqtSignature("") def on_actionMerge_points_triggered(self): if self._previousScene.mode == TrackingScene.AddCell: old_cell = self._previousScene.selected_cell new_cell = self._currentScene.selected_cell if old_cell is None or new_cell is None: QMessageBox.critical(self, "Cannot merge cells", "You have to select exactly one cell in the old state " "and one in the new state to merge them.") return try: if old_cell != new_cell: self.undo_stack.push(MergeCells(self._data, self._previousScene.image_name, old_cell, new_cell)) else: self.undo_stack.push(SplitCells(self._data, self._previousScene.image_name, old_cell, new_cell)) except AssertionError as error: QMessageBox.critical(self, "Cannot merge the cells", str(error)) else: old_pts = self._previousScene.getSelectedIds() new_pts = self._currentScene.getSelectedIds() if len(old_pts) != 1 or len(new_pts) != 1: QMessageBox.critical(self, "Cannot merge points", "You have to select exactly one point in the old state " "and one in the new state to link them.") return try: if old_pts != new_pts: self.undo_stack.push(ChangePointsId(self._data, self._previousScene.image_name, old_pts, new_pts)) else: log_debug("Splitting point of id %d" % old_pts[0]) self.undo_stack.push(SplitPointsId(self._data, self._previousScene.image_name, old_pts)) except AssertionError as error: QMessageBox.critical(self, "Cannot merge the points", str(error)) @pyqtSignature("") def on_actionCopy_selection_from_Current_triggered(self): cur_sel = self._currentScene.getSelectedIds() self._previousScene.setSelectedIds(cur_sel) @pyqtSignature("") def on_actionCopy_selection_from_Previous_triggered(self): cur_sel = self._previousScene.getSelectedIds() self._currentScene.setSelectedIds(cur_sel) @pyqtSignature("") def on_actionNew_data_file_triggered(self): if self.ensure_save_data("Leaving unsaved data", "The last modifications you made were not saved." " Are you sure you want to change the current data file?"): fn = QFileDialog.getSaveFileName(self, "Select a new data file to create", self._project.data_dir, "CSV Files (*.csv);;All files (*.*)") if fn: fn = path(fn) if fn.exists(): button = QMessageBox.question(self, "Erasing existing file", "Are you sure yo want to empty the file '%s' ?" % fn, QMessageBox.Yes, QMessageBox.No) if button == QMessageBox.No: return fn.remove() self._data.clear() self._previousScene.resetNewPoints() self._currentScene.resetNewPoints() self._project.data_file = fn self.initFromData() log_debug("Data file = %s" % (self._project.data_file,)) @pyqtSignature("") def on_actionAbout_triggered(self): dlg = QMessageBox(self) dlg.setWindowTitle("About Point Tracker") dlg.setIconPixmap(self.windowIcon().pixmap(64, 64)) #dlg.setTextFormat(Qt.RichText) dlg.setText("""Point Tracker Tool version %s rev %s Developper: Pierre Barbier de Reuille <*****@*****.**> Copyright 2008 """ % (__version__, __revision__)) img_read = ", ".join(str(s) for s in QImageReader.supportedImageFormats()) img_write = ", ".join(str(s) for s in QImageWriter.supportedImageFormats()) dlg.setDetailedText("""Supported image formats: - For reading: %s - For writing: %s """ % (img_read, img_write)) dlg.exec_() @pyqtSignature("") def on_actionAbout_Qt_triggered(self): QMessageBox.aboutQt(self, "About Qt") @pyqtSignature("") def on_actionReset_alignment_triggered(self): self.undo_stack.push(ResetAlignment(self._data)) @pyqtSignature("") def on_actionAlign_images_triggered(self): fn = QFileDialog.getOpenFileName(self, "Select a data file for alignment", self._project.data_dir, "CSV Files (*.csv);;All files (*.*)") if fn: d = self._data.copy() fn = path(fn) try: d.load(fn) except TrackingDataException as ex: showException(self, "Error while loading data file", ex) return if d._last_pt_id > 0: dlg = AlignmentDlg(d._last_pt_id+1, self) if dlg.exec_(): ref = dlg.ui.referencePoint.currentText() try: ref = int(ref) except ValueError: ref = str(ref) if dlg.ui.twoPointsRotation.isChecked(): r1 = int(dlg.ui.rotationPt1.currentText()) r2 = int(dlg.ui.rotationPt2.currentText()) rotation = ("TwoPoint", r1, r2) else: rotation = None else: return else: ref = 0 rotation = None try: shifts, angles = algo.alignImages(self._data, d, ref, rotation) self.undo_stack.push(AlignImages(self._data, shifts, angles)) except algo.AlgoException as ex: showException(self, "Error while aligning images", ex) def sceneSizeChanged(self): previous_rect = self._previousScene.real_scene_rect current_rect = self._currentScene.real_scene_rect rect = previous_rect | current_rect self._previousScene.setSceneRect(rect) self._currentScene.setSceneRect(rect) @pyqtSignature("") def on_actionEdit_timing_triggered(self): data = self._data dlg = TimeEditDlg(data.images_name, data.images_time, [data.image_path(n) for n in data.images_name], self) self.current_dlg = dlg if dlg.exec_() == QDialog.Accepted: self.undo_stack.push(ChangeTiming(data, [t for n, t in dlg.model])) del self.current_dlg @pyqtSignature("") def on_actionEdit_scales_triggered(self): data = self._data dlg = EditResDlg(data.images_name, data.images_scale, [data.image_path(n) for n in data.images_name], self) self.current_dlg = dlg if dlg.exec_() == QDialog.Accepted: self.undo_stack.push(ChangeScales(data, [sc for n, sc in dlg.model])) del self.current_dlg @pyqtSignature("") def on_actionCompute_growth_triggered(self): data = self._data dlg = GrowthComputationDlg(data, self) self.current_dlg = dlg dlg.exec_() del self.current_dlg @pyqtSignature("") def on_actionPlot_growth_triggered(self): data = self._data dlg = PlottingDlg(data, self) self.current_dlg = dlg dlg.exec_() del self.current_dlg @pyqtSignature("") def on_actionClean_cells_triggered(self): self.undo_stack.push(CleanCells(self._data)) @pyqtSignature("") def on_actionGotoCell_triggered(self): cells = [str(cid) for cid in self._data.cells] selected, ok = QInputDialog.getItem(self, "Goto cell", "Select the cell to go to", cells, 0) if ok: cid = int(selected) self.ui.actionAdd_cell.setChecked(True) data = self._data if cid not in data.cells: return ls = data.cells_lifespan[cid] prev_pos = self._previousScene.current_data._current_index cur_pos = self._currentScene.current_data._current_index full_poly = data.cells[cid] poly = [pid for pid in full_poly if pid in data[prev_pos]] #log_debug("Cell %d on time %d: %s" % (cid, prev_pos, poly)) if prev_pos < ls.start or prev_pos >= ls.end or not poly: for i in range(*ls.slice().indices(len(data))): poly = [pid for pid in full_poly if pid in data[i]] if poly: log_debug("Found cell %d on image %d with polygon %s" % (cid, i, poly)) new_prev_pos = i break else: log_debug("Cell %d found nowhere in range %s!!!" % (cid, ls.slice())) else: new_prev_pos = prev_pos new_cur_pos = min(max(cur_pos + new_prev_pos - prev_pos, 0), len(data)) self.ui.previousState.setCurrentIndex(new_prev_pos) self.ui.currentState.setCurrentIndex(new_cur_pos) self._previousScene.current_cell = cid self._currentScene.current_cell = cid prev_data = self._previousScene.current_data poly = data.cells[cid] prev_poly = QPolygonF([prev_data[ptid] for ptid in poly if ptid in prev_data]) prev_bbox = prev_poly.boundingRect() log_debug("Previous bounding box = %dx%d+%d+%d" % (prev_bbox.width(), prev_bbox.height(), prev_bbox.left(), prev_bbox.top())) self.ui.previousData.ensureVisible(prev_bbox) @pyqtSignature("") def on_actionGotoPoint_triggered(self): data = self._data points = [str(pid) for pid in data.cell_points] selected, ok = QInputDialog.getItem(self, "Goto point", "Select the point to go to", points, 0) if ok: pid = int(selected) self.ui.action_Move_point.setChecked(True) if pid not in data.cell_points: return prev_pos = self._previousScene.current_data._current_index cur_pos = self._currentScene.current_data._current_index prev_data = self._previousScene.current_data if not pid in prev_data: closest = -1 best_dist = len(data)+1 for img_data in data: if pid in img_data: dist = abs(img_data._current_index - prev_pos) if dist < best_dist: best_dist = dist closest = img_data._current_index new_prev_pos = closest else: new_prev_pos = prev_pos new_cur_pos = min(max(cur_pos + new_prev_pos - prev_pos, 0), len(data)) self.ui.previousState.setCurrentIndex(new_prev_pos) self.ui.currentState.setCurrentIndex(new_cur_pos) self._previousScene.setSelectedIds([pid]) self._currentScene.setSelectedIds([pid]) self.ui.previousData.centerOn(self._previousScene.current_data[pid])
class Table(QAbstractTableModel): def __init__(self, ob, parent): QAbstractTableModel.__init__(self) # store things self.ob = ob self.stack = QUndoStack() self._parent = parent # Make sure we have Name information first # _header contains the row headers self._header = [ QVariant('X'), QVariant('#'), QVariant(getattr(self.ob, 'UniqueName', 'name'))] # _tooltips contains the tooltips self._tooltips = [ QVariant('An X indicates row is disabled %s'%bool), QVariant('Comment for row'), QVariant('Object name %s'%str)] # _required is a list of required columns self._required = [] # _defaults is a dict of column -> default values self._defaults = {0: QVariant(False), 1: QVariant("")} # _optional is a list of optional columns self._optional = [2] # _cItems is a dict of column -> QVariant QStringList items, returned # to combo box self._cItems = {} # _cValues is a dict of column -> list of QVariant values, stored when # corresponding label stored by combobox self._cValues = {} # _idents is a list of identifier lookup fields self._idents = [] # _types is a list of types for validation self._types = [bool, str, str] # rows is a list of rows. each row is a list of QVariants self.rows = [] # work out the header and descriptions from the ArgInfo object a = ob.ArgInfo # for required names just process the ArgType object for name in a.required_names: self.__processArgType(name, a.descriptions[name]) # for defaulted names give it a default too for name, default in zip(a.default_names, a.default_values): self.__processArgType(name, a.descriptions[name], default = default) # for optional names flag it as optional for name in a.optional_names: self.__processArgType(name, a.descriptions[name], optional = True) # maps (filt, without, upto) -> (timestamp, stringList) self._cachedNameList = {} # this is the top left item visible in the TableView widget self.topLeftIndex = None def __processArgType(self, name, ob, **args): # this is the column index col = len(self._header) # If it's a name then be careful not to add it twice if name == getattr(self.ob, 'UniqueName', 'name'): assert ob.typ == str, 'Object name must be a string' self._tooltips[2] = QVariant(ob.desc) else: # add the header, type and tooltip self._header.append(QVariant(name)) self._types.append(ob.typ) self._tooltips.append(QVariant(ob.desc)) # if we have a default value, set it if 'default' in args: if args['default'] is None: self._defaults[col] = QVariant('None') else: self._defaults[col] = QVariant(args['default']) # if this is optional elif 'optional' in args: self._optional.append(col) # it must be required else: self._required.append(col) # if we have combo box items if hasattr(ob, 'labels'): self._cItems[col] = QVariant( QStringList([QString(str(x)) for x in ob.labels])) # if we have combo box values if hasattr(ob, 'values'): self._cValues[col] = [QVariant(x) for x in ob.values] # if it's an ident if hasattr(ob, 'ident'): self._idents.append(col) def __convert(self, variant, typ): # convert to the requested type val = variant.toString() if typ == bool: if val.toLower() == QString("true"): return (True, True) elif val.toLower() == QString("false"): return (False, True) elif "$(" in val: return (val, True) return (val, False) elif typ == int: if "$(" in val: return (val, True) return variant.toInt() elif typ == float: if "$(" in val: return (val, True) _, ret = variant.toDouble() return (val, ret) elif typ == str: return (variant.toString(), True) else: return (variant, False) def createElements(self, doc, name): # create xml elements from this table header = [ str(x.toString()) for x in self._header ] for row in self.rows: el = doc.createElement(name) # lookup and add attributes for i in range(2, len(row)): if not row[i].isNull(): val = str(row[i].toString()) # We want True and False to be capitalised in the xml file if self._types[i] == bool and val in ["true", "false"]: val = val.title() el.setAttribute(header[i], val) if len(row[1].toString()) > 0: doc.documentElement.appendChild(doc.createComment(str(row[1].toString()).strip())) if row[0].toBool() == True: # can't put -- in a comment unfortunately... el = doc.createComment(el.toxml().replace("--", "&dashdash;")) doc.documentElement.appendChild(el) def addNode(self, node, commented = False, commentText = ""): # add xml nodes as rows in the table w = [] row = [ QVariant() ] * len(self._header) self.rows.append(row) if commented: row[0] = QVariant(True) else: row[0] = QVariant(False) for attr, value in node.attributes.items(): attr = str(attr) value = str(value) index = -1 for i, item in enumerate(self._header): if str(item.toString()) == attr: index = i break if index == -1: w.append('%s doesn\'t have attr %s' % (node.nodeName, attr)) continue typ = self._types[index] row[index] = QVariant(self.__convert(QVariant(value), typ)[0]) if not commented: invalid = self._isInvalid(row[index], len(self.rows)-1, index) if invalid: w.append('%s.%s: %s' %(node.nodeName, attr, invalid)) # add the row to the table if commentText: row[1] = QVariant(commentText) return w def flags(self, index): return Qt.ItemIsSelectable | Qt.ItemIsEnabled | Qt.ItemIsEditable def rowCount(self, parent = None): return len(self.rows) def columnCount(self, parent = None): return len(self._header) def headerData(self, section, orientation, role = Qt.DisplayRole ): if role == Qt.DisplayRole: if orientation == Qt.Horizontal: return self._header[section] elif self.rows and self.rows[section] and \ not self.rows[section][2].isNull(): return self.rows[section][2] else: return QVariant('[row %s]' % (section+1)) elif role == Qt.ToolTipRole and orientation == Qt.Horizontal: return self._tooltips[section] else: return QVariant() # put the change request on the undo stack def setData(self, index, val, role=Qt.EditRole): if role == Qt.EditRole and \ val != self.rows[index.row()][index.column()]: col = index.column() if col in self._cValues: # lookup the display in the list of _cItems for i, v in enumerate(self._cItems[col].toStringList()): if v.toLower() == val.toString().toLower(): val = self._cValues[col][i] self.stack.push( ChangeValueCommand(index.row(), index.column(), val, self)) return True else: return False def insertRows(self, row, count, parent = QModelIndex()): if count > 1: self.stack.beginMacro('Insert rows %d..%d'%(row+1, row+count)) for row in range(row, row+count): self.stack.push(RowCommand(row, self, parent)) if count > 1: self.stack.endMacro() def removeRows(self, row, count, parent = QModelIndex()): if count > 1: self.stack.beginMacro('Remove rows %d..%d'%(row+1, row+count)) for row in reversed(range(row, row+count)): self.stack.push(RowCommand(row, self, parent, False)) if count > 1: self.stack.endMacro() def sectionMoved(self, logicalIndex, oldVisualIndex, newVisualIndex, parent = QModelIndex()): assert oldVisualIndex == logicalIndex, \ "oldVisualIndex %d should be equal to logicalIndex %d" % ( oldVisualIndex, logicalIndex) self.stack.beginMacro('Move row %d to %d'%(oldVisualIndex, newVisualIndex)) # grab the old data olddata = [QVariant(x) for x in self.rows[oldVisualIndex]] # delete the old row self.stack.push(RowCommand(oldVisualIndex, self, parent, False)) # create a command to make a new row with the old data cmd = RowCommand(newVisualIndex, self, parent) cmd.rowdata = olddata self.stack.push(cmd) self.stack.endMacro() def _isCommented(self, row): return self.rows[row][0].toBool() def _nameList(self, filt = None, without = None, upto = None): # need to search all tables for a string list of object names # filt is a ModuleBase subclass to filter by # without is a row number to exclude from the current table # upto means only look at objects up to "upto" row in the current table if (filt, without, upto) in self._cachedNameList: timestamp, sl = self._cachedNameList[(filt, without, upto)] if self._parent.lastModified() < timestamp: return sl sl = QStringList() for name in self._parent.getTableNames(): table = self._parent._tables[name] # if we have a filter, then make sure this table is a subclass of it if filt is not None and type(filt) == types.ClassType and \ type(table.ob) == types.ClassType and \ not issubclass(table.ob, filt): # if we are only going up to a certain table and this is it if table == self and upto is not None: return sl continue for i,trow in enumerate(table.rows): if table == self: # if the current table is self, make sure we are excluding # the without row if without is not None and without == i: continue # make sure we only go up to upto if upto is not None and upto == i: return sl # add a non-null name, which is not commented out to the list if not trow[2].isNull() and not \ (not trow[0].isNull() and trow[0].toBool() == True): sl.append(trow[2].toString()) # store the cached value self._cachedNameList[(filt, without, upto)] = (time.time(), sl) return sl def _isInvalid(self, value, row, col): # check that required rows are filled in if value.isNull(): if col in self._required: return 'Required argument not filled in' else: return False # check that names are unique elif col == 2: name = value.toString() index = self._nameList(without = row).indexOf(name) if index != -1: return 'Object with name "%s" already exists' % name # check that idents are valid elif col in self._idents: name = value.toString() ob = self._types[col] index = self._nameList(filt = ob, upto = row).indexOf(name) if index == -1: return 'Can\'t perform identifier lookup on "%s"' % name # check that enums are valid elif col in self._cValues: if not max([value == x for x in self._cValues[col]]): return '"%s" is not a supported enum' % value.toString() # check that choices are valid elif col in self._cItems: if not max( [value == QVariant(x) for x in self._cItems[col].toStringList()]): return '"%s" is not a supported choice' % value.toString() # check the type of basetypes else: typ = self._types[col] v, ret = self.__convert(value, typ) if ret != True: return 'Cannot convert "%s" to %s' % (value.toString(), typ) return False def _isDefault(self, value, col): return value.isNull() and col in self._defaults def data(self, index, role): col = index.column() row = index.row() value = self.rows[row][col] # default view if role == Qt.DisplayRole: # comment row if col == 1: if len(value.toString()) > 0: return QVariant("#..") else: return QVariant() # if the cell is defaulted, display the default value elif self._isDefault(value, col): value = self._defaults[col] # if we've got a combo box lookup the appropriate value for the enum if col in self._cValues: # lookup the display in the list of _cItems for i, v in enumerate(self._cValues[col]): if v.toString() == value.toString(): return QVariant(self._cItems[col].toStringList()[i]) # display commented out rows as X elif col == 0: if value.toBool(): return QVariant(QString('X')) else: return QVariant(QString('')) # empty string rows should be "" elif not value.isNull() and self._types[col] == str and \ str(value.toString()) == '' and col != 1: value = QVariant(QString('""')) return value # text editor elif role == Qt.EditRole: # if the cell is defaulted, display the default value if self._isDefault(value, col): value = self._defaults[col] # if we've got a combo box lookup the appropriate value for the enum if col in self._cValues: # lookup the display in the list of _cItems for i, v in enumerate(self._cValues[col]): if v.toString() == value.toString(): return QVariant(self._cItems[col].toStringList()[i]) # empty string rows should be "" elif not value.isNull() and self._types[col] == str and \ str(value.toString()) == '' and col != 1: value = QVariant(QString('""')) return value elif role == Qt.ToolTipRole: # tooltip error = self._isInvalid(value, row, col) text = str(self._tooltips[col].toString()) if error: text = '***Error: %s\n%s'%(error, text) if col in self._idents: lines = ['\nPossible Values: '] for name in self._nameList(filt = self._types[col], upto = row): if len(lines[-1]) > 80: lines.append('') lines[-1] += str(name) + ', ' text += '\n'.join(lines).rstrip(' ,') if col == 1 and len(value.toString()) > 0: text += ":\n\n" + value.toString() return QVariant(text) elif role == Qt.ForegroundRole: # cell foreground if self._isCommented(row): # comment return QVariant(QColor(120,140,180)) if self._isDefault(value, col): # is default arg (always valid) return QVariant(QColor(160,160,160)) elif self._isInvalid(value, row, col): # invalid return QVariant(QColor(255,0,0)) else: # valid return QVariant(QColor(0,0,0)) elif role == Qt.BackgroundRole: # cell background if self._isCommented(row): # commented return QVariant(QColor(160,180,220)) elif self._isInvalid(value, row, col): # invalid return QVariant(QColor(255,200,200)) elif col in self._defaults: # has default return QVariant(QColor(255,255,240)) elif col in self._optional: #is optional return QVariant(QColor(180,180,180)) else: # valid return QVariant(QColor(250,250,250)) elif role == Qt.UserRole: # combo box asking for list of items if col in self._idents: return QVariant( self._nameList(filt = self._types[col], upto = row)) elif col in self._cItems: return self._cItems[col] else: return QVariant() else: return QVariant() def clearIndexes(self, indexes): # clear cells from a list of QModelIndex's begun = False for item in indexes: if not self.rows[item.row()][item.column()].isNull(): if not begun: begun = True celltexts = [ '(%s, %d)' % (self._header[c.column()].toString(), c.row() + 1) \ for c in indexes ] self.stack.beginMacro('Cleared Cells: '+' '.join(celltexts)) self.setData(item, QVariant(), Qt.EditRole) if begun: self.stack.endMacro()
class UmbraLayer(object): count=0 layer_count=0 def __init__(self,umbra,grid,name=None): """ Does not add the layers to the GUI - call register_layers for that. """ # having some trouble getting reliable output from the log... if 0: self.log=log else: class DumbLog(object): def info(self,*a): s=" ".join(a) with open(os.path.join(os.path.dirname(__file__),'log'),'a') as fp: fp.write(s+"\n") fp.flush() log=DumbLog() log.debug=log.info log.warning=log.info log.error=log.info self.log=log self.umbra=umbra self.grid=grid self.iface=None # gets set in register_layers UmbraLayer.layer_count+=1 if name is None: name="grid%d"%UmbraLayer.layer_count self.name=name self.layers=[] # SubLayer objects associated with this grid. self.umbra.register_grid(self) self.undo_stack=QUndoStack() def match_to_qlayer(self,ql): # need to either make the names unique, or find a better test. # since we can't really enforce the grouped layout, better to make the names # unique. for layer in self.layers: if layer.qlayer.name() == ql.name(): return True return False def grid_name(self): """ used to label the group """ UmbraLayer.count+=1 return "grid%4d"%UmbraLayer.count @classmethod def open_layer(klass,umbra,grid_format,path): g=klass.load_grid(path=path,grid_format=grid_format) return klass(umbra=umbra,grid=g) @classmethod def load_grid(klass,grid_format=None,path=None): if path is None: # for development, load sample data: suntans_path=os.path.join( os.path.dirname(__file__), "sample_data/sfbay" ) grid=unstructured_grid.SuntansGrid(suntans_path) else: if grid_format=='SUNTANS': grid=unstructured_grid.SuntansGrid(path) elif grid_format=='pickle': grid=unstructured_grid.UnstructuredGrid.from_pickle(path) elif grid_format=='DFM': grid=dfm_grid.DFMGrid(fn=path) else: raise Exception("Need to add other grid types, like %s!"%grid_format) return grid def on_layer_deleted(self,sublayer): self.layers.remove(sublayer) self.log.info("signal received: node layer deleted") sublayer.unextend_grid() # these are the steps which lead to this callback. don't do them # again. # reg=QgsMapLayerRegistry.instance() # reg.removeMapLayers([sublayer.qlayer]) # create the memory layers and populate accordingly def register_layer(self,sublayer): self.layers.append( sublayer ) def callback(sublayer=sublayer): self.on_layer_deleted(sublayer) sublayer.qlayer.layerDeleted.connect(callback) QgsMapLayerRegistry.instance().addMapLayer(sublayer.qlayer) li=self.iface.legendInterface() li.moveLayer(sublayer.qlayer,self.group_index) def layer_by_tag(self,tag): for layer in self.layers: if layer.tag == tag: return layer return None def create_group(self): # Create a group for the layers - li=self.iface.legendInterface() grp_name=self.grid_name() self.group_index=li.addGroup(grp_name) def register_layers(self): crs="?crs=epsg:26910" # was 4326 self.iface=self.umbra.iface self.create_group() self.register_layer( UmbraCellLayer(self.log, self.grid, crs=crs, prefix=self.name, tag='cells') ) self.register_layer( UmbraEdgeLayer(self.log, self.grid, crs=crs, prefix=self.name, tag='edges' ) ) self.register_layer( UmbraNodeLayer(self.log, self.grid, crs=crs, prefix=self.name, tag='nodes') ) # set extent to the extent of our layer # skip while developing # canvas.setExtent(layer.extent()) def remove_all_qlayers(self): layers=[] for sublayer in self.layers: layers.append( sublayer.qlayer.name() ) reg=QgsMapLayerRegistry.instance() self.log.info("Found %d layers to remove"%len(layers)) reg.removeMapLayers(layers) def distance_to_node(self,pnt,i): """ compute distance from the given point to the given node, returned in physical distance units [meters]""" if not self.grid: return 1e6 return mag( self.grid.nodes['x'][i] - np.asarray(pnt) ) def distance_to_cell(self,pnt,c): if not self.grid: return 1e6 return mag( self.grid.cells_center()[c] - np.asarray(pnt) ) def find_closest_node(self,xy): # xy: [x,y] print "Finding closest node to ",xy return self.grid.select_nodes_nearest(xy) def find_closest_cell(self,xy): # xy: [x,y] return self.grid.select_cells_nearest(xy) def extent(self): xmin,xmax,ymin,ymax = self.grid.bounds() print "extent() called on UmbraLayer" return QgsRectangle(xmin,ymin,xmax,ymax) def renumber(self): self.grid.renumber() # thin wrapper to grid editing calls # channel them through here to (a) keep consistent interface # and (b) track undo stacks at the umbra layer level. def undo(self): if self.undo_stack.canUndo(): self.undo_stack.undo() else: self.log.warning("request for undo, but stack cannot") def redo(self): if self.undo_stack.canRedo(): self.undo_stack.redo() else: self.log.warning("request for undo, but stack cannot") def modify_node(self,n,**kw): cmd=GridCommand(self.grid, "Modify node", lambda: self.grid.modify_node(n,**kw)) self.undo_stack.push(cmd) def toggle_cell_at_point(self,xy): def do_toggle(): self.log.info("umbra_layer: toggle cell at %s"%xy) self.grid.toggle_cell_at_point(xy) cmd=GridCommand(self.grid, "Toggle cell", lambda: self.grid.toggle_cell_at_point(xy)) self.undo_stack.push(cmd) def delete_node(self,n): cmd=GridCommand(self.grid, "Delete node", lambda: self.grid.delete_node_cascade(n)) self.undo_stack.push(cmd) def delete_edge(self,e): cmd=GridCommand(self.grid, "Delete edge", lambda: self.grid.delete_edge_cascade(e)) self.undo_stack.push(cmd) def add_edge(self,nodes): if self.grid.nodes_to_edge(*nodes) is not None: self.log.info("Edge already existed, probably") return self.add_edge_last_id=None def redo(): j=self.grid.add_edge(nodes=nodes) self.add_edge_last_id=j cmd=GridCommand(self.grid,"Add edge",redo) self.undo_stack.push(cmd) assert self.add_edge_last_id is not None self.log.info("Adding an edge! j=%d"%self.add_edge_last_id) return self.add_edge_last_id def add_node(self,x): # awkward jumping through hoops to both use the undo stack # and get the id of a node which was just added self.add_node_last_id=None def redo(): n=self.grid.add_node(x=x) self.add_node_last_id=n cmd=GridCommand(self.grid,"Add node",redo) self.undo_stack.push(cmd) assert self.add_node_last_id is not None return self.add_node_last_id def delete_selected(self): cell_layer=self.layer_by_tag('cells') if cell_layer is not None: selected_cells = cell_layer.selection() self.log.info("Found %d selected cells"%len(selected_cells)) def redo(cells=selected_cells): # pass this way b/c of python bindings weirdness for c in cells: self.grid.delete_cell(c) cmd=GridCommand(self.grid,"Delete cells",redo) self.undo_stack.push(cmd)
class TrackingWindow(QMainWindow): """ Main window of the application. This class is responsible for the global data structures too. :IVariables: undo_stack : `QUndoStack` Undo stack. All actions that can be undone should be pushed on the stack. _project : `project.Project` Project object managing the loaded data _data : `tracking_data.TrackingData` Data object keeping track of points and cells toolGroup : `QActionGroup` Group of actions to be enabled only when actions can be taken on images previousSelAct : `QActionGroup` Actions enabled when points are selected in the previous pane currentSelAct : `QActionGroup` Actions enabled when points are selected in the current pane projectAct : `QActionGroup` Actions to enable once a project is loaded _previousScene : `tracking_scene.TrackingScene` Object managing the previous pane _currentScene : `tracking_scene.LinkedTrackingScene` Object managing the current pane """ def __init__(self, *args, **kwords): QMainWindow.__init__(self, *args) self.undo_stack = QUndoStack(self) self.ui = Ui_TrackingWindow() self.ui.setupUi(self) self._project = None self._data = None self.toolGroup = QActionGroup(self) self.toolGroup.addAction(self.ui.actionAdd_point) self.toolGroup.addAction(self.ui.action_Move_point) self.toolGroup.addAction(self.ui.actionAdd_cell) self.toolGroup.addAction(self.ui.actionRemove_cell) self.toolGroup.addAction(self.ui.action_Pan) self.toolGroup.addAction(self.ui.actionZoom_out) self.toolGroup.addAction(self.ui.actionZoom_in) self.previousSelAct = QActionGroup(self) self.previousSelAct.addAction( self.ui.actionCopy_selection_from_Previous) self.previousSelAct.addAction(self.ui.actionDelete_Previous) self.previousSelAct.setEnabled(False) self.currentSelAct = QActionGroup(self) self.currentSelAct.addAction(self.ui.actionCopy_selection_from_Current) self.currentSelAct.addAction(self.ui.actionDelete_Current) self.currentSelAct.setEnabled(False) self.projectAct = QActionGroup(self) self.projectAct.addAction(self.ui.action_Next_image) self.projectAct.addAction(self.ui.action_Previous_image) self.projectAct.addAction(self.ui.actionAdd_point) self.projectAct.addAction(self.ui.action_Move_point) self.projectAct.addAction(self.ui.action_Pan) self.projectAct.addAction(self.ui.actionAdd_cell) self.projectAct.addAction(self.ui.actionRemove_cell) self.projectAct.addAction(self.ui.action_Change_data_file) self.projectAct.addAction(self.ui.actionNew_data_file) self.projectAct.addAction(self.ui.actionZoom_out) self.projectAct.addAction(self.ui.actionZoom_in) self.projectAct.addAction(self.ui.actionSave_as) self.projectAct.addAction(self.ui.action_Fit) self.projectAct.addAction(self.ui.actionZoom_100) self.projectAct.addAction(self.ui.actionMerge_points) self.projectAct.addAction(self.ui.actionCopy_from_previous) self.projectAct.addAction(self.ui.actionCopy_from_current) self.projectAct.addAction(self.ui.actionReset_alignment) self.projectAct.addAction(self.ui.actionAlign_images) self.projectAct.addAction(self.ui.actionSelectPreviousAll) self.projectAct.addAction(self.ui.actionSelectPreviousNew) self.projectAct.addAction(self.ui.actionSelectPreviousNone) self.projectAct.addAction(self.ui.actionSelectPreviousNon_associated) self.projectAct.addAction(self.ui.actionSelectPreviousAssociated) self.projectAct.addAction(self.ui.actionSelectPreviousInvert) self.projectAct.addAction(self.ui.actionSelectCurrentAll) self.projectAct.addAction(self.ui.actionSelectCurrentNew) self.projectAct.addAction(self.ui.actionSelectCurrentNone) self.projectAct.addAction(self.ui.actionSelectCurrentNon_associated) self.projectAct.addAction(self.ui.actionSelectCurrentAssociated) self.projectAct.addAction(self.ui.actionSelectCurrentInvert) self.projectAct.addAction(self.ui.actionEdit_timing) self.projectAct.addAction(self.ui.actionEdit_scales) self.projectAct.addAction(self.ui.actionCompute_growth) self.projectAct.addAction(self.ui.actionClean_cells) self.projectAct.addAction(self.ui.actionGotoCell) self.projectAct.setEnabled(False) current_sel_actions = [ self.ui.actionSelectCurrentAll, self.ui.actionSelectCurrentNew, self.ui.actionSelectCurrentNone, self.ui.actionSelectCurrentInvert, '-', self.ui.actionSelectCurrentNon_associated, self.ui.actionSelectCurrentAssociated, self.ui.actionCopy_selection_from_Previous ] previous_sel_actions = [ self.ui.actionSelectPreviousAll, self.ui.actionSelectPreviousNew, self.ui.actionSelectPreviousNone, self.ui.actionSelectPreviousInvert, '-', self.ui.actionSelectPreviousNon_associated, self.ui.actionSelectPreviousAssociated, self.ui.actionCopy_selection_from_Current ] self._previousScene = TrackingScene(self.undo_stack, self.ui.actionDelete_Previous, previous_sel_actions, self) self._currentScene = LinkedTrackingScene(self._previousScene, self.undo_stack, self.ui.actionDelete_Current, current_sel_actions, self) self._previousScene.hasSelectionChanged.connect( self.previousSelAct.setEnabled) self._currentScene.hasSelectionChanged.connect( self.currentSelAct.setEnabled) self._previousScene.realSceneSizeChanged.connect(self.sceneSizeChanged) self._currentScene.realSceneSizeChanged.connect(self.sceneSizeChanged) self._previousScene.zoomIn[QPointF].connect(self.zoomIn) self._currentScene.zoomIn.connect(self.zoomIn) self._previousScene.zoomOut[QPointF].connect(self.zoomOut) self._currentScene.zoomOut.connect(self.zoomOut) self.ui.previousData.setScene(self._previousScene) self.ui.currentData.setScene(self._currentScene) self.ui.previousData.setDragMode(QGraphicsView.ScrollHandDrag) self.ui.currentData.setDragMode(QGraphicsView.ScrollHandDrag) #self.ui.previousData.setCacheMode(QGraphicsView.CacheBackground) #self.ui.currentData.setCacheMode(QGraphicsView.CacheBackground) # Redefine shortcuts to standard key sequences self.ui.action_Save.setShortcut(QKeySequence.Save) self.ui.actionSave_as.setShortcut(QKeySequence.SaveAs) self.ui.action_Open_project.setShortcut(QKeySequence.Open) self.ui.action_Undo.setShortcut(QKeySequence.Undo) self.ui.action_Redo.setShortcut(QKeySequence.Redo) self.ui.action_Next_image.setShortcut(QKeySequence.Forward) self.ui.action_Previous_image.setShortcut(QKeySequence.Back) # Connecting undo stack signals self.ui.action_Undo.triggered.connect(self.undo) self.ui.action_Redo.triggered.connect(self.redo) self.undo_stack.canRedoChanged[bool].connect( self.ui.action_Redo.setEnabled) self.undo_stack.canUndoChanged[bool].connect( self.ui.action_Undo.setEnabled) self.undo_stack.redoTextChanged["const QString&"].connect( self.changeRedoText) self.undo_stack.undoTextChanged["const QString&"].connect( self.changeUndoText) self.undo_stack.cleanChanged[bool].connect( self.ui.action_Save.setDisabled) # link_icon = QIcon() # pix = QPixmap(":/icons/link.png") # link_icon.addPixmap(pix, QIcon.Normal, QIcon.On) # pix = QPixmap(":/icons/link_broken.png") # link_icon.addPixmap(pix, QIcon.Normal, QIcon.Off) # self.link_icon = link_icon # #self.ui.linkViews.setIconSize(QSize(64,32)) # self.ui.linkViews.setIcon(link_icon) self._recent_projects_menu = QMenu(self) self.ui.actionRecent_projects.setMenu(self._recent_projects_menu) self._recent_projects_act = [] self._projects_mapper = QSignalMapper(self) self._projects_mapper.mapped[int].connect(self.loadRecentProject) self.param_dlg = None # Setting up the status bar bar = self.statusBar() # Adding current directory cur_dir = QLabel("") bar.addPermanentWidget(cur_dir) self._current_dir_label = cur_dir # Adding up zoom zoom = QLabel("") bar.addPermanentWidget(zoom) self._zoom_label = zoom self.changeZoom(1) self.loadConfig() parameters.instance.renderingChanged.connect(self.changeRendering) self.changeRendering() def changeRendering(self): if parameters.instance.use_OpenGL: self.ui.previousData.setViewport( QGLWidget(QGLFormat(QGL.SampleBuffers))) self.ui.currentData.setViewport( QGLWidget(QGLFormat(QGL.SampleBuffers))) else: self.ui.previousData.setViewport(QWidget()) self.ui.currentData.setViewport(QWidget()) def undo(self): self.undo_stack.undo() def redo(self): self.undo_stack.redo() def changeRedoText(self, text): self.ui.action_Redo.setText(text) self.ui.action_Redo.setToolTip(text) self.ui.action_Redo.setStatusTip(text) def changeUndoText(self, text): self.ui.action_Undo.setText(text) self.ui.action_Undo.setToolTip(text) self.ui.action_Undo.setStatusTip(text) def closeEvent(self, event): self.saveConfig() if not self.ensure_save_data( "Exiting whith unsaved data", "The last modifications you made were not saved." " Are you sure you want to exit?"): event.ignore() return QMainWindow.closeEvent(self, event) #sys.exit(0) def loadConfig(self): params = parameters.instance self.ui.action_Show_vector.setChecked(params.show_vectors) self.ui.linkViews.setChecked(params.link_views) self.ui.action_Show_template.setChecked( parameters.instance.show_template) self.ui.actionShow_id.setChecked(parameters.instance.show_id) self.ui.action_Estimate_position.setChecked( parameters.instance.estimate) self.updateRecentFiles() params.recentProjectsChange.connect(self.updateRecentFiles) def updateRecentFiles(self): for a in self._recent_projects_act: self._projects_mapper.removeMappings(a) del self._recent_projects_act[:] menu = self._recent_projects_menu menu.clear() recent_projects = parameters.instance.recent_projects for i, p in enumerate(recent_projects): act = QAction(self) act.setText("&{0:d} {1}".format(i + 1, p)) self._recent_projects_act.append(act) act.triggered.connect(self._projects_mapper.map) self._projects_mapper.setMapping(act, i) menu.addAction(act) def saveConfig(self): parameters.instance.save() def check_for_data(self): if self._project is None: QMessageBox.critical( self, "No project loaded", "You have to load a project before performing this operation") return False return True def loadRecentProject(self, i): if self.ensure_save_data( "Leaving unsaved data", "The last modifications you made were not saved." " Are you sure you want to change project?"): self.loadProject(parameters.instance.recent_projects[i]) @pyqtSignature("") def on_action_Open_project_triggered(self): if self.ensure_save_data( "Leaving unsaved data", "The last modifications you made were not saved." " Are you sure you want to change project?"): dir_ = QFileDialog.getExistingDirectory( self, "Select a project directory", parameters.instance._last_dir) if dir_: self.loadProject(dir_) def loadProject(self, dir_): dir_ = path(dir_) project = Project(dir_) if project.valid: self._project = project else: create = QMessageBox.question( self, "Invalid project directory", "This directory does not contain a valid project. Turn into a directory?", QMessageBox.No, QMessageBox.Yes) if create == QMessageBox.No: return project.create() self._project = project self._project.use() parameters.instance.add_recent_project(dir_) parameters.instance._last_dir = dir_ if self._data is not None: _data = self._data _data.saved.disconnect(self.undo_stack.setClean) try: #self._project.load() self.load_data() _data = self._project.data _data.saved.connect(self.undo_stack.setClean) self._project.changedDataFile.connect(self.dataFileChanged) self._data = _data self._previousScene.changeDataManager(self._data) self._currentScene.changeDataManager(self._data) self.initFromData() self.projectAct.setEnabled(True) except TrackingDataException as ex: showException(self, "Error while loaded data", ex) def dataFileChanged(self, new_file): if new_file is None: self._current_dir_label.setText("") else: self._current_dir_label.setText(new_file) def initFromData(self): """ Initialize the interface using the current data """ self.ui.previousState.clear() self.ui.currentState.clear() for name in self._data.images_name: self.ui.previousState.addItem(name) self.ui.currentState.addItem(name) self.ui.previousState.setCurrentIndex(0) self.ui.currentState.setCurrentIndex(1) self._previousScene.changeImage( self._data.image_path(self._data.images_name[0])) self._currentScene.changeImage( self._data.image_path(self._data.images_name[1])) self.dataFileChanged(self._project.data_file) @pyqtSignature("int") def on_previousState_currentIndexChanged(self, index): #print "Previous image loaded: %s" % self._data.images[index] self.changeScene(self._previousScene, index) self._currentScene.changeImage(None) @pyqtSignature("int") def on_currentState_currentIndexChanged(self, index): #print "Current image loaded: %s" % self._data.images[index] self.changeScene(self._currentScene, index) def changeScene(self, scene, index): """ Set the scene to use the image number index. """ scene.changeImage(self._data.image_path(self._data.images_name[index])) @pyqtSignature("") def on_action_Save_triggered(self): self.save_data() @pyqtSignature("") def on_actionSave_as_triggered(self): fn = QFileDialog.getSaveFileName(self, "Select a data file to save in", self._project.data_dir, "CSV Files (*.csv);;All files (*.*)") if fn: self.save_data(path(fn)) def save_data(self, data_file=None): if self._data is None: raise TrackingDataException( "Trying to save data when none have been loaded") try: self._project.save(data_file) return True except TrackingDataException as ex: showException(self, "Error while saving data", ex) return False def load_data(self, **opts): if self._project is None: raise TrackingDataException( "Trying to load data when no project have been loaded") try: if self._project.load(**opts): log_debug("Data file was corrected. Need saving.") self.ui.action_Save.setEnabled(True) else: log_debug("Data file is clean.") self.ui.action_Save.setEnabled(False) return True except TrackingDataException as ex: showException(self, "Error while loading data", ex) return False except RetryTrackingDataException as ex: if retryException(self, "Problem while loading data", ex): new_opts = dict(opts) new_opts.update(ex.method_args) return self.load_data(**new_opts) return False def ensure_save_data(self, title, reason): if self._data is not None and not self.undo_stack.isClean(): button = QMessageBox.warning( self, title, reason, QMessageBox.Yes | QMessageBox.Save | QMessageBox.Cancel) if button == QMessageBox.Save: return self.save_data() elif button == QMessageBox.Cancel: return False self.undo_stack.clear() return True @pyqtSignature("") def on_action_Change_data_file_triggered(self): if self.ensure_save_data( "Leaving unsaved data", "The last modifications you made were not saved." " Are you sure you want to change the current data file?"): fn = QFileDialog.getOpenFileName( self, "Select a data file to load", self._project.data_dir, "CSV Files (*.csv);;All files (*.*)") if fn: self._project.data_file = str(fn) if self.load_data(): self._previousScene.resetNewPoints() self._currentScene.resetNewPoints() @pyqtSignature("bool") def on_action_Show_vector_toggled(self, value): parameters.instance.show_vector = value self._currentScene.showVector(value) @pyqtSignature("bool") def on_action_Show_template_toggled(self, value): parameters.instance.show_template = value @pyqtSignature("bool") def on_actionShow_id_toggled(self, value): parameters.instance.show_id = value @pyqtSignature("") def on_action_Next_image_triggered(self): cur = self.ui.currentState.currentIndex() pre = self.ui.previousState.currentIndex() l = len(self._data.images_name) if cur < l - 1 and pre < l - 1: self.ui.previousState.setCurrentIndex(pre + 1) self.ui.currentState.setCurrentIndex(cur + 1) @pyqtSignature("") def on_action_Previous_image_triggered(self): cur = self.ui.currentState.currentIndex() pre = self.ui.previousState.currentIndex() if cur > 0 and pre > 0: self.ui.previousState.setCurrentIndex(pre - 1) self.ui.currentState.setCurrentIndex(cur - 1) @pyqtSignature("") def on_copyToPrevious_clicked(self): self._currentScene.copyFromLinked(self._previousScene) @pyqtSignature("") def on_copyToCurrent_clicked(self): self._previousScene.copyToLinked(self._currentScene) @pyqtSignature("bool") def on_action_Estimate_position_toggled(self, value): parameters.instance.estimate = value # @pyqtSignature("") # def on_action_Undo_triggered(self): # print "Undo" # @pyqtSignature("") # def on_action_Redo_triggered(self): # print "Redo" @pyqtSignature("bool") def on_action_Parameters_toggled(self, value): if value: from .parametersdlg import ParametersDlg self._previousScene.showTemplates() self._currentScene.showTemplates() #tracking_scene.saveParameters() parameters.instance.save() max_size = max(self._currentScene.width(), self._currentScene.height(), self._previousScene.width(), self._previousScene.height(), 400) self.param_dlg = ParametersDlg(max_size, self) self.param_dlg.setModal(False) self.ui.action_Pan.setChecked(True) self.ui.actionAdd_point.setEnabled(False) self.ui.action_Move_point.setEnabled(False) self.ui.actionAdd_cell.setEnabled(False) self.ui.actionRemove_cell.setEnabled(False) self.ui.action_Undo.setEnabled(False) self.ui.action_Redo.setEnabled(False) self.ui.action_Open_project.setEnabled(False) self.ui.actionRecent_projects.setEnabled(False) self.ui.action_Change_data_file.setEnabled(False) self.ui.copyToCurrent.setEnabled(False) self.ui.copyToPrevious.setEnabled(False) self.param_dlg.finished[int].connect(self.closeParam) self.param_dlg.show() elif self.param_dlg: self.param_dlg.accept() def closeParam(self, value): if value == QDialog.Rejected: parameters.instance.load() self.ui.actionAdd_point.setEnabled(True) self.ui.action_Move_point.setEnabled(True) self.ui.actionAdd_cell.setEnabled(True) self.ui.actionRemove_cell.setEnabled(True) self.ui.action_Undo.setEnabled(True) self.ui.action_Redo.setEnabled(True) self.ui.action_Open_project.setEnabled(True) self.ui.actionRecent_projects.setEnabled(True) self.ui.action_Change_data_file.setEnabled(True) self.ui.copyToCurrent.setEnabled(True) self.ui.copyToPrevious.setEnabled(True) self._previousScene.showTemplates(False) self._currentScene.showTemplates(False) self._previousScene.update() self._currentScene.update() self.param_dlg = None self.ui.action_Parameters.setChecked(False) @pyqtSignature("bool") def on_actionZoom_in_toggled(self, value): if value: self._previousScene.mode = TrackingScene.ZoomIn self._currentScene.mode = TrackingScene.ZoomIn @pyqtSignature("bool") def on_actionZoom_out_toggled(self, value): if value: self._previousScene.mode = TrackingScene.ZoomOut self._currentScene.mode = TrackingScene.ZoomOut #def resizeEvent(self, event): # self.ensureZoomFit() def ensureZoomFit(self): if self._data: prev_rect = self._previousScene.sceneRect() cur_rect = self._currentScene.sceneRect() prev_wnd = QRectF(self.ui.previousData.childrenRect()) cur_wnd = QRectF(self.ui.currentData.childrenRect()) prev_matrix = self.ui.previousData.matrix() cur_matrix = self.ui.currentData.matrix() prev_mapped_rect = prev_matrix.mapRect(prev_rect) cur_mapped_rect = cur_matrix.mapRect(cur_rect) if (prev_mapped_rect.width() < prev_wnd.width() or prev_mapped_rect.height() < prev_wnd.height() or cur_mapped_rect.width() < cur_wnd.width() or cur_mapped_rect.height() < cur_wnd.height()): self.on_action_Fit_triggered() @pyqtSignature("") def on_action_Fit_triggered(self): prev_rect = self._previousScene.sceneRect() cur_rect = self._currentScene.sceneRect() prev_wnd = self.ui.previousData.childrenRect() cur_wnd = self.ui.currentData.childrenRect() prev_sw = prev_wnd.width() / prev_rect.width() prev_sh = prev_wnd.height() / prev_rect.height() cur_sw = cur_wnd.width() / cur_rect.width() cur_sh = cur_wnd.height() / cur_rect.height() s = max(prev_sw, prev_sh, cur_sw, cur_sh) self.ui.previousData.resetMatrix() self.ui.previousData.scale(s, s) self.ui.currentData.resetMatrix() self.ui.currentData.scale(s, s) self.changeZoom(s) def zoomOut(self, point=None): self.ui.currentData.scale(0.5, 0.5) self.ui.previousData.scale(0.5, 0.5) self.changeZoom(self.ui.previousData.matrix().m11()) if point is not None: self.ui.previousData.centerOn(point) self.ui.currentData.centerOn(point) #self.ensureZoomFit() def zoomIn(self, point=None): self.ui.currentData.scale(2, 2) self.ui.previousData.scale(2, 2) self.changeZoom(self.ui.previousData.matrix().m11()) if point is not None: self.ui.previousData.centerOn(point) self.ui.currentData.centerOn(point) def changeZoom(self, zoom): self._zoom_label.setText("Zoom: %.5g%%" % (100 * zoom)) @pyqtSignature("") def on_actionZoom_100_triggered(self): self.ui.previousData.resetMatrix() self.ui.currentData.resetMatrix() self.changeZoom(1) @pyqtSignature("bool") def on_actionAdd_point_toggled(self, value): if value: self._previousScene.mode = TrackingScene.Add self._currentScene.mode = TrackingScene.Add @pyqtSignature("bool") def on_actionAdd_cell_toggled(self, value): if value: self._previousScene.mode = TrackingScene.AddCell self._currentScene.mode = TrackingScene.AddCell @pyqtSignature("bool") def on_actionRemove_cell_toggled(self, value): if value: self._previousScene.mode = TrackingScene.RemoveCell self._currentScene.mode = TrackingScene.RemoveCell @pyqtSignature("bool") def on_action_Move_point_toggled(self, value): if value: self._previousScene.mode = TrackingScene.Move self._currentScene.mode = TrackingScene.Move @pyqtSignature("bool") def on_action_Pan_toggled(self, value): if value: self._previousScene.mode = TrackingScene.Pan self._currentScene.mode = TrackingScene.Pan @pyqtSignature("bool") def on_linkViews_toggled(self, value): parameters.instance.link_views = value phor = self.ui.previousData.horizontalScrollBar() pver = self.ui.previousData.verticalScrollBar() chor = self.ui.currentData.horizontalScrollBar() cver = self.ui.currentData.verticalScrollBar() if value: phor.valueChanged[int].connect(chor.setValue) pver.valueChanged[int].connect(cver.setValue) chor.valueChanged[int].connect(phor.setValue) cver.valueChanged[int].connect(pver.setValue) self._previousScene.templatePosChange.connect( self._currentScene.setTemplatePos) self._currentScene.templatePosChange.connect( self._previousScene.setTemplatePos) phor.setValue(chor.value()) pver.setValue(cver.value()) else: phor.valueChanged[int].disconnect(chor.setValue) pver.valueChanged[int].disconnect(cver.setValue) chor.valueChanged[int].disconnect(phor.setValue) cver.valueChanged[int].disconnect(pver.setValue) self._previousScene.templatePosChange.disconnect( self._currentScene.setTemplatePos) self._currentScene.templatePosChange.disconnect( self._previousScene.setTemplatePos) def copyFrom(self, start, items): if parameters.instance.estimate: dlg = createForm('copy_progress.ui', None) dlg.buttonBox.clicked["QAbstractButton*"].connect(self.cancelCopy) params = parameters.instance ts = params.template_size ss = params.search_size fs = params.filter_size self.copy_thread = algo.FindInAll(self._data, start, items, ts, ss, fs, self) dlg.imageProgress.setMaximum(self.copy_thread.num_images) self.copy_thread.start() self.copy_dlg = dlg dlg.exec_() else: algo.copyFromImage(self._data, start, items, self.undo_stack) def cancelCopy(self, *args): self.copy_thread.stop = True dlg = self.copy_dlg dlg.buttonBox.clicked['QAbstractButton*)'].disconnect(self.cancelCopy) self._previousScene.changeImage(None) self._currentScene.changeImage(None) def event(self, event): if isinstance(event, algo.NextImage): dlg = self.copy_dlg if dlg is not None: dlg.imageProgress.setValue(event.currentImage) dlg.pointProgress.setMaximum(event.nbPoints) dlg.pointProgress.setValue(0) return True elif isinstance(event, algo.NextPoint): dlg = self.copy_dlg if dlg is not None: dlg.pointProgress.setValue(event.currentPoint) return True elif isinstance(event, algo.FoundAll): dlg = self.copy_dlg if dlg is not None: self.cancelCopy() dlg.accept() return True elif isinstance(event, algo.Aborted): dlg = self.copy_dlg if dlg is not None: self.cancelCopy() dlg.accept() return True return QMainWindow.event(self, event) def itemsToCopy(self, scene): items = scene.getSelectedIds() if items: answer = QMessageBox.question( self, "Copy of points", "Some points were selected in the previous data window." " Do you want to copy only these point on the successive images?", QMessageBox.Yes, QMessageBox.No) if answer == QMessageBox.Yes: return items return scene.getAllIds() @pyqtSignature("") def on_actionCopy_from_previous_triggered(self): items = self.itemsToCopy(self._previousScene) if items: self.copyFrom(self.ui.previousState.currentIndex(), items) @pyqtSignature("") def on_actionCopy_from_current_triggered(self): items = self.itemsToCopy(self._currentScene) if items: self.copyFrom(self.ui.currentState.currentIndex(), items) @pyqtSignature("") def on_actionSelectPreviousAll_triggered(self): self._previousScene.selectAll() @pyqtSignature("") def on_actionSelectPreviousNew_triggered(self): self._previousScene.selectNew() @pyqtSignature("") def on_actionSelectPreviousNone_triggered(self): self._previousScene.selectNone() @pyqtSignature("") def on_actionSelectPreviousNon_associated_triggered(self): self._previousScene.selectNonAssociated() @pyqtSignature("") def on_actionSelectPreviousAssociated_triggered(self): self._previousScene.selectAssociated() @pyqtSignature("") def on_actionSelectPreviousInvert_triggered(self): self._previousScene.selectInvert() @pyqtSignature("") def on_actionSelectCurrentAll_triggered(self): self._currentScene.selectAll() @pyqtSignature("") def on_actionSelectCurrentNew_triggered(self): self._currentScene.selectNew() @pyqtSignature("") def on_actionSelectCurrentNone_triggered(self): self._currentScene.selectNone() @pyqtSignature("") def on_actionSelectCurrentNon_associated_triggered(self): self._currentScene.selectNonAssociated() @pyqtSignature("") def on_actionSelectCurrentAssociated_triggered(self): self._currentScene.selectAssociated() @pyqtSignature("") def on_actionSelectCurrentInvert_triggered(self): self._currentScene.selectInvert() def whichDelete(self): """ Returns a function deleting what the user wants """ dlg = createForm("deletedlg.ui", None) ret = dlg.exec_() if ret: if dlg.inAllImages.isChecked(): return TrackingScene.deleteInAllImages if dlg.toImage.isChecked(): return TrackingScene.deleteToImage if dlg.fromImage.isChecked(): return TrackingScene.deleteFromImage return lambda x: None @pyqtSignature("") def on_actionDelete_Previous_triggered(self): del_fct = self.whichDelete() del_fct(self._previousScene) @pyqtSignature("") def on_actionDelete_Current_triggered(self): del_fct = self.whichDelete() del_fct(self._currentScene) @pyqtSignature("") def on_actionMerge_points_triggered(self): if self._previousScene.mode == TrackingScene.AddCell: old_cell = self._previousScene.selected_cell new_cell = self._currentScene.selected_cell if old_cell is None or new_cell is None: QMessageBox.critical( self, "Cannot merge cells", "You have to select exactly one cell in the old state " "and one in the new state to merge them.") return try: if old_cell != new_cell: self.undo_stack.push( MergeCells(self._data, self._previousScene.image_name, old_cell, new_cell)) else: self.undo_stack.push( SplitCells(self._data, self._previousScene.image_name, old_cell, new_cell)) except AssertionError as error: QMessageBox.critical(self, "Cannot merge the cells", str(error)) else: old_pts = self._previousScene.getSelectedIds() new_pts = self._currentScene.getSelectedIds() if len(old_pts) != 1 or len(new_pts) != 1: QMessageBox.critical( self, "Cannot merge points", "You have to select exactly one point in the old state " "and one in the new state to link them.") return try: if old_pts != new_pts: self.undo_stack.push( ChangePointsId(self._data, self._previousScene.image_name, old_pts, new_pts)) else: log_debug("Splitting point of id %d" % old_pts[0]) self.undo_stack.push( SplitPointsId(self._data, self._previousScene.image_name, old_pts)) except AssertionError as error: QMessageBox.critical(self, "Cannot merge the points", str(error)) @pyqtSignature("") def on_actionCopy_selection_from_Current_triggered(self): cur_sel = self._currentScene.getSelectedIds() self._previousScene.setSelectedIds(cur_sel) @pyqtSignature("") def on_actionCopy_selection_from_Previous_triggered(self): cur_sel = self._previousScene.getSelectedIds() self._currentScene.setSelectedIds(cur_sel) @pyqtSignature("") def on_actionNew_data_file_triggered(self): if self.ensure_save_data( "Leaving unsaved data", "The last modifications you made were not saved." " Are you sure you want to change the current data file?"): fn = QFileDialog.getSaveFileName( self, "Select a new data file to create", self._project.data_dir, "CSV Files (*.csv);;All files (*.*)") if fn: fn = path(fn) if fn.exists(): button = QMessageBox.question( self, "Erasing existing file", "Are you sure yo want to empty the file '%s' ?" % fn, QMessageBox.Yes, QMessageBox.No) if button == QMessageBox.No: return fn.remove() self._data.clear() self._previousScene.resetNewPoints() self._currentScene.resetNewPoints() self._project.data_file = fn self.initFromData() log_debug("Data file = %s" % (self._project.data_file, )) @pyqtSignature("") def on_actionAbout_triggered(self): dlg = QMessageBox(self) dlg.setWindowTitle("About Point Tracker") dlg.setIconPixmap(self.windowIcon().pixmap(64, 64)) #dlg.setTextFormat(Qt.RichText) dlg.setText("""Point Tracker Tool version %s rev %s Developper: Pierre Barbier de Reuille <*****@*****.**> Copyright 2008 """ % (__version__, __revision__)) img_read = ", ".join( str(s) for s in QImageReader.supportedImageFormats()) img_write = ", ".join( str(s) for s in QImageWriter.supportedImageFormats()) dlg.setDetailedText("""Supported image formats: - For reading: %s - For writing: %s """ % (img_read, img_write)) dlg.exec_() @pyqtSignature("") def on_actionAbout_Qt_triggered(self): QMessageBox.aboutQt(self, "About Qt") @pyqtSignature("") def on_actionReset_alignment_triggered(self): self.undo_stack.push(ResetAlignment(self._data)) @pyqtSignature("") def on_actionAlign_images_triggered(self): fn = QFileDialog.getOpenFileName(self, "Select a data file for alignment", self._project.data_dir, "CSV Files (*.csv);;All files (*.*)") if fn: d = self._data.copy() fn = path(fn) try: d.load(fn) except TrackingDataException as ex: showException(self, "Error while loading data file", ex) return if d._last_pt_id > 0: dlg = AlignmentDlg(d._last_pt_id + 1, self) if dlg.exec_(): ref = dlg.ui.referencePoint.currentText() try: ref = int(ref) except ValueError: ref = str(ref) if dlg.ui.twoPointsRotation.isChecked(): r1 = int(dlg.ui.rotationPt1.currentText()) r2 = int(dlg.ui.rotationPt2.currentText()) rotation = ("TwoPoint", r1, r2) else: rotation = None else: return else: ref = 0 rotation = None try: shifts, angles = algo.alignImages(self._data, d, ref, rotation) self.undo_stack.push(AlignImages(self._data, shifts, angles)) except algo.AlgoException as ex: showException(self, "Error while aligning images", ex) def sceneSizeChanged(self): previous_rect = self._previousScene.real_scene_rect current_rect = self._currentScene.real_scene_rect rect = previous_rect | current_rect self._previousScene.setSceneRect(rect) self._currentScene.setSceneRect(rect) @pyqtSignature("") def on_actionEdit_timing_triggered(self): data = self._data dlg = TimeEditDlg(data.images_name, data.images_time, [data.image_path(n) for n in data.images_name], self) self.current_dlg = dlg if dlg.exec_() == QDialog.Accepted: self.undo_stack.push(ChangeTiming(data, [t for n, t in dlg.model])) del self.current_dlg @pyqtSignature("") def on_actionEdit_scales_triggered(self): data = self._data dlg = EditResDlg(data.images_name, data.images_scale, [data.image_path(n) for n in data.images_name], self) self.current_dlg = dlg if dlg.exec_() == QDialog.Accepted: self.undo_stack.push( ChangeScales(data, [sc for n, sc in dlg.model])) del self.current_dlg @pyqtSignature("") def on_actionCompute_growth_triggered(self): data = self._data dlg = GrowthComputationDlg(data, self) self.current_dlg = dlg dlg.exec_() del self.current_dlg @pyqtSignature("") def on_actionPlot_growth_triggered(self): data = self._data dlg = PlottingDlg(data, self) self.current_dlg = dlg dlg.exec_() del self.current_dlg @pyqtSignature("") def on_actionClean_cells_triggered(self): self.undo_stack.push(CleanCells(self._data)) @pyqtSignature("") def on_actionGotoCell_triggered(self): cells = [str(cid) for cid in self._data.cells] selected, ok = QInputDialog.getItem(self, "Goto cell", "Select the cell to go to", cells, 0) if ok: cid = int(selected) self.ui.actionAdd_cell.setChecked(True) data = self._data if cid not in data.cells: return ls = data.cells_lifespan[cid] prev_pos = self._previousScene.current_data._current_index cur_pos = self._currentScene.current_data._current_index full_poly = data.cells[cid] poly = [pid for pid in full_poly if pid in data[prev_pos]] #log_debug("Cell %d on time %d: %s" % (cid, prev_pos, poly)) if prev_pos < ls.start or prev_pos >= ls.end or not poly: for i in range(*ls.slice().indices(len(data))): poly = [pid for pid in full_poly if pid in data[i]] if poly: log_debug("Found cell %d on image %d with polygon %s" % (cid, i, poly)) new_prev_pos = i break else: log_debug("Cell %d found nowhere in range %s!!!" % (cid, ls.slice())) else: new_prev_pos = prev_pos new_cur_pos = min(max(cur_pos + new_prev_pos - prev_pos, 0), len(data)) self.ui.previousState.setCurrentIndex(new_prev_pos) self.ui.currentState.setCurrentIndex(new_cur_pos) self._previousScene.current_cell = cid self._currentScene.current_cell = cid prev_data = self._previousScene.current_data poly = data.cells[cid] prev_poly = QPolygonF( [prev_data[ptid] for ptid in poly if ptid in prev_data]) prev_bbox = prev_poly.boundingRect() log_debug("Previous bounding box = %dx%d+%d+%d" % (prev_bbox.width(), prev_bbox.height(), prev_bbox.left(), prev_bbox.top())) self.ui.previousData.ensureVisible(prev_bbox) @pyqtSignature("") def on_actionGotoPoint_triggered(self): data = self._data points = [str(pid) for pid in data.cell_points] selected, ok = QInputDialog.getItem(self, "Goto point", "Select the point to go to", points, 0) if ok: pid = int(selected) self.ui.action_Move_point.setChecked(True) if pid not in data.cell_points: return prev_pos = self._previousScene.current_data._current_index cur_pos = self._currentScene.current_data._current_index prev_data = self._previousScene.current_data if not pid in prev_data: closest = -1 best_dist = len(data) + 1 for img_data in data: if pid in img_data: dist = abs(img_data._current_index - prev_pos) if dist < best_dist: best_dist = dist closest = img_data._current_index new_prev_pos = closest else: new_prev_pos = prev_pos new_cur_pos = min(max(cur_pos + new_prev_pos - prev_pos, 0), len(data)) self.ui.previousState.setCurrentIndex(new_prev_pos) self.ui.currentState.setCurrentIndex(new_cur_pos) self._previousScene.setSelectedIds([pid]) self._currentScene.setSelectedIds([pid]) self.ui.previousData.centerOn( self._previousScene.current_data[pid])