def _reportProblem(self): ID, meas, cur = self._getIDmeasCur() self._contact = Contact(self.gui) self._contact.subject.setText('%s\\%s\\%s' % (ID.text(0), meas.text(0), cur.text(0))) self._contact.editor.text.setText( 'E.g. bad image correction, \n remaining vignetting, \n image looks distorted' ) self._contact.show()
class TabCheck(QtWidgets.QSplitter): help = '''Although applied image processing routines automatically detect PV modules and cells, manual verification / modification can be needed in case device corners and grid parameters were detected wrongly or the camera correction causes erroneous results. XXXXXXXXXXXXXXX ''' def __init__(self, gui=None): super().__init__() self.gui = gui self.vertices_list = [] self._lastP = None self._grid = CompareGridEditor() self._grid.gridChanged.connect(self._updateGrid) # self._grid.cornerChanged.connect(self._updateCorner) self._grid.verticesChanged.connect(self._updateVertices) self.btn_markCurrentDone = QtWidgets.QPushButton("Mark verified") self.btn_markCurrentDone.clicked.connect(self._toggleVerified) self._grid.bottomLayout.addWidget(self.btn_markCurrentDone, 0, 0) self.list = QTreeWidget() self.list.setHeaderHidden(True) btnReset = QtWidgets.QPushButton('Reset') btnReset.clicked.connect(self._resetAll) btnSubmit = QtWidgets.QPushButton('Submit') btnSubmit.clicked.connect(self._acceptAll) llist = QtWidgets.QHBoxLayout() llist.addWidget(QtWidgets.QLabel("All:")) llist.addWidget(btnReset) llist.addWidget(btnSubmit) llist.addStretch() l3 = QtWidgets.QVBoxLayout() l3.addLayout(llist) l3.addWidget(self.list) btn_actions = QtWidgets.QPushButton("Actions") menu = QtWidgets.QMenu() menu.addAction("Reset changes").triggered.connect(self._resetChanges) a = menu.addAction("Recalculate all measurements") a.setToolTip( '''Choose this option to run image processing on all submitted images of the selected module again. This is useful, since QELA image processing routines are continuously developed and higher quality results can be available. Additionally, this option will define a new template image (the image other images are perspectively aligned to) depending on the highest resolution/quality image within the image set.''') a.triggered.connect(self._processAllAgain) menu.addAction("Report a problem").triggered.connect( self._reportProblem) menu.addAction("Upload images again").triggered.connect( self._uploadAgain) menu.addAction("Remove measurement").triggered.connect( self._removeMeasurement) btn_actions.setMenu(menu) l3.addWidget(btn_actions) wleft = QtWidgets.QWidget() wleft.setLayout(l3) self.addWidget(wleft) self.addWidget(self._grid) self.list.currentItemChanged.connect(self._loadImg) if self.gui is not None: self._timer = QtCore.QTimer() self._timer.setInterval(3000) self._timer.timeout.connect(self.checkUpdates) self._timer.start() def _processAllAgain(self): # TODO reply = QtWidgets.QMessageBox.question( self, 'TOD:', "This option is not available at the moment, SORRY", QtWidgets.QMessageBox.Yes, QtWidgets.QMessageBox.No) def _uploadAgain(self): items = self.list.getAffectedItems() lines = [] for item in items: data = item.data(1, QtCore.Qt.UserRole) agenda = self.gui.PATH_USER.join('upload', data['timestamp'] + '.csv') lines.extend(agendaFromChanged(agenda, data)) self.gui.tabUpload.dropLines(lines) def _getAffectedPaths(self): items = self.list.getAffectedItems() out = [] for item in items: p = item.parent() pp = p.parent() out.append("%s\\%s\\%s" % (pp.text(0), p.text(0), item.text(0))) return out def _removeMeasurement(self): affected = self._getAffectedPaths() if affected: box = QtWidgets.QMessageBox() box.setStandardButtons(box.Ok | box.Cancel) box.setWindowTitle('Remove measurement') box.setText("Do you want to remove ...\n%s" % "\n".join(affected)) box.exec_() if box.result() == box.Ok: self.gui.server.removeMeasurements(*affected) self.checkUpdates() def _reportProblem(self): ID, meas, cur = self._getIDmeasCur() self._contact = Contact(self.gui) self._contact.subject.setText('%s\\%s\\%s' % (ID.text(0), meas.text(0), cur.text(0))) self._contact.editor.text.setText( 'E.g. bad image correction, \n remaining vignetting, \n image looks distorted' ) self._contact.show() def _resetAll(self): reply = QtWidgets.QMessageBox.question(self, 'Resetting all changes', "Are you sure?", QtWidgets.QMessageBox.Yes, QtWidgets.QMessageBox.No) if reply == QtWidgets.QMessageBox.Yes: self._resetChanges(self.list.invisibleRootItem()) def _resetChangesCurrentItem(self): item = self.list.currentItem() return self._resetChanges(item) def _excludeUnchangableKeys(self, data): if 'vertices' in data: return {k: data[k] for k in ['verified', 'vertices']} return data def _resetChanges(self, item): def _reset(item): data = item.data(1, QtCore.Qt.UserRole) if data is not None: item.setData(0, QtCore.Qt.UserRole, self._excludeUnchangableKeys(data)) f = item.font(0) f.setBold(False) item.setFont(0, f) for i in range(item.childCount()): _reset(item.child(i)) _reset(item) cur = self._getIDmeasCur()[2] data = cur.data(0, QtCore.Qt.UserRole) self._grid.grid.setVertices(data['vertices']) self._changeVerifiedColor(item) def _isIDmodified(self, ID): data0 = ID.data(0, QtCore.Qt.UserRole) data1 = ID.data(1, QtCore.Qt.UserRole) return (data0['nsublines'] != data1['nsublines'] or data0['grid'] != data1['grid']) def _updateVertices(self, vertices): cur = self._getIDmeasCur()[2] data = cur.data(1, QtCore.Qt.UserRole) originalvertices = data['vertices'] data['vertices'] = vertices cur.setData(0, QtCore.Qt.UserRole, data) changed = not np.allclose( vertices, np.array(originalvertices), rtol=0.01) f = cur.font(0) f.setBold(changed) cur.setFont(0, f) def _updateGrid(self, key, val): ID = self._getIDmeasCur()[0] data = ID.data(0, QtCore.Qt.UserRole) data[key] = val ID.setData(0, QtCore.Qt.UserRole, data) f = ID.font(0) changed = self._isIDmodified(ID) f.setBold(changed) ID.setFont(0, f) def _getIDmeasCur(self): ''' returns current items for ID, measurement and current ''' item = self.list.currentItem() p = item.parent() if p is not None: pp = p.parent() if pp is not None: ID, meas, cur = pp, p, item else: ID, meas, cur = p, item, item.child(0) else: ID, meas, cur = (item, item.child(0), item.child(0).child(0)) return ID, meas, cur def _loadImg(self): try: ID, meas, cur = self._getIDmeasCur() txt = ID.text(0), meas.text(0), cur.text(0) root = self.gui.projectFolder() p = root.join(*txt) if p == self._lastP: return self._lastP = p p0 = p.join("prev_A.jpg") p1 = p.join("prev_B.jpg") ll = len(root) + 1 if not p0.exists(): self.gui.server.download(p0[ll:], root) if not p1.exists(): self.gui.server.download(p1[ll:], root) img = imread(p0) self._grid.imageview.setImage(img, autoRange=False) img = imread(p1) self._grid.imageview2.setImage(img, autoRange=False) # load/change grid cells = ID.data(0, QtCore.Qt.UserRole)['grid'] nsublines = ID.data(0, QtCore.Qt.UserRole)['nsublines'] cdata = cur.data(0, QtCore.Qt.UserRole) # print(1111123, self._grid.grid.vertices()) # print(cdata['vertices']) vertices = cdata['vertices'] # TODO: remove different conventions # cells = cells[::-1] # nsublines = nsublines[::-1] vertices = np.array(vertices)[np.array([0, 3, 2, 1])] # print(vertices, 9898) self._grid.grid.setNCells(cells) self._grid.grid.setVertices(vertices) # print(self._grid.grid.vertices(), 888888888888888888) self._grid.edX.setValue(cells[0]) self._grid.edY.setValue(cells[1]) self._grid.edBBX.setValue(nsublines[0]) self._grid.edBBY.setValue(nsublines[1]) self._updateBtnVerified(cdata['verified']) except AttributeError as e: print(e) def toggleShowTab(self, show): t = self.gui.tabs t.setTabEnabled(t.indexOf(self), show) def buildTree(self, tree): show = bool(tree) self.toggleShowTab(show) if show: root = self.list.invisibleRootItem() last = [root, None, None] def _addParam(name, params, nindents): if nindents: parent = last[nindents - 1] else: parent = root item = self.list.findChildItem(parent, name) # if params: # params = json.loads(params) if item is None: item = QtWidgets.QTreeWidgetItem(parent, [name]) if params: if nindents == 2: # modifiable: item.setData(0, QtCore.Qt.UserRole, self._excludeUnchangableKeys(params)) else: # nindents==1 -> grid item.setData(0, QtCore.Qt.UserRole, params) self._changeVerifiedColor(item) last[nindents] = item if params: params = params # original: item.setData(1, QtCore.Qt.UserRole, params) # add new items / update existing: treenames = [] for ID, param, meas in tree: _addParam(ID, param, 0) treenames.append([ID]) for m, currents in meas: _addParam(m, None, 1) treenames.append([ID, m]) for c, param in currents: _addParam(c, param, 2) treenames.append([ID, m, c]) # remove items that are in client but not in server tree: for item, texts in self.list.recursiveItemsText(): if texts not in treenames: p = item.parent() if not p: p = root p.takeChild(p.indexOfChild(item)) root.sortChildren(0, QtCore.Qt.AscendingOrder) self.list.resizeColumnToContents(0) self.list.setCurrentItem(self.list.itemAt(0, 0)) def modules(self): ''' return generator for all module names in .list ''' item = self.list.invisibleRootItem() for i in range(item.childCount()): yield item.child(i).text(0) def checkUpdates(self): if self.gui.server.isReady() and self.gui.server.hasNewCheckTree(): self.buildTree(self.gui.server.checkTree()) def _toggleVerified(self): item = self._getIDmeasCur()[2] data = item.data(0, QtCore.Qt.UserRole) v = data['verified'] = not data['verified'] item.setData(0, QtCore.Qt.UserRole, data) self._updateBtnVerified(v) self._changeVerifiedColor(item) def _updateBtnVerified(self, verified): if verified: self.btn_markCurrentDone.setText('Mark unverified') else: self.btn_markCurrentDone.setText('Mark verified ') def _changeVerifiedColor(self, item): data = item.data(0, QtCore.Qt.UserRole) if data is None or 'verified' not in data: return if data['verified']: color = QtCore.Qt.darkGreen else: color = QtCore.Qt.black item.setForeground(0, color) # apply upwards, if there is only one item in list while True: parent = item.parent() if not parent: break if parent.childCount() == 1: item = parent item.setForeground(0, color) def _acceptAll(self): reply = QtWidgets.QMessageBox.question(self, 'Submitting changes', "Are you sure?", QtWidgets.QMessageBox.Yes, QtWidgets.QMessageBox.No) if reply == QtWidgets.QMessageBox.Yes: self._doSubmitAllChanges() def _doSubmitAllChanges(self): out = {'grid': {}, 'unchanged': {}, 'changed': {}} for item, nindent in self.list.buildCheckTree(): data = item.data(0, QtCore.Qt.UserRole) if nindent == 1: # only grid and curr interesting continue path = '\\'.join(self.list.itemInheranceText(item)) changed = item.font(0).bold() print(path, data, nindent) if nindent == 2: if changed: # item is modified out['changed'][path] = data else: out['unchanged'][path] = data['verified'] elif changed: out['grid'][path] = data # ['verified'] self.gui.server.submitChanges(json.dumps(out) + '<EOF>') def config(self): return {} # 'name': self.camOpts.currentText()} def restore(self, c): pass
class MainWindow(QtWidgets.QMainWindow): PATH = client.PATH sigMoved = QtCore.pyqtSignal(QtCore.QPoint) def __init__(self, server, user, pwd): super().__init__() self.user = user self.pwd = pwd self.server = server self.PATH_USER = self.PATH.mkdir(user) # TODO: read last root from config self.root = self.PATH_USER.mkdir("local") self.updateProjectFolder() self._lastW = None self._about, self._api, self._contact, \ self.help, self._pricing, self._security = None, None, None, None, None, None self.setWindowIcon(QtGui.QIcon(client.ICON)) self.updateWindowTitle() self.resize(1100, 600) ll = QtWidgets.QHBoxLayout() w = QtWidgets.QWidget() w.setLayout(ll) self.setCentralWidget(w) self.progressbar = CancelProgressBar() self.progressbar.hide() self.setStatusBar(StatusBar()) self.statusBar().addPermanentWidget(self.progressbar, stretch=1) self.server.sigError.connect(self.statusBar().showError) # self.btnMenu = QtWidgets.QPushButton('Menu') self.btnMenu = QtWidgets.QPushButton() self.btnMenu.setIcon( QtGui.QIcon(client.MEDIA_PATH.join('btn_menu.svg'))) # make button smaller: self.btnMenu.setIconSize(QtCore.QSize(20, 20)) # self.btnMenu.sizeHint = lambda: QtCore.QSize(40, 20) self.btnMenu.setFlat(True) self._menu = QtWidgets.QMenu() a = self._menu.addAction('About QELA') a.setToolTip(About.__doc__) a.triggered.connect(self._menuShowAbout) a = self._menu.addAction('Help') a.setToolTip(Help.__doc__) a.triggered.connect(self._menuShowHelp) a = self._menu.addAction('Change current project') a.setToolTip(Projects.__doc__) a.triggered.connect(self._menuShowProjects) a = self._menu.addAction('Pricing') a.setToolTip(Pricing.__doc__) a.triggered.connect(self._menuShowPlan) a = self._menu.addAction('Website') a.setToolTip('Open the application website in your browser') a.triggered.connect(self._menuShowWebsite) a = self._menu.addAction('Contact') a.setToolTip(Contact.__doc__) a.triggered.connect(self.menuShowContact) self.btnMenu.clicked.connect(self._menuPopup) self.btnMenu.setSizePolicy(QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Minimum) self.tabs = QtWidgets.QTabWidget() self.tabs.setCornerWidget(self.btnMenu) self.help = Help(self) self.help.hide() self.tabUpload = TabUpload(self) self.tabDownload = TabDownload(self) self.tabCheck = TabCheck(self) self.config = TabConfig(self) self.loadConfig() self.tabs.addTab(self.config, "Configuration") self.tabs.addTab(self.tabUpload, "Upload") self.tabs.addTab(self.tabCheck, "Check") self.tabs.addTab(self.tabDownload, "Download") self.tabCheck.checkUpdates() self.tabs.currentChanged.connect(self._activateTab) self.tabs.currentChanged.connect(self.help.tabChanged) self.tabs.setCurrentIndex(1) ll.setContentsMargins(0, 0, 0, 0) ll.addWidget(self.tabs) ll.addWidget(self.help) self._tempBars = [] def updateProjectFolder(self): self._proj = self.server.projectCode() self.root.mkdir(self._proj) def projectFolder(self): return self.root.join(self._proj) def _menuShowProjects(self): self._projects = Projects(self) self._projects.show() def loadConfig(self): try: c = self.server.lastConfig() self.config.restore(c) except json.decoder.JSONDecodeError: print('ERROR loading last config: %s' % c) def updateWindowTitle(self, project=None): if project is None: project = self.server.projectName() self.setWindowTitle( 'QELA | User: %s | Project: %s | Credit: %s' % (self.user, project, self.server.remainingCredit())) def _menuShowAbout(self): if self._about is None: self._about = About(self) self._about.show() def _menuShowHelp(self): if self.help.isVisible(): self.help.hide() else: self.help.show() def _menuShowWebsite(self): os.startfile('http://%s' % self.server.address[0]) def _menuShowPlan(self): if self._pricing is None: self._pricing = Pricing(self) self._pricing.show() def menuShowContact(self): if self._contact is None: self._contact = Contact(self) self._contact.show() def modules(self): '''return a list of all modules either found in imageuploadtable (from client) or tabCheck (from server)''' ll = list(self.tabCheck.modules()) ll.extend(self.tabUpload.table.modules()) return ll def openImage(self, path): txt = self.config.analysis.cbViewer.currentText() if txt == 'dataArtist': dataArtist.importFile(path) elif txt == 'Inline': self._tempview = InlineView(path) self._tempview.show() else: os.startfile(path) def _menuPopup(self): g = self.btnMenu.geometry() p = g.bottomLeft() p.setX(p.x() - (self._menu.sizeHint().width() - g.width())) self._menu.popup(self.mapToGlobal(p)) # def _toggleShowHelp(self, checked): # if not checked: # self.help.hide() # self.tabs.setCornerWidget(self.btnMenu) # self.btnMenu.show() # # else: # self.tabs.setCornerWidget(None) # s = self.btnMenu.size() # self.help.ltop.addWidget(self.btnMenu, stretch=0) # # self.help.show() # self.btnMenu.setFixedSize(s) # self.btnMenu.show() # def eventFilter(self, _obj, event): # if self.help.isVisible(): # # if event.type() == QtCore.QEvent.WindowActivate: # # print("widget window has gained focus") # # elif event.type() == QtCore.QEvent.WindowDeactivate: # # print("widget window has lost focus") # if event.type() == QtCore.QEvent.FocusIn: # print("widget window has lost focus") # return False # event.accept() def removeTemporaryProcessBar(self, bar): self._tempBars.remove(bar) self.statusBar().removeWidget(bar) self.progressbar.show() def addTemporaryProcessBar(self): c = CancelProgressBar() self._tempBars.append(c) self.progressbar.hide() # self.statusBar().removePermanentWidget self.statusBar().addPermanentWidget(c, stretch=1) return c def moveEvent(self, evt): self.sigMoved.emit(evt.pos()) # return QtWidgets.QMainWindow.moveEvent(self, *args, **kwargs) def closeEvent(self, ev): if not self.server.isReady(): msg = QtWidgets.QMessageBox() msg.setText("You are still uploading/downloading data") msg.setStandardButtons(QtWidgets.QMessageBox.Ok | QtWidgets.QMessageBox.Cancel) msg.exec_() if msg.result() == QtWidgets.QMessageBox.Ok: ev.accept() else: ev.ignore() return self.server.logout() # self.server.close() # sleep(1) QtWidgets.QApplication.instance().quit() def setTabsEnabled(self, enable=True, exclude=None): for i in range(self.tabs.count() - 1): if i != exclude: self.tabs.setTabEnabled(i, enable) if enable: self.tabs.setCurrentIndex(2) def _activateTab(self, index): w = self.tabs.widget(index) if self._lastW and hasattr(self._lastW, 'deactivate'): self._lastW.deactivate() if hasattr(w, 'activate'): w.activate() self._lastW = w
def menuShowContact(self): if self._contact is None: self._contact = Contact(self) self._contact.show()
class TabCheck(QtWidgets.QSplitter): '''Nobody is perfect. Although our image processing routines are fully automated, they can sometimes fail to precisely detect a solar module. Especially for uncommon module types or low quality images your help for verify or alter our results can be needed. This tab displays results from camera and perspective correction of all EL images in your current project. In here, images are highly compressed to reduce download times. As soon as new results are available, this tab will be highlighted. Please take your time to go through the results. You can verify of change: * Position of the four module corners. * Position of the bottom left corner * Number of horizontal/vertical cells and busbars. After clicking on <Submit changes> all images modified by you will be processed again. Manual verification increases quality of the generated module report. Please inform us, if you find odd or erroneous results. For this click on Actions -> Report a problem ''' def __init__(self, gui=None): super().__init__() self.gui = gui self.vertices_list = [] self._lastP = None self._grid = CompareGridEditor() self._grid.gridChanged.connect(self._updateGrid) # self._grid.cornerChanged.connect(self._updateCorner) self._grid.verticesChanged.connect(self._updateVertices) self.btn_markCurrentDone = QtWidgets.QPushButton("Mark verified") self.btn_markCurrentDone.setToolTip( '''Confirm that detected module position and type are correct. As soon as all modules are verified, click on <Submit> to inform our server.''' ) self.btn_markCurrentDone.clicked.connect(self._toggleVerified) self._grid.bottomLayout.addWidget(self.btn_markCurrentDone, 0, 0) self.list = QTreeWidget() self.list.setHeaderHidden(True) self.list.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) self.list.customContextMenuRequested.connect( lambda pos: self._menu.popup(pos)) btnSubmit = QtWidgets.QPushButton('Submit') btnSubmit.setToolTip('''Submit all changes to the server. Correct results will be marked as 'verified' in the module report and modified result will be processed again.''') btnSubmit.clicked.connect(self._acceptAll) llist = QtWidgets.QHBoxLayout() llist.addStretch() llist.addWidget(btnSubmit) l3 = QtWidgets.QVBoxLayout() l3.addLayout(llist) l3.addWidget(self.list) btn_actions = QtWidgets.QPushButton("Actions") self._menu = menu = QMenu() menu.aboutToShow.connect(self._showMenu) m = menu.addMenu('All') a = m.addAction("Reset changes") a.setToolTip('Reset everything to the state given by the server.') a.triggered.connect(self._resetAll) a = m.addAction("Recalculate all measurements") a.setToolTip( '''Choose this option to run image processing on all submitted images of the selected module again. This is useful, since QELA image processing routines are continuously developed and higher quality results can be available. Additionally, this option will define a new template image (the image other images are perspectively aligned to) depending on the highest resolution/quality image within the image set.''') a.triggered.connect(self._processAllAgain) a = menu.addAction("Reset changes") a.setToolTip('Reset everything to the state given by the server.') a.triggered.connect(self._resetChanges) a = menu.addAction("Report a problem") a.setToolTip( 'Write us a mail and inform us on the problem you experience.') a.triggered.connect(self._reportProblem) a = menu.addAction("Upload images again") a.setToolTip( 'Click this button to upload and process the images of the selected measurement again.' ) a.triggered.connect(self._uploadAgain) self._aRemove = a = menu.addAction("Remove measurement") a.setToolTip( 'Remove the current measurement/device from the project. This includes all corresponding data. Pleas write us a mail, to undo this step.' ) a.triggered.connect(self._removeMeasurement) btn_actions.setMenu(menu) l3.addWidget(btn_actions) wleft = QtWidgets.QWidget() wleft.setLayout(l3) self.btnCollapse = QtWidgets.QPushButton(wleft) self.btnCollapse.setIcon( QtGui.QIcon(client.MEDIA_PATH.join('btn_toggle_collapse.svg'))) self.btnCollapse.toggled.connect(self._toggleExpandAll) self.btnCollapse.setCheckable(True) self.btnCollapse.setFlat(True) self.btnCollapse.resize(15, 15) self.btnCollapse.move(7, 15) header = QtWidgets.QLabel('ID > Meas > Current', wleft) header.move(25, 7) self.addWidget(wleft) self.addWidget(self._grid) self.list.currentItemChanged.connect(self._loadImg) if self.gui is not None: self._timer = QtCore.QTimer() self._timer.setInterval(5000) self._timer.timeout.connect(self.checkUpdates) self._timer.start() def saveState(self): try: ID, meas, cur, typ = self._getIDmeasCur() except AttributeError: # no items ll = [] else: if typ == 'device': ll = [ID.text(0)] elif typ == 'measurement': ll = [ID.text(0), meas.text(0)] else: ll = [ID.text(0), meas.text(0), cur.text(0)] return {'expanded': self.btnCollapse.isChecked(), 'selected': ll} def restoreState(self, state): self.btnCollapse.setChecked(state['expanded']) self._selectFromName(state['selected']) def _selectFromName(self, ll): if ll: root = self.list.invisibleRootItem() def fn(name, parent, ll ): # try to select item by listed name ll=[ID,meas,current] for i in range(parent.childCount()): child = parent.child(i) if child.text(0) == name: if ll: return fn(ll.pop(0), child, ll) else: self.list.setCurrentItem(child) fn(ll.pop(0), root, ll) def _toggleExpandAll(self, checked): if checked: self.list.expandAll() else: self.list.collapseAll() def _showMenu(self): self._aRemove.setText('Remove %s' % self._getIDmeasCur()[-1]) def _processAllAgain(self): # TODO reply = QtWidgets.QMessageBox.question( self, 'TOD:', "This option is not available at the moment, SORRY", QtWidgets.QMessageBox.Yes, QtWidgets.QMessageBox.No) def _uploadAgain(self): items = self.list.getAffectedItems() lines = [] for item in items: data = item.data(1, QtCore.Qt.UserRole) agenda = self.gui.PATH_USER.join('upload', data['timestamp'] + '.csv') lines.extend(agendaFromChanged(agenda, data)) self.gui.tabUpload.dropLines(lines) def _getAffectedPaths(self): items = self.list.getAffectedItems() out = [] for item in items: p = item.parent() pp = p.parent() out.append("%s\\%s\\%s" % (pp.text(0), p.text(0), item.text(0))) return out def _removeMeasurement(self): affected = self._getAffectedPaths() if affected: box = QtWidgets.QMessageBox() box.setStandardButtons(box.Ok | box.Cancel) box.setWindowTitle('Remove measurement') box.setText("Do you want to remove ...\n%s" % "\n".join(affected)) box.exec_() if box.result() == box.Ok: res = self.gui.server.removeMeasurements(*affected) if res != 'OK': QtWidgets.QMessageBox.critical( self, 'Error removing measurements', res) else: item = self.list.currentItem() parent = item.parent() if parent is None: parent = self.list.invisibleRootItem() parent.removeChild(item) # self.checkUpdates() def _reportProblem(self): ID, meas, cur = self._getIDmeasCur()[:-1] self._contact = Contact(self.gui) self._contact.subject.setText('%s\\%s\\%s' % (ID.text(0), meas.text(0), cur.text(0))) self._contact.editor.text.setText( 'E.g. bad image correction, \n remaining vignetting, \n image looks distorted' ) self._contact.show() def _resetAll(self): reply = QtWidgets.QMessageBox.question(self, 'Resetting all changes', "Are you sure?", QtWidgets.QMessageBox.Yes, QtWidgets.QMessageBox.No) if reply == QtWidgets.QMessageBox.Yes: self._resetChanges(self.list.invisibleRootItem()) def _resetChangesCurrentItem(self): item = self.list.currentItem() return self._resetChanges(item) def _excludeUnchangableKeys(self, data): if 'vertices' in data: return {k: data[k] for k in ['verified', 'vertices']} return data def _resetChanges(self, item): if type(item) is bool: item = self.list.currentItem() def _reset(item): data = item.data(1, QtCore.Qt.UserRole) if data is not None: item.setData(0, QtCore.Qt.UserRole, self._excludeUnchangableKeys(data)) f = item.font(0) f.setBold(False) item.setFont(0, f) for i in range(item.childCount()): _reset(item.child(i)) _reset(item) cur = self._getIDmeasCur()[2] data = cur.data(0, QtCore.Qt.UserRole) self._grid.grid.setVertices(data['vertices']) self._changeVerifiedColor(item) def _isIDmodified(self, ID): data0 = ID.data(0, QtCore.Qt.UserRole) data1 = ID.data(1, QtCore.Qt.UserRole) return (data0['nsublines'] != data1['nsublines'] or data0['grid'] != data1['grid']) def _updateVertices(self, vertices): cur = self._getIDmeasCur()[2] data = cur.data(1, QtCore.Qt.UserRole) originalvertices = data['vertices'] data['vertices'] = vertices cur.setData(0, QtCore.Qt.UserRole, data) changed = not np.allclose( vertices, np.array(originalvertices), rtol=0.01) f = cur.font(0) f.setBold(changed) cur.setFont(0, f) def _updateGrid(self, key, val): ID = self._getIDmeasCur()[0] # update data: data = ID.data(0, QtCore.Qt.UserRole) data[key] = val ID.setData(0, QtCore.Qt.UserRole, data) # font -> bold f = ID.font(0) changed = self._isIDmodified(ID) f.setBold(changed) ID.setFont(0, f) # # grid is identical for all current and measurements of one device, so # # update other items: # for i in range(ID.childCount()): # meas = ID.child(i) # for j in range(meas.childCount()): # current = meas.child(j) def _getIDmeasCur(self): ''' returns ID, meas, cur, typ of current item index(0...id,1...meas,2...current) ''' item = self.list.currentItem() p = item.parent() if p is not None: pp = p.parent() if pp is not None: # item is current ID, meas, cur = pp, p, item index = 'current' else: # item is measurement ID, meas, cur = p, item, item.child(0) index = 'measurement' else: # item is ID ID, meas, cur = (item, item.child(0), item.child(0).child(0)) index = 'device' return ID, meas, cur, index def _loadImg(self): if not self.list.currentItem(): return try: ID, meas, cur = self._getIDmeasCur()[:-1] txt = ID.text(0), meas.text(0), cur.text(0) root = self.gui.projectFolder() p = root.join(*txt) if p == self._lastP: return self._lastP = p p0 = p.join(".prev_A.jpg") p1 = p.join(".prev_B.jpg") ll = len(root) + 1 if not p0.exists(): self.gui.server.download(p0[ll:], root.join(p0[ll:])) if not p1.exists(): self.gui.server.download(p1[ll:], root.join(p1[ll:])) self._grid.readImg1(p0) self._grid.readImg2(p1) # load/change grid idata = ID.data(0, QtCore.Qt.UserRole) cells = idata['grid'][::-1] nsublines = idata['nsublines'] cdata = cur.data(0, QtCore.Qt.UserRole) vertices = cdata['vertices'] # TODO: remove different conventions # vertices = np.array(vertices)[np.array([0, 3, 2, 1])] self._grid.grid.setNCells(cells) self._grid.grid.setVertices(vertices) self._grid.edX.setValue(cells[0]) self._grid.edY.setValue(cells[1]) self._grid.edBBX.setValue(nsublines[1]) self._grid.edBBY.setValue(nsublines[0]) self._updateBtnVerified(cdata['verified']) except AttributeError as e: print('error loading image: ', e) def toggleShowTab(self, show): t = self.gui.tabs t.setTabEnabled(t.indexOf(self), show) def buildTree(self, tree): show = bool(tree) if show: self.list.show() citem = self.list.currentItem() root = self.list.invisibleRootItem() last = [root, None, None] def _addParam(name, params, nindents): if nindents: parent = last[nindents - 1] else: parent = root item = self.list.findChildItem(parent, name) if item is None: item = QtWidgets.QTreeWidgetItem(parent, [name]) if params: if nindents == 2: # modifiable: item.setData(0, QtCore.Qt.UserRole, self._excludeUnchangableKeys(params)) else: # nindents==1 -> grid item.setData(0, QtCore.Qt.UserRole, params) self._changeVerifiedColor(item) last[nindents] = item if params: params = params # original: item.setData(1, QtCore.Qt.UserRole, params) # add new items / update existing: IDdict = {} for ID, data, meas in tree: _addParam(ID, data, 0) measdict = {} IDdict[ID] = measdict # treenames.append([ID]) for m, currents in meas: _addParam(m, None, 1) curlist = [] measdict[m] = curlist for current, data in currents: _addParam(current, data, 2) curlist.append(current) # remove old ones: def iterremove(parent, dic): c = parent.childCount() i = 0 while i < c: child = parent.child(i) txt = child.text(0) if isinstance(dic, dict): iterremove(child, dic[txt]) if not child.childCount(): # ... or empty parent items parent.removeChild(child) c -= 1 i -= 1 elif txt not in dic: # only remove 'current' items parent.removeChild(child) c -= 1 i -= 1 i += 1 iterremove(root, IDdict) root.sortChildren(0, QtCore.Qt.AscendingOrder) self.list.resizeColumnToContents(0) if citem is None or citem.parent() is None: self.list.setCurrentItem(self.list.itemAt(0, 0)) self.toggleShowTab(show) def modules(self): ''' return generator for all module names in .list ''' item = self.list.invisibleRootItem() for i in range(item.childCount()): yield item.child(i).text(0) def checkUpdates(self): if self.gui.server.isReady() and self.gui.server.hasNewCheckTree(): self.buildTree(self.gui.server.checkTree()) def _toggleVerified(self): item = self._getIDmeasCur()[2] data = item.data(0, QtCore.Qt.UserRole) v = data['verified'] = not data['verified'] item.setData(0, QtCore.Qt.UserRole, data) self._updateBtnVerified(v) self._changeVerifiedColor(item) def _updateBtnVerified(self, verified): if verified: self.btn_markCurrentDone.setText('Mark unverified') else: self.btn_markCurrentDone.setText('Mark verified ') def _changeVerifiedColor(self, item): data = item.data(0, QtCore.Qt.UserRole) if data is None or 'verified' not in data: return if data['verified']: color = QtCore.Qt.darkGreen else: color = QtCore.Qt.black item.setForeground(0, color) # apply upwards, if there is only one item in list while True: parent = item.parent() if not parent: break if parent.childCount() == 1: item = parent item.setForeground(0, color) else: break def _acceptAll(self): reply = QtWidgets.QMessageBox.question(self, 'Submitting changes', "Are you sure?", QtWidgets.QMessageBox.Yes, QtWidgets.QMessageBox.No) if reply == QtWidgets.QMessageBox.Yes: self._doSubmitAllChanges() def _doSubmitAllChanges(self): out = {'grid': {}, 'unchanged': {}, 'changed': {}} for item, nindent in self.list.buildCheckTree(): data = item.data(0, QtCore.Qt.UserRole) if nindent == 1: # only grid and curr interesting continue path = '\\'.join(self.list.itemInheranceText(item)) changed = item.font(0).bold() print(path, data, nindent) if nindent == 2: if changed: # item is modified out['changed'][path] = data else: out['unchanged'][path] = data['verified'] elif changed: out['grid'][path] = data # ['verified'] self.gui.server.submitChanges(json.dumps(out) + '<EOF>')
class MainWindow(QtWidgets.QMainWindow): PATH = client.PATH sigMoved = QtCore.pyqtSignal(QtCore.QPoint) sigResized = QtCore.pyqtSignal(QtCore.QSize) def __init__(self, login, server, user, pwd): super(). __init__() self.user = user self.pwd = pwd self.login = login self.server = server FIRST_START = not self.PATH.join(user).exists() self.PATH_USER = self.PATH.mkdir(user) # TODO: read last root from config self.root = self.PATH_USER.mkdir("local") self.updateProjectFolder() self._downloadQueue = [] self._downloadThread = None self._tempview = None self._lastW = None self._startTourExample = False self._about, self._api, self._contact, \ self.help, self._pricing, self._security = None, None, None, None, None, None self.setWindowIcon(QtGui.QIcon(client.ICON)) self.updateWindowTitle() # QtCore.QLocale.setDefault() self.resize(1100, 600) ll = QtWidgets.QHBoxLayout() w = QtWidgets.QWidget() w.setLayout(ll) self.setCentralWidget(w) self.progressbar = CancelProgressBar() self.progressbar.hide() self.setStatusBar(StatusBar()) self.statusBar().addPermanentWidget(self.progressbar, stretch=1) self.server.sigError.connect(self.statusBar().showError) self.btnMenu = QtWidgets.QPushButton() self.btnMenu.setIcon(QtGui.QIcon( client.MEDIA_PATH.join('btn_menu.svg'))) # make button smaller: self.btnMenu.setIconSize(QtCore.QSize(20, 20)) self.btnMenu.setFlat(True) self._menu = QMenu() a = self._menu.addAction('About QELA') a.setToolTip(About.__doc__) a.triggered.connect(self._menuShowAbout) a = self._menu.addAction('Help') f = a.font() f.setBold(True) a.setFont(f) a.setToolTip(Help.__doc__) a.triggered.connect(self._menuShowHelp) a = self._menu.addAction('Change current project') a.setToolTip(Projects.__doc__) a.triggered.connect(self._menuShowProjects) a = self._menu.addAction('Download example images') a.triggered.connect(self._downloadExampleImages) a = self._menu.addAction('Pricing') a.setToolTip(Pricing.__doc__) a.triggered.connect(self._menuShowPlan) a = self._menu.addAction('Website') a.setToolTip( 'Open the application website in your browser') a.triggered.connect(self._menuShowWebsite) a = self._menu.addAction('Contact us') a.setToolTip(Contact.__doc__) a.triggered.connect(self.menuShowContact) self._menu.addSeparator() a = self._menu.addAction('Account') a.triggered.connect(self.account) a = self._menu.addAction('Change User') a.triggered.connect(self.changeUser) self.btnMenu.clicked.connect(self._menuPopup) self.btnMenu.setSizePolicy( QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Minimum) self.tabs = QtWidgets.QTabWidget() self.tabs.setCornerWidget(self.btnMenu) self.help = Help(self) self.help.hide() self.tabUpload = TabUpload(self) self.tabDownload = TabDownload(self) self.tabCheck = TabCheck(self) self.tabConfig = TabConfig(self) # TODO: rename to tabconfig # self.loadConfig() self.tabs.addTab(self.tabConfig, "Configuration") self.tabs.addTab(self.tabUpload, "Upload") self.tabs.addTab(self.tabCheck, "Check") self.tabs.addTab(self.tabDownload, "Download") self.tabs.currentChanged.connect(self._activateTab) self.tabs.currentChanged.connect(self.help.tabChanged) self.tabs.setCurrentIndex(1) # show TabUpload at start ll.setContentsMargins(0, 0, 0, 0) ll.addWidget(self.tabs) ll.addWidget(self.help) self._tempBars = [] self.tabCheck.checkUpdates() # FIRST_START = True self.restoreLastSession() self.show() if FIRST_START: B = QtWidgets.QMessageBox b = B(B.Information, 'Starting QELA for the first time...', '''Hi %s! It looks like this is your first time using QELA on this PC. Would you like to take a quick tour?''' % self.user, B.Yes | B.No) b.setDefaultButton(B.Yes) b.setWindowIcon(self.windowIcon()) if b.exec_() == B.Yes: self._tourInit() def saveSession(self): s = self.PATH_USER.join('session.json') c = {'config':self.tabConfig.saveState(), 'upload':self.tabUpload.saveState(), 'download':self.tabDownload.saveState(), 'check':self.tabCheck.saveState(), } with open(s, 'w') as f: f.write(json.dumps(c, indent=4)) def restoreConfigFromServer(self): self.tabConfig.restoreState(self.server.lastConfig()) def restoreLastSession(self): try: s = self.PATH_USER.join('session.json') c = None if s.exists(): try: with open(s, 'r') as f: c = json.loads(f.read()) except json.decoder.JSONDecodeError: print('ERROR loading last session: %s' % c) if c is None: self.restoreConfigFromServer() else: self.tabConfig.restoreState(c['config']) self.tabDownload.restoreState(c['download']) self.tabUpload.restoreState(c['upload']) self.tabCheck.restoreState(c['check']) except Exception: print('Could not restore last session') def _downloadDoneExampleImages(self, path): if not hasattr(self, '_tourSa'): self._tourExample_init(path) else: # other tour is still running - start after that self._startTourExample = path def _tourExample_init(self, path): # TODO: temporarily disable all other tabs self._startTourExample = False dirname = path[:-4] # remove zip with ZipFile(path) as myzip: myzip.extractall(dirname) QtGui.QDesktopServices.openUrl( QtCore.QUrl.fromLocalFile(dirname)) self.tabs.setCurrentWidget(self.tabUpload) self._tourEx = Tour(self, 1, self._tourExNext, 'Correct example images', self._tourExCleanUp) self._tourEx.show() self._tourEx.next(0) def _tourExCleanUp(self): if hasattr(self, '_dragWBtn_style'): # reset self.tabUpload.dragW.btn.setStyleSheet(self._dragWBtn_style) del self._dragWBtn_style def _tourExNext(self, tour): self._tourExCleanUp() if tour.index == -1: tour.lab.setText('''\ 1. Select all folders in the open directory 2. Drag and drop them into QELA 3. Click on Button <Blocks>''') elif tour.index == 0: tour.lab.setText('''\ 1. Click on Button <Blocks> 2. Click on Items <Measurement>, <Module ID> and <Current> 3. Move <Measurement>, <Module ID> and <Current> to position 3,4 and 5 respectively''') self._dragWBtn_style = self.tabUpload.dragW.btn.styleSheet() self.tabUpload.dragW.btn.setStyleSheet('QPushButton { %s }' % tour.style) elif tour.index == 1: tour.lab.setText('''\ 1. Ensure all field in the table are filled with meaningful data. 2. Go to tab 'Configuration' and make sure that 'exampleCalibration' is chosen as camera 3. Click un button upload to upload all images and start image processing.''') def _downloadExampleImages(self): d = QtWidgets.QFileDialog.getExistingDirectory(directory=self.root) if d: self.addDownload('exampleImages.zip', PathStr(d), fnsDone=self._downloadDoneExampleImages, cmd='exampleImages.zip') def _tourInit(self): B = QtWidgets.QMessageBox b = B(B.Information, 'Download example images', '''You can start right away using our example images. Would you like to download them now? You can also download them at any other time (Menu->Download example images)''', B.Yes | B.No) b.setDefaultButton(B.Yes) b.setWindowIcon(self.windowIcon()) if b.exec_() == B.Yes: self._downloadExampleImages() self._tourSa = Tour(self, 4, self._tourNext, 'First Steps', self._tourClose) # self._tourSa.show() # self._tourSa.next(0) QtCore.QTimer.singleShot(0, lambda: [self._tourSa.show(), self._tourSa.next(0)]) # self._tourSa.adjustSize) def _tourClose(self): self.tabs.setStyleSheet('') self.btnMenu.setStyleSheet('') del self._tourSa if self._startTourExample: self._tourExample_init(self._startTourExample) # self.tabUpload.dragW.btn.setStyleSheet('') def _tourNext(self, tour): # self._tour_btn1.setEnabled(self._tour_index >-2) if tour.index == -1: # begin of tour doc = About.FIRST_STEPS self.tabs.setStyleSheet('') elif tour.index == 4: # end of tour self._menuPopup() doc = '''Click on HELP(-->) to show this help again and to highlight all all input fields with a tooltip''' self.tabs.setStyleSheet('') self.btnMenu.setStyleSheet('QPushButton { %s }' % tour.style) else: # somewhere in the middle self.tabs.setCurrentIndex(tour.index) w = self.tabs.currentWidget() doc = w.__doc__ self.btnMenu.setStyleSheet('') self.tabs.setStyleSheet('QTabBar::tab:selected { %s }' % tour.style) # if self.tabs.currentWidget() == self.tabUpload: # # highlight block button: # self._dragWBtn_style = self.tabUpload.dragW.btn.styleSheet() # self.tabUpload.dragW.btn.setStyleSheet('QPushButton { %s }' % style) # elif hasattr(self, '_dragWBtn_style'): # # reset # self.tabUpload.dragW.btn.setStyleSheet(self._dragWBtn_style) # del self._dragWBtn_style doc = dedent(doc).rstrip().lstrip() tour.lab.setText(doc.replace('\n', '<br>')) # '<br><br>' + # self._initTourWidgetResize() def updateProjectFolder(self): self._proj = self.server.projectCode() self.root.mkdir(self._proj) def projectFolder(self): return self.root.join(self._proj) def _menuShowProjects(self): self._projects = Projects(self) self._projects.show() # def loadConfig(self): # try: # c = self.server.lastConfig() # self.config.restore(c) # except json.decoder.JSONDecodeError: # print('ERROR loading last config: %s' % c) def updateWindowTitle(self, project=None): if project is None: project = self.server.projectName() self.setWindowTitle('QELA | User: %s | Project: %s | Credit: %s' % ( self.user, project, self.server.remainingCredit())) def _menuShowAbout(self): if self._about is None: self._about = About(self) self._about.show() def _menuShowHelp(self): if self.help.isVisible(): self.help.hide() else: self.help.show() def _menuShowWebsite(self): os.startfile('https://%s' % self.server.address[0]) def _menuShowPlan(self): if self._pricing is None: self._pricing = Pricing(self) self._pricing.show() def menuShowContact(self): if self._contact is None: self._contact = Contact(self) self._contact.show() def modules(self): '''return a list of all modules either found in imageuploadtable (from client) or tabCheck (from server)''' ll = list(self.tabCheck.modules()) ll.extend(self.tabUpload.table.modules()) return set(ll) def verifyFile(self, path, warning=True): ''' returns True if local file could be verified ''' localpath = self.projectFolder().join(path) checksum = fileCheckSum(localpath) verified = self.server.verifyFile(path, checksum) if not warning: return verified if not verified: ret = QtWidgets.QMessageBox.warning(self, 'Verification error', '''File <{}> could not be verified by our server. It is possible, that is has been tampered with. Would you like to download it again?. '''.format(path), QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No) return ret == QtWidgets.QMessageBox.Yes return True def openImage(self, path, **kwargs): txt = self.tabConfig.preferences.cbViewer.currentText() if txt == 'dataArtist': dataArtist.importFile(path) elif txt == 'Inline': if self._tempview is None: self._tempview = InlineView() self._tempview(path, **kwargs) else: os.startfile(path) def _menuPopup(self): g = self.btnMenu.geometry() p = g.bottomLeft() p.setX(p.x() - (self._menu.sizeHint().width() - g.width())) self._menu.popup(self.mapToGlobal(p)) def removeTemporaryProcessBar(self, bar): self._tempBars.remove(bar) self.statusBar().removeWidget(bar) # self.progressbar.show() def addTemporaryProcessBar(self): c = CancelProgressBar() self._tempBars.append(c) self.progressbar.hide() self.statusBar().addPermanentWidget(c, stretch=1) return c def moveEvent(self, evt): self.sigMoved.emit(evt.pos()) def resizeEvent(self, evt): self.sigResized.emit(evt.size()) def closeEvent(self, ev): if not self.server.isReady(): msg = QtWidgets.QMessageBox() msg.setText("You are still uploading/downloading data") msg.setStandardButtons( QtWidgets.QMessageBox.Ok | QtWidgets.QMessageBox.Cancel) msg.exec_() if msg.result() == QtWidgets.QMessageBox.Ok: ev.accept() else: ev.ignore() return self._close() def _downloadDone(self): self._downloadThread = None self.progressbar.hide() if len(self._downloadQueue): # work on next in queue self.addDownload(*self._downloadQueue.pop(0)) def addDownload(self, paths, root, fnsDone=None, **kwargs): ''' paths tuple/list -> multiple files, str/Pathstr -> single file fnsDone tuple/list -> multiple functions, else -> single function roon ... either local to-file or download folder ''' b = self.progressbar if self._downloadThread is None: d = self._downloadThread = _DownloadThread(self, paths, root, **kwargs) if fnsDone is not None: if type(fnsDone) not in (tuple, list): fnsDone = [fnsDone] for fn in fnsDone: d.sigDone.connect(fn) d.sigUpdate.connect(b.bar.setValue) d.sigDone.connect(self._downloadDone) d.start() b.setColor('darkblue') b.bar.setFormat("Downloading %p%") b.setCancel(d.kill) b.show() else: # already downloading something: add this one to queue self._downloadQueue.append((paths, root, fnsDone)) def _close(self): self.saveSession() if self.server.isReady(): self.hide() # yieldOtherProgramInstances is quite slow, so close win first if not len(list(yieldOtherProgramInstances())): # is this window is the only one - no other client is opened: self.server.logout() def account(self): a = getattr(self, '_account', None) if not a: self._account = a = Account(self) a.show() def changeUser(self): self.close() L = self.login.__class__(self.server, True) L.show() def setTabsEnabled(self, enable=True, exclude=None): for i in range(self.tabs.count() - 1): if i != exclude: self.tabs.setTabEnabled(i, enable) if enable: self.tabs.setCurrentIndex(2) def _activateTab(self, index): w = self.tabs.widget(index) if self._lastW and hasattr(self._lastW, 'deactivate'): self._lastW.deactivate() if hasattr(w, 'activate'): w.activate() self._lastW = w