__main__.py 16 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
# -*- coding: utf-8 -*-
"""
GEPARD - Gepard-Enabled PARticle Detection
Copyright (C) 2018  Lars Bittrich and Josef Brandt, Leibniz-Institut für 
Polymerforschung Dresden e. V. <bittrich-lars@ipfdd.de>    

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program, see COPYING.  
If not, see <https://www.gnu.org/licenses/>.
"""
21
import logging
22
import logging.handlers
23
import traceback
24
import os
25
from io import StringIO
26 27 28 29 30
from PyQt5 import QtCore, QtWidgets, QtGui
from .sampleview import SampleView
from .scalebar import ScaleBar
from .ramancom.ramancontrol import defaultPath
from .ramancom.ramanSwitch import RamanSwitch
31
from .analysis.colorlegend import ColorLegend
32 33
from .gepardlogging import setDefaultLoggingConfig

JosefBrandt's avatar
 
JosefBrandt committed
34
class GEPARDMainWindow(QtWidgets.QMainWindow):
35
    def __init__(self, logger):
JosefBrandt's avatar
 
JosefBrandt committed
36
        super(GEPARDMainWindow, self).__init__()
37 38 39 40

        self.setWindowTitle("GEPARD")
        self.resize(900, 700)
        
41
        self.view = SampleView(logger)
42 43 44
        self.view.imparent = self
        self.view.ScalingChanged.connect(self.scalingChanged)
        self.scalebar = ScaleBar(self)
45
        self.legend = ColorLegend(self)
46
        self.ramanSwitch = RamanSwitch(self)
47 48 49 50
        self.view.ScalingChanged.connect(self.scalebar.updateScale)
        
        mdiarea = QtWidgets.QMdiArea(self)
        mdiarea.addSubWindow(self.scalebar)
51 52 53 54 55
        mdiarea.addSubWindow(self.legend)
        mdiarea.addSubWindow(self.ramanSwitch)
        self.legend.hide()
        self.ramanSwitch.hide()
        
56 57 58 59 60 61 62 63 64 65 66 67 68
        subview = mdiarea.addSubWindow(self.view)
        subview.showMaximized()
        subview.setWindowFlags(QtCore.Qt.FramelessWindowHint)
        mdiarea.setOption(QtWidgets.QMdiArea.DontMaximizeSubWindowOnActivation)
        
        self.setCentralWidget(mdiarea)
        
        self.createActions()
        self.createMenus()
        self.createToolBar()
        self.updateModes()
        
    def resizeEvent(self, event):
69 70 71
        self.scalebar.move(0,self.height()-self.scalebar.height()-30)
        self.legend.move(self.width()-self.legend.width()-130, 10)
        self.ramanSwitch.move(5, 5)
72 73 74
        
    def closeEvent(self, event):
        self.view.closeEvent(event)
Josef Brandt's avatar
Josef Brandt committed
75 76 77 78
        closeAll()

    @QtCore.pyqtSlot(float)
    def scalingChanged(self, scale: float):
79 80 81 82 83 84 85
        self.zoomInAct.setEnabled(self.view.scaleFactor < 20.0)
        self.zoomOutAct.setEnabled(self.view.scaleFactor > .01)
        self.normalSizeAct.setEnabled(self.view.scaleFactor != 1.)

    @QtCore.pyqtSlot() 
    def open(self, fileName=False):
        if fileName is False:
Josef Brandt's avatar
Josef Brandt committed
86
            fileName = QtWidgets.QFileDialog.getOpenFileName(self, "Open Project", defaultPath, "*.pkl")[0]
87 88 89 90
        if fileName:
            self.fname = str(fileName)
            self.view.open(self.fname)  
            self.scalingChanged(1.)
91 92 93 94 95 96 97 98 99 100
    
    @QtCore.pyqtSlot()         
    def importProject(self, fileName=False):
        if fileName is False:
            fileName = QtWidgets.QFileDialog.getOpenFileName(self, "Import Zeiss Zen Project",
                                        defaultPath, "*.xml")[0]
        if fileName:
            self.fname = str(fileName)
            self.view.importProject(self.fname)  
            self.scalingChanged(1.)
101 102 103 104 105 106 107
            
    @QtCore.pyqtSlot() 
    def new(self, fileName=False):
        if fileName is False:
            fileName = QtWidgets.QFileDialog.getSaveFileName(self, "Create New Project",
                                        defaultPath, "*.pkl")[0]
        if fileName:
108 109
            isValid, msg = self.testFilename(fileName)
            if isValid:
Hackmet's avatar
Hackmet committed
110
                self.fname = str(fileName)
111 112 113 114
                self.view.new(self.fname)  
                self.scalingChanged(1.)
            else:
                QtWidgets.QMessageBox.critical(self, "Error", msg)
