image_plot.py 28.6 KB
Newer Older
1
2
3
4
# -*- coding: utf-8 -*-
#
# This file is part of the bliss project
#
Benoit Formet's avatar
Benoit Formet committed
5
# Copyright (c) 2015-2020 Beamline Control Unit, ESRF
6
7
8
9
10
11
# Distributed under the GNU LGPLv3. See LICENSE for more info.

from __future__ import annotations
from typing import Optional
from typing import Dict
from typing import List
Valentin Valls's avatar
Typo    
Valentin Valls committed
12
from typing import NamedTuple
13

14
import logging
15
import numpy
16

17
from silx.gui import qt
18
from silx.gui import icons
19
from silx.gui import colors
20
from silx.gui.plot.actions import histogram
21
from silx.gui.plot.items.marker import Marker
Valentin Valls's avatar
Valentin Valls committed
22
23
24
25
26
from silx.gui.plot.tools.roi import RegionOfInterestManager
from silx.gui.plot.items import roi as silx_rois
from bliss.controllers.lima import roi as lima_rois
from bliss.flint.widgets.utils import rois as flint_rois

27
28
29
from bliss.flint.model import scan_model
from bliss.flint.model import flint_model
from bliss.flint.model import plot_model
Valentin Valls's avatar
Valentin Valls committed
30
from bliss.flint.model import style_model
31
from bliss.flint.model import plot_item_model
Valentin Valls's avatar
Valentin Valls committed
32
from bliss.flint.helper import scan_info_helper
33
from bliss.flint.helper import model_helper
Valentin Valls's avatar
Valentin Valls committed
34
35
36
37
38
39
40
41
42
from .utils import plot_helper
from .utils import view_helper
from .utils import refresh_helper
from .utils import tooltip_helper
from .utils import export_action
from .utils import marker_action
from .utils import camera_live_action
from .utils import profile_action
from .utils import plot_action
43
from .utils import style_action
Valentin Valls's avatar
Valentin Valls committed
44

45
46

_logger = logging.getLogger(__name__)
47
48


Valentin Valls's avatar
Typo    
Valentin Valls committed
49
50
51
52
class _ItemDescription(NamedTuple):
    key: str
    kind: str
    shape: numpy.ndarray
53
54


55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
class _Title:
    def __init__(self, plot):
        self.__plot = plot

        self.__hasPreviousImage: bool = False
        """Remember that there was an image before this scan, to avoid to
        override the title at startup and waiting for the first image"""
        self.__lastSubTitle = None
        """Remembers the last subtitle in case it have to be reuse when
        displaying the data from the previous scan"""

    def itemUpdated(self, scan, item):
        self.__updateAll(scan, item)

    def scanRemoved(self, scan):
        """Removed scan, just before using another scan"""
        if scan is not None:
            self.__updateTitle("From previous scan")
            self.__hasPreviousImage = True
        else:
            self.__hasPreviousImage = False

    def scanStarted(self, scan):
        if not self.__hasPreviousImage:
            self.__updateAll(scan)

    def scanFinished(self, scan):
        title = scan_info_helper.get_full_title(scan)
        if scan.state() == scan_model.ScanState.FINISHED:
            title += " (finished)"
        self.__updateTitle(title)

    def __formatItemTitle(self, scan: scan_model.Scan, item=None):
        if item is None:
            return None
        channel = item.imageChannel()
        if channel is None:
            return None

        frameInfo = ""
        displayName = channel.displayName(scan)
96
        shape = ""
97
98
        data = channel.data(scan)
        if data is not None:
99
100
101
102
103
            array = data.array()
            if array is not None:
                height, width = array.shape[0:2]
                shape = f": {width} × {height}"

104
105
106
107
108
109
110
111
112
113
            if data.source() == "video":
                op = " ≈ "
            else:
                op = " = "

            if data.frameId() is not None:
                frameInfo = f", id{op}{data.frameId()}"
            if frameInfo != "":
                frameInfo += " "
            frameInfo += f"[{data.source()}]"
