__main__.py 17.8 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/>.
"""
Elisa Kanaki's avatar
Elisa Kanaki committed
21 22 23
import logging
import logging.handlers
import traceback
24
import os
Elisa Kanaki's avatar
Elisa Kanaki committed
25 26
from io import StringIO
from typing import List
27
from PyQt5 import QtCore, QtWidgets, QtGui
28
from .sampleview import SampleView
Elisa Kanaki's avatar
Elisa Kanaki committed
29 30 31 32 33 34 35 36
from .gui.scalebar import ScaleBar
from .instrumentcom.instrumentConfig import defaultPath
from .instrumentcom.lightModeSwitch import LightModeSwitch
from .gui.colorlegend import ColorLegend
from .gepardlogging import setDefaultLoggingConfig
from .workmodes import ModeHandler
from .unittests.test_gepard import testGepard
from .helperfunctions import getAppFolder
37
from gepard import __version__
Elisa Kanaki's avatar
Elisa Kanaki committed
38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58


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 execution!")
    logging.critical(tbinfo)
    from .errors import showErrorMessageAsWidget
    showErrorMessageAsWidget(tbinfo)


JosefBrandt's avatar
 
JosefBrandt committed
59
class GEPARDMainWindow(QtWidgets.QMainWindow):
Elisa Kanaki's avatar
Elisa Kanaki committed
60
    def __init__(self, logger):
JosefBrandt's avatar
 
JosefBrandt committed
61
        super(GEPARDMainWindow, self).__init__()
62

63
        self.setWindowTitle("GEPARD " + __version__)
Elisa Kanaki's avatar
Elisa Kanaki committed
64
        self.fname: str = ''
65 66
        self.resize(900, 700)
        self.scalebar = ScaleBar(self)
67
        self.legend = ColorLegend(self)
Elisa Kanaki's avatar
Elisa Kanaki committed
68 69 70
        self.lightModeSwitch = LightModeSwitch(self)
        self.view = SampleView(self, logger)
        self.view.ScalingChanged.connect(self.scalingChanged)
71 72 73 74
        self.view.ScalingChanged.connect(self.scalebar.updateScale)
        
        mdiarea = QtWidgets.QMdiArea(self)
        mdiarea.addSubWindow(self.scalebar)
75
        mdiarea.addSubWindow(self.legend)
Elisa Kanaki's avatar
Elisa Kanaki committed
76
        mdiarea.addSubWindow(self.lightModeSwitch)
77
        self.legend.hide()
Elisa Kanaki's avatar
Elisa Kanaki committed
78
        self.lightModeSwitch.hide()
79
        
80 81 82 83 84 85 86 87 88 89
        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()
Josef Brandt's avatar
Josef Brandt committed
90
        self.modeHandler: ModeHandler = ModeHandler(self)
91
        self.modeHandler.modeSwitched.connect(self.view.viewItemHandler.switchToNewMode)
92 93 94
        self.updateModes()
        
    def resizeEvent(self, event):
95
        self.scalebar.move(0, self.height()-self.scalebar.height()-30)
96
        self.legend.move(self.width()-self.legend.width()-130, 10)
97
        self.lightModeSwitch.move(5, 5)
98 99 100
        
    def closeEvent(self, event):
        self.view.closeEvent(event)
101 102

    def forceCloseAll(self) -> None:
Josef Brandt's avatar
Josef Brandt committed
103
        closeAll()
104 105
        
    @QtCore.pyqtSlot(float) 
106
    def scalingChanged(self):
107 108 109 110 111 112 113
        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
114
            fileName = QtWidgets.QFileDialog.getOpenFileName(self, "Open Project", defaultPath, "*.pkl")[0]
115 116 117
        if fileName:
            self.fname = str(fileName)
            self.view.open(self.fname)  
118
            self.scalingChanged()
119 120 121 122
    
    @QtCore.pyqtSlot()         
    def importProject(self, fileName=False):
        if fileName is False:
123
            fileName = QtWidgets.QFileDialog.getOpenFileName(self, "Import Zeiss Zen Project", defaultPath, "*.xml")[0]
124 125 126
        if fileName:
            self.fname = str(fileName)
            self.view.importProject(self.fname)  
127
            self.scalingChanged()
128 129 130 131
            
    @QtCore.pyqtSlot() 
    def new(self, fileName=False):
        if fileName is False:
132
            fileName = QtWidgets.QFileDialog.getSaveFileName(self, "Create New Project", defaultPath, "*.pkl")[0]
133
        if fileName:
134 135
            isValid, msg = self.testFilename(fileName)
            if isValid:
Hackmet's avatar
Hackmet committed
136
                self.fname = str(fileName)
137
                self.view.new(self.fname)  
138
                self.scalingChanged()
139 140
            else:
                QtWidgets.QMessageBox.critical(self, "Error", msg)
Josef Brandt's avatar
 
Josef Brandt committed
141 142
            
    @QtCore.pyqtSlot()  
143
    def testFilename(self, fileName):
144
        if self.view.instrctrl.name == 'RenishawCOM':  # the renishawCom does not allow Spaces within filePath
145 146 147 148 149 150
            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
151
    
152 153
    @QtCore.pyqtSlot() 
    def about(self):
Elisa Kanaki's avatar
Elisa Kanaki committed
154 155 156 157 158 159 160 161 162 163
        devbranch = None
        fname = os.path.join(os.path.split(__file__)[0],
                             os.path.join('.git', 'HEAD'))
        if os.path.exists(fname):
            with open(fname, 'r') as fp:
                cont = fp.read()
            devbranch = cont.rsplit('/', maxsplit=1)[1]
        QtWidgets.QMessageBox.about(self, 'GEPARD',
                                    'Gepard-Enabled PARticle Detection for Raman and FTIR microscopes. \nVersion:' + \
                                    __version__ + '' if devbranch is None else ('\nDevelopment branch: ' + devbranch))
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
    def getParticleRelevantActs(self) -> List[QtWidgets.QAction]:
        return [self.noOverlayAct, self.selOverlayAct, self.fullOverlayAct, self.hideLabelAct, self.transpAct, self.darkenAct, self.seedAct]

    def disableParticleRelevantActs(self) -> None:
        for act in self.getParticleRelevantActs():
            act.setDisabled(True)

    def enableParticleRelevantActs(self) -> None:
        for act in self.getParticleRelevantActs():
            act.setEnabled(True)

    def updateModes(self, active=None, maxenabled=None):
        ose, osc, pde, pdc, rse, rsc = [False]*6
        if maxenabled == "OpticalScan":
            ose = True
        elif maxenabled == "ParticleDetection":
            ose, pde = True, True
        elif maxenabled == "SpectrumScan":
            ose, pde, rse = True, True, True
            
        if active == "OpticalScan" and ose:
            osc = True
        elif active == "ParticleDetection" and pde:
            pdc = True
        elif active == "SpectrumScan" and rse:
            rsc = True

        self.opticalScanAct.setEnabled(ose)
        self.opticalScanAct.setChecked(osc)
        self.detectParticleAct.setEnabled(pde)
        self.detectParticleAct.setChecked(pdc)
196 197
        self.specScanAct.setEnabled(rse)
        self.specScanAct.setChecked(rsc)
198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223

    def activateMaxMode(self, loadnew=False) -> None:
        self.modeHandler.activateMaxMode(loadnew)

    def getCurrentMode(self) -> str:
        mode: str = 'None'
        if self.modeHandler.activeMode is not None:
            mode = self.modeHandler.activeMode.name
        return mode
    
    def unblockUI(self, connected):
        self.openAct.setEnabled(True)
        self.importAct.setEnabled(True)
        self.newAct.setEnabled(True)
        self.updateConnected(connected)
        self.exitAct.setEnabled(True)
    
    def blockUI(self):
        self.openAct.setEnabled(False)
        self.importAct.setEnabled(False)
        self.newAct.setEnabled(False)
        self.connectRamanAct.setEnabled(False)
        self.disconnectRamanAct.setEnabled(False)
        self.exitAct.setEnabled(False)
        self.opticalScanAct.setEnabled(False)
        self.detectParticleAct.setEnabled(False)
224
        self.specScanAct.setEnabled(False)
225 226 227 228 229 230 231 232 233
        
    def updateConnected(self, connected):
        if connected:
            self.connectRamanAct.setEnabled(False)
            self.disconnectRamanAct.setEnabled(True)
        else:
            self.connectRamanAct.setEnabled(True)
            self.disconnectRamanAct.setEnabled(False)

234
    def createActions(self):
235 236
        def runGepardTest(gebbard):
            return lambda: testGepard(gebbard)
Josef Brandt's avatar
Josef Brandt committed
237

238 239 240
        fname = os.path.join(os.path.split(__file__)[0], os.path.join('data', 'brand.png'))
        self.aboutAct = QtWidgets.QAction(QtGui.QIcon(fname), "About Particle Measurment", self)
        self.aboutAct.triggered.connect(self.about)
Josef Brandt's avatar
Josef Brandt committed
241

242 243 244
        self.openAct = QtWidgets.QAction("&Open Project...", self)
        self.openAct.setShortcut("Ctrl+O")
        self.openAct.triggered.connect(self.open)
245

246
        self.importAct = QtWidgets.QAction("&Import Zeiss Project...", self)
247 248
        self.importAct.setShortcut("Ctrl+I")
        self.importAct.triggered.connect(self.importProject)
249

250 251 252 253 254 255 256 257 258
        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)
259
        self.zoomInAct.setShortcut("Ctrl++")
260 261 262 263
        self.zoomInAct.setEnabled(False)
        self.zoomInAct.triggered.connect(self.view.zoomIn)

        self.zoomOutAct = QtWidgets.QAction("Zoom &Out (25%)", self)
264
        self.zoomOutAct.setShortcut("Ctrl+-")
265 266 267 268 269 270 271 272 273 274 275 276
        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)
277

278 279 280
        self.connectRamanAct = QtWidgets.QAction("Connect to Microscope", self)
        self.connectRamanAct.setEnabled(True)
        self.connectRamanAct.triggered.connect(self.view.connectRaman)
281

282 283 284
        self.disconnectRamanAct = QtWidgets.QAction("Release Microscope", self)
        self.disconnectRamanAct.setEnabled(False)
        self.disconnectRamanAct.triggered.connect(self.view.disconnectRaman)
285

286 287 288
        self.opticalScanAct = QtWidgets.QAction("Optical Scan", self)
        self.opticalScanAct.setEnabled(False)
        self.opticalScanAct.setCheckable(True)
289
        self.opticalScanAct.triggered.connect(QtCore.pyqtSlot()(lambda: self.modeHandler.switchMode("OpticalScan")))
290 291 292 293

        self.detectParticleAct = QtWidgets.QAction("Detect Particles", self)
        self.detectParticleAct.setEnabled(False)
        self.detectParticleAct.setCheckable(True)
294 295 296
        self.detectParticleAct.triggered.connect(
            QtCore.pyqtSlot()(lambda: self.modeHandler.switchMode("ParticleDetection")))

297 298 299 300
        self.specScanAct = QtWidgets.QAction("Spectrum Scan", self)
        self.specScanAct.setEnabled(False)
        self.specScanAct.setCheckable(True)
        self.specScanAct.triggered.connect(QtCore.pyqtSlot()(lambda: self.modeHandler.switchMode("SpectrumScan")))
301

302
        self.snapshotAct = QtWidgets.QAction("&Save Screenshot", self)
303 304
        self.snapshotAct.triggered.connect(self.view.takeScreenshot)
        self.snapshotAct.setDisabled(True)
305

306
        self.configRamanCtrlAct = QtWidgets.QAction("&Configure Raman Control", self)
307
        self.configRamanCtrlAct.triggered.connect(self.view.configureInstrumentControl)
308 309
        if self.view.simulatedRaman:
            self.configRamanCtrlAct.setDisabled(True)
310

311 312 313
        self.exportSLFAct: QtWidgets.QAction = QtWidgets.QAction("&Export FTIR Apertures to .slf")
        self.exportSLFAct.triggered.connect(self.view.exportAptsToSLF)

314
        self.testAct: QtWidgets.QAction = QtWidgets.QAction("&Run Automated Gepard Test")
315
        self.testAct.setShortcut("Ctrl+T")
316 317
        self.testAct.triggered.connect(runGepardTest(self))

318
        self.noOverlayAct = QtWidgets.QAction("&No Overlay", self)
319
        self.noOverlayAct.setShortcut("1")
320 321
        self.noOverlayAct.setCheckable(True)
        self.selOverlayAct = QtWidgets.QAction("&Selected Overlay", self)  # TODO: Is that needed??
322
        self.selOverlayAct.setShortcut("2")
323
        self.selOverlayAct.setCheckable(True)
324
        self.fullOverlayAct = QtWidgets.QAction("&Full Overlay", self)
325
        self.fullOverlayAct.setShortcut("3")
326
        self.fullOverlayAct.setCheckable(True)
327
        self.transpAct = QtWidgets.QAction("&Transparent Overlay", self)
328
        self.transpAct.setShortcut("T")
329 330 331 332
        self.transpAct.setCheckable(True)
        self.transpAct.setChecked(False)
        self.transpAct.triggered.connect(self.view.viewItemHandler.adjustParticleViewItemsVisibility)

333
        self.hideLabelAct = QtWidgets.QAction('&Hide Spectra Numbers', self)
334
        self.hideLabelAct.setShortcut("H")
335
        self.darkenAct = QtWidgets.QAction("&Darken Image", self)
336
        self.darkenAct.setShortcut("D")
337
        self.seedAct = QtWidgets.QAction("&Set Color Seed", self)
338

339
        self.disableParticleRelevantActs()
340 341 342 343

    def createMenus(self):
        self.fileMenu = QtWidgets.QMenu("&File", self)
        self.fileMenu.addAction(self.newAct)
344
        self.fileMenu.addAction(self.importAct)
345 346 347
        self.fileMenu.addAction(self.openAct)
        self.fileMenu.addSeparator()
        self.fileMenu.addAction(self.exitAct)
348
        
349 350 351 352 353 354 355 356 357 358 359
        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
360
        self.toolsMenu = QtWidgets.QMenu("&Tools")
361 362
        self.toolsMenu.addAction(self.snapshotAct)
        self.toolsMenu.addAction(self.configRamanCtrlAct)
363 364
        self.toolsMenu.addAction(self.exportSLFAct)
        self.toolsMenu.addSeparator()
365
        self.toolsMenu.addAction(self.testAct)
366 367 368 369

        self.dispMenu = QtWidgets.QMenu("&Display", self)
        self.overlayActGroup = QtWidgets.QActionGroup(self.dispMenu)
        self.overlayActGroup.setExclusive(True)
370
        self.overlayActGroup.triggered.connect(self.view.viewItemHandler.adjustParticleViewItemsVisibility)
371 372 373 374
        
        for act in [self.noOverlayAct, self.selOverlayAct, self.fullOverlayAct]:
            self.dispMenu.addAction(act)
            self.overlayActGroup.addAction(act)
375
        self.fullOverlayAct.setChecked(True)
376 377
        self.dispMenu.addSeparator()
        self.dispMenu.addActions([self.transpAct, self.hideLabelAct, self.darkenAct, self.seedAct])
378
        
379 380
        self.helpMenu = QtWidgets.QMenu("&Help", self)
        self.helpMenu.addAction(self.aboutAct)
381

382 383 384
        self.menuBar().addMenu(self.fileMenu)
        self.menuBar().addMenu(self.viewMenu)
        self.menuBar().addMenu(self.modeMenu)
385
        self.menuBar().addMenu(self.toolsMenu)
386
        self.menuBar().addMenu(self.dispMenu)
387
        self.menuBar().addMenu(self.helpMenu)
388

389 390 391 392 393
    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)
394
        self.toolbar.addAction(self.importAct)
395 396 397 398 399 400 401
        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)
402
        self.toolbar.addAction(self.specScanAct)
403 404 405 406 407
        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
408

409 410 411
if __name__ == '__main__':
    import sys
    from time import localtime, strftime
Josef Brandt's avatar
Josef Brandt committed
412 413 414 415 416

    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
417
        ONLY close it at the end of running the program. Closing it on disconnect of the instrctrl is not suitable,
Josef Brandt's avatar
Josef Brandt committed
418 419 420
        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.
        """
421
        app.closeAllWindows()
Josef Brandt's avatar
Josef Brandt committed
422

423 424 425
    sys.excepthook = excepthook
    logger = logging.getLogger(__name__)
    
426
    app = QtWidgets.QApplication(sys.argv)
427
    logpath = getAppFolder()
428 429 430 431
    if logpath != "":
        if not os.path.exists(logpath):
            os.mkdir(logpath)
        logname = os.path.join(logpath, 'logfile.txt')
432 433 434 435
        logger.addHandler(
            logging.handlers.RotatingFileHandler(
                logname, maxBytes=5*(1 << 20), backupCount=10)
        )
Josef Brandt's avatar
Josef Brandt committed
436
    logger.setLevel(logging.DEBUG)
437
    setDefaultLoggingConfig(logger)
438 439 440
    logger.info("starting GEPARD at: " + strftime("%d %b %Y %H:%M:%S", localtime()))
    
    gepard = GEPARDMainWindow(logger)
JosefBrandt's avatar
 
JosefBrandt committed
441
    gepard.showMaximized()
442

443
    ret = app.exec_()