Josef Brandt's avatar
 
Josef Brandt committed
115 116
            
    @QtCore.pyqtSlot()  
117 118 119 120 121 122 123 124
    def testFilename(self, fileName):
        if self.view.ramanctrl.name == 'RenishawCOM':   #the renishawCom does not allow Spaces within filePath
            if fileName.find(' ') == 0:
                return False, "File path must not contain spaces."
            else:
                return True, ""
        else:
            return True, ""
Josef Brandt's avatar
 
Josef Brandt committed
125
    
126 127 128 129 130 131 132 133
    @QtCore.pyqtSlot() 
    def about(self):
        QtWidgets.QMessageBox.about(self, 'GEPARD',
            "Developed by Complex Fiber Structures GmbH on behalf of Leibniz-IPF Dresden")


    def createActions(self):
        fname = os.path.join(os.path.split(__file__)[0],
134
                             os.path.join('data', 'brand.png'))
135 136 137 138 139 140 141 142
        self.aboutAct = QtWidgets.QAction(QtGui.QIcon(fname),
                                          "About Particle Measurment", self)
        self.aboutAct.triggered.connect(self.about)
        
        self.openAct = QtWidgets.QAction("&Open Project...", self)
        self.openAct.setShortcut("Ctrl+O")
        self.openAct.triggered.connect(self.open)
        
143
        self.importAct = QtWidgets.QAction("&Import Zeiss Project...", self)
144 145 146
        self.importAct.setShortcut("Ctrl+I")
        self.importAct.triggered.connect(self.importProject)
        
147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197
        self.newAct = QtWidgets.QAction("&New Measurement...", self)
        self.newAct.setShortcut("Ctrl+N")
        self.newAct.triggered.connect(self.new)

        self.exitAct = QtWidgets.QAction("E&xit", self)
        self.exitAct.setShortcut("Ctrl+Q")
        self.exitAct.triggered.connect(self.close)

        self.zoomInAct = QtWidgets.QAction("Zoom &In (25%)", self)
        self.zoomInAct.setShortcut("Ctrl++") 
        self.zoomInAct.setEnabled(False)
        self.zoomInAct.triggered.connect(self.view.zoomIn)

        self.zoomOutAct = QtWidgets.QAction("Zoom &Out (25%)", self)
        self.zoomOutAct.setShortcut("Ctrl+-") 
        self.zoomOutAct.setEnabled(False)
        self.zoomOutAct.triggered.connect(self.view.zoomOut)

        self.normalSizeAct = QtWidgets.QAction("&Normal Size", self)
        self.normalSizeAct.setShortcut("Ctrl+S")
        self.normalSizeAct.setEnabled(False)
        self.normalSizeAct.triggered.connect(self.view.normalSize)

        self.fitToWindowAct = QtWidgets.QAction("&Fit to Window", self)
        self.fitToWindowAct.setShortcut("Ctrl+E")
        self.fitToWindowAct.setEnabled(True)
        self.fitToWindowAct.triggered.connect(self.view.fitToWindow)
        
        self.connectRamanAct = QtWidgets.QAction("Connect to Microscope", self)
        self.connectRamanAct.setEnabled(True)
        self.connectRamanAct.triggered.connect(self.view.connectRaman)
        
        self.disconnectRamanAct = QtWidgets.QAction("Release Microscope", self)
        self.disconnectRamanAct.setEnabled(False)
        self.disconnectRamanAct.triggered.connect(self.view.disconnectRaman)
                
        self.opticalScanAct = QtWidgets.QAction("Optical Scan", self)
        self.opticalScanAct.setEnabled(False)
        self.opticalScanAct.setCheckable(True)
        self.opticalScanAct.triggered.connect(QtCore.pyqtSlot()(lambda :self.view.switchMode("OpticalScan")))

        self.detectParticleAct = QtWidgets.QAction("Detect Particles", self)
        self.detectParticleAct.setEnabled(False)
        self.detectParticleAct.setCheckable(True)
        self.detectParticleAct.triggered.connect(QtCore.pyqtSlot()(lambda :self.view.switchMode("ParticleDetection")))
        
        self.ramanScanAct = QtWidgets.QAction("Raman Scan", self)
        self.ramanScanAct.setEnabled(False)
        self.ramanScanAct.setCheckable(True)
        self.ramanScanAct.triggered.connect(QtCore.pyqtSlot()(lambda :self.view.switchMode("RamanScan")))
        
198 199 200
        self.snapshotAct = QtWidgets.QAction("Save Screenshot", self)
        self.snapshotAct.triggered.connect(self.view.takeScreenshot)
        self.snapshotAct.setDisabled(True)
201