114
        return f"{displayName}{shape}{frameInfo}"
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135

    def __updateTitle(self, title):
        subtitle = None
        if self.__lastSubTitle is not None:
            subtitle = self.__lastSubTitle
        if subtitle is not None:
            title = f"{title}\n{subtitle}"
        self.__plot.setGraphTitle(title)

    def __updateAll(self, scan: scan_model.Scan, item=None):
        title = scan_info_helper.get_full_title(scan)
        subtitle = None
        itemTitle = self.__formatItemTitle(scan, item)
        self.__lastSubTitle = itemTitle
        if itemTitle is not None:
            subtitle = f"{itemTitle}"
        if subtitle is not None:
            title = f"{title}\n{subtitle}"
        self.__plot.setGraphTitle(title)


136
class ImagePlotWidget(plot_helper.PlotWidget):
137
138
139
140
141
    def __init__(self, parent=None):
        super(ImagePlotWidget, self).__init__(parent=parent)
        self.__scan: Optional[scan_model.Scan] = None
        self.__flintModel: Optional[flint_model.FlintState] = None
        self.__plotModel: plot_model.Plot = None
142
        self.__deviceName: str = None
143

144
        self.__items: Dict[plot_model.Item, List[_ItemDescription]] = {}
145
146

        self.__plotWasUpdated: bool = False
147
        self.__plot = plot_helper.FlintPlot(parent=self)
148
        self.__plot.setActiveCurveStyle(linewidth=2)
149
        self.__plot.setKeepDataAspectRatio(True)
Valentin Valls's avatar
Valentin Valls committed
150
        self.__plot.setDataMargins(0.05, 0.05, 0.05, 0.05)
Valentin Valls's avatar
Valentin Valls committed
151
        self.__plot.getYAxis().setInverted(True)
Valentin Valls's avatar
Valentin Valls committed
152

Valentin Valls's avatar
Valentin Valls committed
153
        self.__roiManager = RegionOfInterestManager(self.__plot)
154
        self.__profileAction = None
Valentin Valls's avatar
Valentin Valls committed
155

156
157
        self.__title = _Title(self.__plot)

158
159
        self.__colormap = colors.Colormap("viridis")
        """Each detector have a dedicated widget and a dedicated colormap"""
160
        self.__colormapInitialized = False
161

162
163
164
        self.setFocusPolicy(qt.Qt.StrongFocus)
        self.__plot.installEventFilter(self)
        self.__plot.getWidgetHandle().installEventFilter(self)
165
        self.__view = view_helper.ViewManager(self.__plot)
166
        self.__view.setResetWhenScanStarts(False)
167
        self.__view.setResetWhenPlotCleared(False)
168

169
        self.__aggregator = plot_helper.PlotEventAggregator(self)
170
        self.__refreshManager = refresh_helper.RefreshManager(self)
171
        self.__refreshManager.refreshModeChanged.connect(self.__refreshModeChanged)
172
173
174
        self.__refreshManager.setAggregator(self.__aggregator)

        toolBar = self.__createToolBar()
175
176

        # Try to improve the look and feel
177
        # FIXME: This should be done with stylesheet
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
        line = qt.QFrame(self)
        line.setFrameShape(qt.QFrame.HLine)
        line.setFrameShadow(qt.QFrame.Sunken)

        frame = qt.QFrame(self)
        frame.setFrameShape(qt.QFrame.StyledPanel)
        frame.setAutoFillBackground(True)
        layout = qt.QVBoxLayout(frame)
        layout.setContentsMargins(0, 0, 0, 0)
        layout.setSpacing(0)
        layout.addWidget(toolBar)
        layout.addWidget(line)
        layout.addWidget(self.__plot)
        widget = qt.QFrame(self)
        layout = qt.QVBoxLayout(widget)
        layout.addWidget(frame)
        layout.setContentsMargins(0, 1, 0, 0)
        self.setWidget(widget)
196

197
        self.__tooltipManager = tooltip_helper.TooltipItemManager(self, self.__plot)
198
199
        self.__tooltipManager.setFilter(plot_helper.FlintImage)

200
201
202
203
204
        self.__minMarker = Marker()
        self.__minMarker.setSymbol("")
        self.__minMarker.setVisible(False)
        self.__minMarker.setColor("pink")
        self.__minMarker.setZValue(0.1)
Valentin Valls's avatar
Valentin Valls committed
205
        self.__minMarker.setName("min")
206
207
208
209
210
211

        self.__maxMarker = Marker()
        self.__maxMarker.setSymbol("")
        self.__maxMarker.setVisible(False)
        self.__maxMarker.setColor("pink")
        self.__maxMarker.setZValue(0.1)
Valentin Valls's avatar
Valentin Valls committed
212
        self.__maxMarker.setName("max")
213

214
215
216
217
        self.__imageReceived = 0
        """Count the received image for this scan to allow to clean up the
        screen in the end if nothing was received"""

218
219
220
        self.__plot.addItem(self.__tooltipManager.marker())
        self.__plot.addItem(self.__minMarker)
        self.__plot.addItem(self.__maxMarker)
221

222
223
224
225
226
227
        self.widgetActivated.connect(self.__activated)

    def __activated(self):
        self.__initColormapWidget()

    def __initColormapWidget(self):
Valentin Valls's avatar
Valentin Valls committed
228
229
230
231
        flintModel = self.flintModel()
        if flintModel is None:
            return
        live = flintModel.liveWindow()
232
233
234
235
236
237
238
239
240
        colormapWidget = live.acquireColormapWidget(self)
        if colormapWidget is not None:
            for item in self.__plot.getItems():
                if isinstance(item, plot_helper.FlintImage):
                    colormapWidget.setItem(item)
                    break
            else:
                colormapWidget.setColormap(self.__colormap)

241
    def deviceName(self):
242
243
244
245
        return self.__deviceName

    def setDeviceName(self, name):
        self.__deviceName = name
246

247
248
249
250
251
252
253
    def configuration(self):
        config = super(ImagePlotWidget, self).configuration()
        try:
            config.colormap = self.__colormap._toDict()
        except Exception:
            # As it relies on private API, make it safe
            _logger.error("Impossible to save colormap preference", exc_info=True)
254
255

        config.profile_state = self.__profileAction.saveState()
256
257
258
        return config

    def setConfiguration(self, config):
Valentin Valls's avatar
Valentin Valls committed
259
260
261
262
263
264
265
266
267
        if config.colormap is not None:
            try:
                self.__colormap._setFromDict(config.colormap)
                self.__colormapInitialized = True
            except Exception:
                # As it relies on private API, make it safe
                _logger.error(
                    "Impossible to restore colormap preference", exc_info=True
                )
268
269
270
        if config.profile_state is not None:
            self.__profileAction.restoreState(config.profile_state)

271
272
        super(ImagePlotWidget, self).setConfiguration(config)

Valentin Valls's avatar
Valentin Valls committed
273
274
275
    def defaultColormap(self):
        return self.__colormap

276
277
278
    def getRefreshManager(self) -> plot_helper.RefreshManager:
        return self.__refreshManager

279
280
    def __createToolBar(self):
        toolBar = qt.QToolBar(self)
Valentin Valls's avatar
Valentin Valls committed
281
        toolBar.setMovable(False)
282
283
284

        from silx.gui.plot.actions import mode
        from silx.gui.plot.actions import control
Valentin Valls's avatar
Valentin Valls committed
285
        from silx.gui.widgets.MultiModeAction import MultiModeAction
286

Valentin Valls's avatar
Valentin Valls committed
287
288
289
290
        modeAction = MultiModeAction(self)
        modeAction.addAction(mode.ZoomModeAction(self.__plot, self))
        modeAction.addAction(mode.PanModeAction(self.__plot, self))
        toolBar.addAction(modeAction)
291
292
293
294
295
296
297
298

        resetZoom = self.__view.createResetZoomAction(parent=self)
        toolBar.addAction(resetZoom)
        toolBar.addSeparator()

        # Axis
        action = self.__refreshManager.createRefreshAction(self)
        toolBar.addAction(action)
Valentin Valls's avatar
Valentin Valls committed
299
        toolBar.addAction(plot_action.CustomAxisAction(self.__plot, self, kind="image"))