202 203 204 205
        self.configRamanCtrlAct = QtWidgets.QAction("Configure Raman Control", self)
        self.configRamanCtrlAct.triggered.connect(self.view.configureRamanControl)
        if self.view.simulatedRaman:
            self.configRamanCtrlAct.setDisabled(True)
206 207 208 209

        self.recalculateCoordAct = QtWidgets.QAction("&Recalculate Coordinate System")
        self.recalculateCoordAct.setDisabled(True)
        self.recalculateCoordAct.triggered.connect(self.view.recalculateCoordinateSystem)
Josef Brandt's avatar
 
Josef Brandt committed
210
            
211
        self.noOverlayAct = QtWidgets.QAction("&No Overlay", self)
212
        self.noOverlayAct.setShortcut("1")
213
        self.selOverlayAct = QtWidgets.QAction("&Selected Overlay", self)
214
        self.selOverlayAct.setShortcut("2")
215
        self.fullOverlayAct = QtWidgets.QAction("&Full Overlay", self)
216
        self.fullOverlayAct.setShortcut("3")
217
        self.transpAct = QtWidgets.QAction("&Transparent Overlay", self)
218
        self.transpAct.setShortcut("T")
219
        self.hideLabelAct = QtWidgets.QAction('&Hide Spectra Numbers', self)
220
        self.hideLabelAct.setShortcut("H")
221
        self.darkenAct = QtWidgets.QAction("&Darken Image", self)
222
        self.darkenAct.setShortcut("D")
223 224 225 226 227
        self.seedAct = QtWidgets.QAction("&Set Color Seed", self)
        
        for act in [self.noOverlayAct, self.selOverlayAct, self.fullOverlayAct, self.hideLabelAct, self.transpAct, self.darkenAct, self.seedAct]:
            act.setDisabled(True)
            
228
    def updateModes(self, active=None, maxenabled=None):
229
        ose, osc, pde, pdc, rse, rsc = [False]*6
230
        if maxenabled == "OpticalScan":
231
            ose = True
232
        elif maxenabled == "ParticleDetection":
233
            ose, pde = True, True
234
        elif maxenabled == "RamanScan":
235 236
            ose, pde, rse = True, True, True
            
237
        if active == "OpticalScan" and ose:
238
            osc = True
239
        elif active == "ParticleDetection" and pde:
240
            pdc = True
241
        elif active == "RamanScan" and rse:
242
            rsc = True
243

244 245 246 247 248 249 250 251 252
        self.opticalScanAct.setEnabled(ose)
        self.opticalScanAct.setChecked(osc)
        self.detectParticleAct.setEnabled(pde)
        self.detectParticleAct.setChecked(pdc)
        self.ramanScanAct.setEnabled(rse)
        self.ramanScanAct.setChecked(rsc)
    
    def unblockUI(self, connected):
        self.openAct.setEnabled(True)
253
        self.importAct.setEnabled(True)
254 255 256 257 258 259
        self.newAct.setEnabled(True)
        self.updateConnected(connected)
        self.exitAct.setEnabled(True)
    
    def blockUI(self):
        self.openAct.setEnabled(False)
260
        self.importAct.setEnabled(False)
261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279
        self.newAct.setEnabled(False)
        self.connectRamanAct.setEnabled(False)
        self.disconnectRamanAct.setEnabled(False)
        self.exitAct.setEnabled(False)
        self.opticalScanAct.setEnabled(False)
        self.detectParticleAct.setEnabled(False)
        self.ramanScanAct.setEnabled(False)
        
    def updateConnected(self, connected):
        if connected:
            self.connectRamanAct.setEnabled(False)
            self.disconnectRamanAct.setEnabled(True)
        else:
            self.connectRamanAct.setEnabled(True)
            self.disconnectRamanAct.setEnabled(False)

    def createMenus(self):
        self.fileMenu = QtWidgets.QMenu("&File", self)
        self.fileMenu.addAction(self.newAct)
280
        self.fileMenu.addAction(self.importAct)
281 282 283
        self.fileMenu.addAction(self.openAct)
        self.fileMenu.addSeparator()
        self.fileMenu.addAction(self.exitAct)
284
        
285 286 287 288 289 290 291 292 293 294 295
        self.viewMenu = QtWidgets.QMenu("&View", self)
        self.viewMenu.addAction(self.zoomInAct)
        self.viewMenu.addAction(self.zoomOutAct)
        self.viewMenu.addAction(self.normalSizeAct)
        self.viewMenu.addSeparator()
        self.viewMenu.addAction(self.fitToWindowAct)
        
        self.modeMenu = QtWidgets.QMenu("&Mode", self)
        self.modeMenu.addAction(self.opticalScanAct)
        self.modeMenu.addAction(self.detectParticleAct)
        
Hackmet's avatar
Hackmet committed
296
        self.toolsMenu = QtWidgets.QMenu("&Tools")