300
301
        toolBar.addSeparator()

302
        # Item
303
304
        action = style_action.FlintSharedColormapAction(self.__plot, self)
        action.setInitColormapWidgetCallback(self.__initColormapWidget)
305
306
307
308
        toolBar.addAction(action)
        self.__contrastAction = action
        toolBar.addSeparator()

309
        # Tools
Valentin Valls's avatar
Valentin Valls committed
310
        self.liveAction = camera_live_action.CameraLiveAction(self)
311
        toolBar.addAction(self.liveAction)
Valentin Valls's avatar
Valentin Valls committed
312
313
314
        action = control.CrosshairAction(self.__plot, parent=self)
        action.setIcon(icons.getQIcon("flint:icons/crosshair"))
        toolBar.addAction(action)
315
316
317
318
        action = histogram.PixelIntensitiesHistoAction(self.__plot, self)
        icon = icons.getQIcon("flint:icons/histogram")
        action.setIcon(icon)
        toolBar.addAction(action)
319

320
321
        self.__profileAction = profile_action.ProfileAction(self.__plot, self, "image")
        toolBar.addAction(self.__profileAction)
322

Valentin Valls's avatar
Valentin Valls committed
323
        action = marker_action.MarkerAction(plot=self.__plot, parent=self, kind="image")
Valentin Valls's avatar
Valentin Valls committed
324
325
326
        self.__markerAction = action
        toolBar.addAction(action)

327
328
329
330
331
332
333
334
        action = control.ColorBarAction(self.__plot, self)
        icon = icons.getQIcon("flint:icons/colorbar")
        action.setIcon(icon)
        toolBar.addAction(action)
        toolBar.addSeparator()

        # Export

Valentin Valls's avatar
Valentin Valls committed
335
336
        self.__exportAction = export_action.ExportAction(self.__plot, self)
        toolBar.addAction(self.__exportAction)
337
338

        return toolBar
339

340
341
342
343
    def logbookAction(self):
        """Expose a logbook action if one"""
        return self.__exportAction.logbookAction()

344
345
346
347
348
349
350
    def _silxPlot(self):
        """Returns the silx plot associated to this view.

        It is provided without any warranty.
        """
        return self.__plot

351
    def eventFilter(self, widget, event):
Valentin Valls's avatar
Valentin Valls committed
352
353
354
        if widget is self.__plot or widget is self.__plot.getWidgetHandle():
            if event.type() == qt.QEvent.MouseButtonPress:
                self.widgetActivated.emit(self)
355
356
357
358
359
360
361
        return widget.eventFilter(widget, event)

    def createPropertyWidget(self, parent: qt.QWidget):
        from . import image_plot_property

        propertyWidget = image_plot_property.ImagePlotPropertyWidget(parent)
        propertyWidget.setFlintModel(self.__flintModel)
362
        propertyWidget.setFocusWidget(self)
363
364
        return propertyWidget

365
366
367
    def flintModel(self) -> Optional[flint_model.FlintState]:
        return self.__flintModel

368
369
    def setFlintModel(self, flintModel: Optional[flint_model.FlintState]):
        self.__flintModel = flintModel
Valentin Valls's avatar
Valentin Valls committed
370
        self.__exportAction.setFlintModel(flintModel)
371
        self.__contrastAction.setFlintModel(flintModel)
372

373
374
375
376
377
        if flintModel is not None:
            if not self.__colormapInitialized:
                style = flintModel.defaultImageStyle()
                self.__colormap.setName(style.colormapLut)

378
379
    def setPlotModel(self, plotModel: plot_model.Plot):
        if self.__plotModel is not None:
380
381
382
383
384
385
            self.__plotModel.itemAdded.disconnect(
                self.__aggregator.callbackTo(self.__itemAdded)
            )
            self.__plotModel.itemRemoved.disconnect(
                self.__aggregator.callbackTo(self.__itemRemoved)
            )
386
387
388
389
390
391
392
393
394
            self.__plotModel.structureChanged.disconnect(
                self.__aggregator.callbackTo(self.__structureChanged)
            )
            self.__plotModel.itemValueChanged.disconnect(
                self.__aggregator.callbackTo(self.__itemValueChanged)
            )
            self.__plotModel.transactionFinished.disconnect(
                self.__aggregator.callbackTo(self.__transactionFinished)
            )
395
        previousPlot = self.__plotModel
396
397
        self.__plotModel = plotModel
        if self.__plotModel is not None:
398
399
400
401
402
403
            self.__plotModel.itemAdded.connect(
                self.__aggregator.callbackTo(self.__itemAdded)
            )
            self.__plotModel.itemRemoved.connect(
                self.__aggregator.callbackTo(self.__itemRemoved)
            )
404
405
406
407
408
409
410
411
412
            self.__plotModel.structureChanged.connect(
                self.__aggregator.callbackTo(self.__structureChanged)
            )
            self.__plotModel.itemValueChanged.connect(
                self.__aggregator.callbackTo(self.__itemValueChanged)
            )
            self.__plotModel.transactionFinished.connect(
                self.__aggregator.callbackTo(self.__transactionFinished)
            )
413
        self.plotModelUpdated.emit(plotModel)
414
415
416
        self.__updatePreferedRefreshRate(
            previousPlot=previousPlot, plot=self.__plotModel
        )
417
418
419
420
421
422
423
424
        self.__redrawAll()

    def plotModel(self) -> plot_model.Plot:
        return self.__plotModel

    def __structureChanged(self):
        self.__redrawAll()

425
426
427
428
429
430
    def __itemAdded(self, item):
        self.__updatePreferedRefreshRate(newItem=item)

    def __itemRemoved(self, item):
        self.__updatePreferedRefreshRate(previousItem=item)

431
432
433
    def __transactionFinished(self):
        if self.__plotWasUpdated:
            self.__plotWasUpdated = False
434
            self.__view.plotUpdated()
435
436
437
438
439
440
441
442

    def __itemValueChanged(
        self, item: plot_model.Item, eventType: plot_model.ChangeEventType
    ):
        if eventType == plot_model.ChangeEventType.VISIBILITY:
            self.__updateItem(item)
        elif eventType == plot_model.ChangeEventType.IMAGE_CHANNEL:
            self.__updateItem(item)
443
444
        elif eventType == plot_model.ChangeEventType.CUSTOM_STYLE:
            self.__updateItem(item)
445

446
447
448
    def scan(self) -> Optional[scan_model.Scan]:
        return self.__scan

449
    def setScan(self, scan: scan_model.Scan = None):
450
451
        if self.__scan is scan:
            return
452
        self.liveAction.setScan(scan)
453
        if self.__scan is not None:
454
455
456
457
458
459
460
461
462
            self.__scan.scanDataUpdated[object].disconnect(
                self.__aggregator.callbackTo(self.__scanDataUpdated)
            )
            self.__scan.scanStarted.disconnect(
                self.__aggregator.callbackTo(self.__scanStarted)
            )
            self.__scan.scanFinished.disconnect(
                self.__aggregator.callbackTo(self.__scanFinished)
            )
463
        self.__title.scanRemoved(self.__scan)
464
        previousScan = self.__scan
465
        self.__scan = scan
466
467
        # As the scan was updated, clear the previous cached events
        self.__aggregator.clear()
468
        if self.__scan is not None:
469
470
471
472
473
474
475
476
477
            self.__scan.scanDataUpdated[object].connect(
                self.__aggregator.callbackTo(self.__scanDataUpdated)
            )
            self.__scan.scanStarted.connect(
                self.__aggregator.callbackTo(self.__scanStarted)
            )
            self.__scan.scanFinished.connect(
                self.__aggregator.callbackTo(self.__scanFinished)
            )
Valentin Valls's avatar
Valentin Valls committed
478
            if self.__scan.state() != scan_model.ScanState.INITIALIZED:
479
                self.__title.scanStarted(self.__scan)
480
        self.scanModelUpdated.emit(scan)
481
482
483
484

        # Note: No redraw here to avoid blinking of the image
        # The image title is explicitly tagged as "outdated"
        # To avoid mistakes