297 298
        self.toolsMenu.addAction(self.snapshotAct)
        self.toolsMenu.addAction(self.configRamanCtrlAct)
299
        self.toolsMenu.addAction(self.recalculateCoordAct)
300 301 302 303 304 305 306 307 308 309 310

        self.dispMenu = QtWidgets.QMenu("&Display", self)
        self.overlayActGroup = QtWidgets.QActionGroup(self.dispMenu)
        self.overlayActGroup.setExclusive(True)
        
        for act in [self.noOverlayAct, self.selOverlayAct, self.fullOverlayAct]:
            self.dispMenu.addAction(act)
            self.overlayActGroup.addAction(act)
        
        self.dispMenu.addSeparator()
        self.dispMenu.addActions([self.transpAct, self.hideLabelAct, self.darkenAct, self.seedAct])
311
        
312 313
        self.helpMenu = QtWidgets.QMenu("&Help", self)
        self.helpMenu.addAction(self.aboutAct)
314

315 316 317
        self.menuBar().addMenu(self.fileMenu)
        self.menuBar().addMenu(self.viewMenu)
        self.menuBar().addMenu(self.modeMenu)
318
        self.menuBar().addMenu(self.toolsMenu)
319
        self.menuBar().addMenu(self.dispMenu)
320
        self.menuBar().addMenu(self.helpMenu)
321

322 323 324 325 326
    def createToolBar(self):
        self.toolbar = QtWidgets.QToolBar("Tools")
        self.toolbar.setIconSize(QtCore.QSize(100,50))
        self.toolbar.addAction(self.aboutAct)
        self.toolbar.addAction(self.newAct)
327
        self.toolbar.addAction(self.importAct)
328 329 330 331 332 333 334 335 336 337 338 339 340
        self.toolbar.addAction(self.openAct)
        self.toolbar.addSeparator()
        self.toolbar.addAction(self.connectRamanAct)
        self.toolbar.addAction(self.disconnectRamanAct)
        self.toolbar.addSeparator()
        self.toolbar.addAction(self.opticalScanAct)
        self.toolbar.addAction(self.detectParticleAct)
        self.toolbar.addAction(self.ramanScanAct)
        self.toolbar.addSeparator()
        self.toolbar.addAction(self.exitAct)
        self.toolbar.setOrientation(QtCore.Qt.Vertical)
        self.addToolBar(QtCore.Qt.LeftToolBarArea, self.toolbar)

Josef Brandt's avatar
Josef Brandt committed
341

342 343 344
if __name__ == '__main__':
    import sys
    from time import localtime, strftime
Josef Brandt's avatar
Josef Brandt committed
345 346 347 348 349 350 351 352 353 354

    def closeAll() -> None:
        """
        Closes the app and, with that, all windows.
        Josef: I implemented this, as with the simulated microscope stage it was difficult to find a proper way to
        ONLY close it at the end of running the program. Closing it on disconnect of the ramanctrl is not suitable,
        as it should be opened also in disconnected stage (e.g., when another instance is running in optical or raman
        scan, but the UI (disconnected) should still update what's going on.
        """
        app.deleteLater()
355
    
356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377
    def excepthook(excType, excValue, tracebackobj):
        """
        Global function to catch unhandled exceptions.
        
        @param excType exception type
        @param excValue exception value
        @param tracebackobj traceback object
        :return:
        """
        tbinfofile = StringIO()
        traceback.print_tb(tracebackobj, None, tbinfofile)
        tbinfofile.seek(0)
        tbinfo = tbinfofile.read()
        logging.critical("Fatal error in program excecution!")
        logging.critical(tbinfo)
        from .errors import showErrorMessageAsWidget
        showErrorMessageAsWidget(tbinfo)
        
        
    sys.excepthook = excepthook
    logger = logging.getLogger(__name__)
    
378
    app = QtWidgets.QApplication(sys.argv)
379
    app.setApplicationName("GEPARD")  # appname needed for logpath
380 381 382

    logpath = QtCore.QStandardPaths.writableLocation(
              QtCore.QStandardPaths.AppLocalDataLocation)
383
    fp = None
384 385 386 387
    if logpath != "":
        if not os.path.exists(logpath):
            os.mkdir(logpath)
        logname = os.path.join(logpath, 'logfile.txt')
388 389 390 391 392 393
        logger.addHandler(
            logging.handlers.RotatingFileHandler(
                logname, maxBytes=5*(1 << 20), backupCount=10)
        )

    setDefaultLoggingConfig(logger)
394 395 396
    logger.info("starting GEPARD at: " + strftime("%d %b %Y %H:%M:%S", localtime()))
    
    gepard = GEPARDMainWindow(logger)
JosefBrandt's avatar
 
JosefBrandt committed
397
    gepard.showMaximized()
398

399
    ret = app.exec_()