485
        self.__updatePreferedRefreshRate(previousScan=previousScan, scan=self.__scan)
486
        self.__redrawAllIfNeeded()
487

488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
    def __refreshModeChanged(self):
        self.__updatePreferedRefreshRate()

    def __updatePreferedRefreshRate(
        self,
        previousScan: scan_model.Scan = None,
        scan: scan_model.Scan = None,
        previousPlot: plot_model.Plot = None,
        plot: plot_model.Plot = None,
        previousItem: plot_model.Item = None,
        newItem: plot_model.Item = None,
    ):
        """Propagate prefered refresh rate to the internal scan model.

        This allow the scan manager to optimize image download.

        The function deals with all the cases which can happen. Changes of the
        scan, the plot, or the items. Item visibility could also be taken into
        account.
        """

        if plot is None:
            plot = self.__plotModel
        if scan is None:
            scan = self.__scan

        key = self.objectName()

        def imageChannels(plotModel, scan):
            """Iterate through all channel scan from image items"""
            for item in plotModel.items():
                if isinstance(item, plot_item_model.ImageItem):
                    channelRef = item.imageChannel()
                    if channelRef is None:
                        continue
                    channel = channelRef.channel(scan)
                    if channel is None:
                        continue
                    yield channel

        # Remove preferences from the previous plot
        if previousPlot is not None and scan is not None:
            for channel in imageChannels(previousPlot, scan):
                channel.setPreferedRefreshRate(key, None)

        if plot is None:
            return

        # Remove preferences from the previous scan
        if previousScan is not None:
            for channel in imageChannels(plot, previousScan):
                channel.setPreferedRefreshRate(key, None)

        rate = self.__refreshManager.refreshMode()

        if scan is not None:
            # Remove preferences from the prevouos item
            if previousItem is not None:
                item = previousItem
                if isinstance(item, plot_item_model.ImageItem):
                    channelRef = item.imageChannel()
                    if channelRef is not None:
                        channel = channelRef.channel(scan)
                        if channel is not None:
                            channel.setPreferedRefreshRate(key, None)
            elif newItem is not None:
                item = newItem
                if isinstance(item, plot_item_model.ImageItem):
                    channelRef = item.imageChannel()
                    if channelRef is not None:
                        channel = channelRef.channel(scan)
                        if channel is not None:
                            channel.setPreferedRefreshRate(key, rate)
            else:
                # Update the preferences to the current plot and current scan
                for channel in imageChannels(plot, scan):
                    channel.setPreferedRefreshRate(key, rate)

566
    def __scanStarted(self):
567
        self.__imageReceived = 0
568
        self.__createScanRois()
569
570
        self.__refreshManager.scanStarted()
        self.__view.scanStarted()
571
        self.__title.scanStarted(self.__scan)
572
573

    def __scanFinished(self):
574
        self.__refreshManager.scanFinished()
575
576
        if self.__imageReceived == 0:
            self.__cleanAll()
577
        self.__title.scanFinished(self.__scan)
578

579
    def __createScanRois(self):
Valentin Valls's avatar
Valentin Valls committed
580
581
582
583
        self.__roiManager.clear()
        if self.__scan is None:
            return

584
        limaDevice = None
Valentin Valls's avatar
Valentin Valls committed
585
        for device in self.__scan.devices():
586
            if device.type() != scan_model.DeviceType.LIMA:
Valentin Valls's avatar
Valentin Valls committed
587
                continue
588
589
            if device.name() == self.deviceName():
                limaDevice = device
Valentin Valls's avatar
Valentin Valls committed
590
591
                break

592
        if limaDevice is None:
Valentin Valls's avatar
Valentin Valls committed
593
594
            return

595
        for device in limaDevice.devices():
Valentin Valls's avatar
Valentin Valls committed
596
597
598
599
600
601
602
603
604
605
606
607
608
609
            roi = device.metadata().roi
            if roi is None:
                continue

            if isinstance(roi, lima_rois.RoiProfile):
                # Must be checked first as a RoiProfile is a RectRoi
                if roi.mode == "vertical":
                    item = flint_rois.LimaVProfileRoi()
                elif roi.mode == "horizontal":
                    item = flint_rois.LimaHProfileRoi()
                else:
                    item = silx_rois.RectangleROI()
                origin = roi.x, roi.y
                size = roi.width, roi.height
610
                self.__roiManager.addRoi(item)
Valentin Valls's avatar
Valentin Valls committed
611
612
613
614
615
                item.setGeometry(origin=origin, size=size)
            elif isinstance(roi, lima_rois.Roi):
                item = silx_rois.RectangleROI()
                origin = roi.x, roi.y
                size = roi.width, roi.height
616
                self.__roiManager.addRoi(item)
Valentin Valls's avatar
Valentin Valls committed
617
618
619
620
                item.setGeometry(origin=origin, size=size)
            elif isinstance(roi, lima_rois.ArcRoi):
                item = silx_rois.ArcROI()
                center = roi.cx, roi.cy
621
                self.__roiManager.addRoi(item)
Valentin Valls's avatar
Valentin Valls committed
622
623
624
625
626
627
628
629
630
631
632
633
634
                item.setGeometry(
                    center=center,
                    innerRadius=roi.r1,
                    outerRadius=roi.r2,
                    startAngle=numpy.deg2rad(roi.a1),
                    endAngle=numpy.deg2rad(roi.a2),
                )
            else:
                item = None
            if item is not None:
                item.setName(device.name())
                item.setEditable(False)
                item.setSelectable(False)
635
                item.setColor(qt.QColor(0x80, 0x80, 0x80))
636
                item.setVisible(False)
Valentin Valls's avatar
Valentin Valls committed
637

638
639
640
641
    def __scanDataUpdated(self, event: scan_model.ScanDataUpdateEvent):
        plotModel = self.__plotModel
        if plotModel is None:
            return
642
        self.__imageReceived += 1
643
644
645
646
647
        for item in plotModel.items():
            if isinstance(item, plot_item_model.ImageItem):
                channelName = item.imageChannel().name()
                if event.isUpdatedChannelName(channelName):
                    self.__updateItem(item)
648
649
            elif isinstance(item, plot_item_model.RoiItem):
                self.__updateItem(item)
650
651
652

    def __cleanAll(self):
        for _item, itemKeys in self.__items.items():
Valentin Valls's avatar
Valentin Valls committed
653
654
            for description in itemKeys:
                self.__plot.remove(description.key, description.kind)
655
        self.__view.plotCleared()
656
657
658

    def __cleanItem(self, item: plot_model.Item):
        itemKeys = self.__items.pop(item, [])
659
660
        if len(itemKeys) == 0:
            return False
Valentin Valls's avatar
Valentin Valls committed
661
662
        for description in itemKeys:
            self.__plot.remove(description.key, description.kind)
663
        return True
664

665
666
    def __redrawAllIfNeeded(self):
        plotModel = self.__plotModel
Valentin Valls's avatar
Valentin Valls committed
667
        if plotModel is None or self.__scan is None:
668
669
670
671
672
673
674
675
676
677
678
679
680
            self.__cleanAll()
            return

        for item in plotModel.items():
            if not isinstance(item, plot_item_model.ImageItem):
                continue
            if not item.isVisible():
                continue
            data = item.imageChannel().data(self.__scan)
            if data is None:
                continue
            self.__redrawAll()

681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
    def __redrawAll(self):
        self.__cleanAll()
        plotModel = self.__plotModel
        if plotModel is None:
            return

        for item in plotModel.items():
            self.__updateItem(item)

    def __updateItem(self, item: plot_model.Item):
        if self.__plotModel is None:
            return
        if self.__scan is None:
            return
        if not item.isValid():
            return
697
698
699
700
701
702
703
704
705
706
        if isinstance(item, plot_item_model.ImageItem):
            self.__updateImageItem(item)
        elif isinstance(item, plot_item_model.RoiItem):
            roi_name = item.roiName()
            roi = [r for r in self.__roiManager.getRois() if r.getName() == roi_name]
            roi = roi[0] if len(roi) > 0 else None
            if roi is not None:
                roi.setVisible(item.isVisible())

    def __updateImageItem(self, item: plot_model.Item):
707
        scan = self.__scan
708
        plot = self.__plot
709
        plotItems: List[_ItemDescription] = []
710

711
712
713
        updateZoomNow = not self.__plotModel.isInTransaction()

        wasUpdated = self.__cleanItem(item)
714
715

        if not item.isVisible():
716
717
718
719
720
721
722
            if wasUpdated:
                self.__updatePlotZoom(updateZoomNow)
            return

        if not item.isValidInScan(scan):
            if wasUpdated:
                self.__updatePlotZoom(updateZoomNow)
723
724
725
726
727
728
729
730
            return

        dataChannel = item.imageChannel()
        if dataChannel is None:
            self.__cleanItem(item)
            return
        image = dataChannel.array(self.__scan)
        if image is None:
731
732
            if wasUpdated:
                self.__updatePlotZoom(updateZoomNow)
733
734
735
736
            return

        legend = dataChannel.name()
        style = item.getStyle(self.__scan)
737
        colormap = model_helper.getColormapFromItem(item, style, self.__colormap)
738

739
740
741
742
743
744
        live = self.flintModel().liveWindow()
        if live is not None:
            colormapWidget = live.ownedColormapWidget(self)
        else:
            colormapWidget = None

Valentin Valls's avatar
Valentin Valls committed
745
        if style.symbolStyle is style_model.SymbolStyle.NO_SYMBOL:
746
747
            if image.ndim == 3:
                imageItem = plot_helper.FlintImageRgba()
748
749
                if colormapWidget is not None:
                    colormapWidget.setItem(None)
750
751
752
            else:
                imageItem = plot_helper.FlintImage()
                imageItem.setColormap(colormap)
753
754
                if colormapWidget is not None:
                    colormapWidget.setItem(imageItem)
755
756
            imageItem.setData(image, copy=False)
            imageItem.setCustomItem(item)
757
            imageItem.setScan(scan)
Valentin Valls's avatar
Valentin Valls committed
758
759
            imageItem.setName(legend)
            self.__plot.addItem(imageItem)
760
761
762

            self.__plot._setActiveItem("image", legend)
            plotItems.append(_ItemDescription(legend, "image", image.shape))
763
            self.__title.itemUpdated(scan, item)
764
765
766
767
768
769
770
771
772

            bottom, left = 0, 0
            height, width = image.shape[0], image.shape[1]
            self.__minMarker.setPosition(0, 0)
            self.__maxMarker.setText(f"{left}, {bottom}")
            self.__minMarker.setVisible(True)
            self.__maxMarker.setPosition(width, height)
            self.__maxMarker.setText(f"{width}, {height}")
            self.__maxMarker.setVisible(True)
773
774
775
776
777
778
779
780
781
782
        else:
            yy = numpy.atleast_2d(numpy.arange(image.shape[0])).T
            xx = numpy.atleast_2d(numpy.arange(image.shape[1]))
            xx = xx * numpy.atleast_2d(numpy.ones(image.shape[0])).T + 0.5
            yy = yy * numpy.atleast_2d(numpy.ones(image.shape[1])) + 0.5
            image, xx, yy = image.reshape(-1), xx.reshape(-1), yy.reshape(-1)
            key = plot.addScatter(
                x=xx, y=yy, value=image, legend=legend, colormap=colormap
            )
            scatter = plot.getScatter(key)
Valentin Valls's avatar
Valentin Valls committed
783
784
785
786
            symbolStyle = style_model.symbol_to_silx(style.symbolStyle)
            if symbolStyle == " ":
                symbolStyle = "o"
            scatter.setSymbol(symbolStyle)
787
            scatter.setSymbolSize(style.symbolSize)
788
789
            plotItems.append(_ItemDescription(key, "scatter", image.shape))

790
        self.__items[item] = plotItems
791
        self.__updatePlotZoom(updateZoomNow)
792

793
794
795
    def __updatePlotZoom(self, updateZoomNow):
        if updateZoomNow:
            self.__view.plotUpdated()
796
797
        else:
            self.__plotWasUpdated = True