From a8f6b65d70aa5ca8387ba8d5b81e6a07c5b25cb1 Mon Sep 17 00:00:00 2001 From: bennahugo Date: Tue, 26 Jul 2022 16:15:31 +0200 Subject: [PATCH 01/28] Refactor profiles to clean up SkyModelPlot --- TigGUI/Plot/SkyModelPlot.py | 1141 +------------------------- TigGUI/Widgets.py | 4 +- TigGUI/kitties/plottable_profiles.py | 94 --- TigGUI/kitties/profiles.py | 21 + 4 files changed, 37 insertions(+), 1223 deletions(-) delete mode 100644 TigGUI/kitties/plottable_profiles.py diff --git a/TigGUI/Plot/SkyModelPlot.py b/TigGUI/Plot/SkyModelPlot.py index 3760736..18e2a76 100644 --- a/TigGUI/Plot/SkyModelPlot.py +++ b/TigGUI/Plot/SkyModelPlot.py @@ -24,43 +24,43 @@ import re import time -from PyQt5 import QtGui -from PyQt5.Qt import (QAction, QActionGroup, QApplication, QBrush, QCheckBox, - QClipboard, QColor, QComboBox, QCoreApplication, QDialog, +from PyQt5.Qt import (QActionGroup, QApplication, QBrush, + QClipboard, QColor, QCoreApplication, QDialog, QEvent, QFileDialog, QHBoxLayout, QImage, QInputDialog, - QLabel, QMenu, QMessageBox, QPainter, QPen, QPixmap, + QMenu, QMessageBox, QPainter, QPen, QPixmap, QPoint, QPointF, QRectF, QSize, QSizePolicy, QTimer, - QToolBar, QToolButton, QTransform, QVBoxLayout, QWidget, QPushButton, - QGridLayout, QColorDialog) + QToolBar, QWidget) from PyQt5.QtCore import Qt from PyQt5.QtCore import pyqtSignal -from PyQt5.QtWidgets import QDockWidget, QLayout +from PyQt5.QtWidgets import QDockWidget from PyQt5.Qwt import (QwtEventPattern, QwtPicker, QwtPickerClickPointMachine, QwtPickerClickRectMachine, QwtPickerDragLineMachine, QwtPickerDragRectMachine, QwtPickerTrackerMachine, QwtPlot, QwtPlotCurve, QwtPlotItem, QwtPlotPicker, - QwtPlotZoomer, QwtScaleEngine, QwtSymbol, QwtText, - QwtLegend) + QwtPlotZoomer, QwtScaleEngine, QwtSymbol, QwtText) + +from TigGUI.Plot.ToolDialogs import (LiveImageZoom, + LiveProfile, SelectedProfile) from TigGUI.Images.ControlDialog import ImageControlDialog from TigGUI.Plot import MouseModes from TigGUI.Widgets import (TDockWidget, TigToolTip, TiggerPlotCurve, TiggerPlotMarker) from TigGUI.init import Config, pixmaps -from TigGUI.kitties.plottable_profiles import PlottableTiggerProfile +from TigGUI.Plot.plottableProfiles import PlottableTiggerProfile import TigGUI.kitties.utils from TigGUI.kitties.utils import PersistentCurrier, curry from TigGUI.kitties.widgets import BusyIndicator - +from TigGUI.Plot.Utils import makeSourceMarker, makeDualColorPen from Tigger import Coordinates from Tigger.Coordinates import Projection from Tigger.Models import ModelClasses from Tigger.Models.SkyModel import SkyModel - -from TigGUI.kitties.profiles import TiggerProfile, TiggerProfileFactory +from TigGUI.Plot.Utils import (Z_CurrentSource, Z_Grid, Z_Image, Z_Markup, + Z_SelectedSource, Z_MarkupOverlays, Z_Source, DefaultGridStep_ArcSec, + DEG) import numpy -import os QStringList = list @@ -68,1119 +68,6 @@ dprint = _verbosity.dprint dprintf = _verbosity.dprintf - -# plot Z depths for various classes of objects -Z_Image = 1000 -Z_Grid = 9000 -Z_Source = 10000 -Z_SelectedSource = 10001 -Z_CurrentSource = 10002 -Z_Markup = 10010 -Z_MarkupOverlays = 10011 - -# default stepping of grid circles -DefaultGridStep_ArcSec = 30 * 60 - -DEG = math.pi / 180 - - -class SourceMarker: - """SourceMarker implements a source marker corresponding to a SkyModel source. - The base class implements a marker at the centre. - """ - QwtSymbolStyles = dict(none=QwtSymbol.NoSymbol, - cross=QwtSymbol.XCross, - plus=QwtSymbol.Cross, - dot=QwtSymbol.Ellipse, - circle=QwtSymbol.Ellipse, - square=QwtSymbol.Rect, - diamond=QwtSymbol.Diamond, - triangle=QwtSymbol.Triangle, - dtriangle=QwtSymbol.DTriangle, - utriangle=QwtSymbol.UTriangle, - ltriangle=QwtSymbol.LTriangle, - rtriangle=QwtSymbol.RTriangle, - hline=QwtSymbol.HLine, - vline=QwtSymbol.VLine, - star1=QwtSymbol.Star1, - star2=QwtSymbol.Star2, - hexagon=QwtSymbol.Hexagon) - - def __init__(self, src, l, m, size, model): - self.src = src - self._lm, self._size = (l, m), size - self.plotmarker = TiggerPlotMarker() - self.plotmarker.setRenderHint(QwtPlotItem.RenderAntialiased) - self.plotmarker.setValue(l, m) - self._symbol = QwtSymbol() - self._font = QApplication.font() - self._model = model - self.resetStyle() - - def lm(self): - """Returns plot coordinates of marker, as an l,m tuple""" - return self._lm - - def lmQPointF(self): - """Returns plot coordinates of marker, as a QPointF""" - return self.plotmarker.value() - - def source(self): - """Returns model source associated with marker""" - return self.src - - def attach(self, plot): - """Attaches to plot""" - self.plotmarker.attach(plot) - - def isVisible(self): - return self.plotmarker.isVisible() - - def setZ(self, z): - self.plotmarker.setZ(z) - - def resetStyle(self): - """Sets the source style based on current model settings""" - self.style, self.label = self._model.getSourcePlotStyle(self.src) - self._selected = getattr(self.src, 'selected', False) - # setup marker components - self._setupMarker(self.style, self.label) - # setup depth - if self._model.currentSource() is self.src: - self.setZ(Z_CurrentSource) - elif self._selected: - self.setZ(Z_SelectedSource) - else: - self.setZ(Z_Source) - - def _setupMarker(self, style, label): - """Sets up the plot marker (self.plotmarker) based on style object and label string. - If style=None, makes marker invisible.""" - if not style: - self.plotmarker.setVisible(False) - return - self.plotmarker.setVisible(True) - self._symbol.setStyle(self.QwtSymbolStyles.get(style.symbol, QwtSymbol.Cross)) - self._font.setPointSize(style.label_size) - symbol_color = QColor(style.symbol_color) - label_color = QColor(style.label_color) - # dots have a fixed size - if style.symbol == "dot": - self._symbol.setSize(2) - else: - self._symbol.setSize(int(self._size)) - self._symbol.setPen(QPen(symbol_color, style.symbol_linewidth)) - self._symbol.setBrush(QBrush(Qt.NoBrush)) - lab_pen = QPen(Qt.NoPen) - lab_brush = QBrush(Qt.NoBrush) - self._label = label or "" - self.plotmarker.setSymbol(self._symbol) - txt = QwtText(self._label) - txt.setColor(label_color) - txt.setFont(self._font) - txt.setBorderPen(lab_pen) - txt.setBackgroundBrush(lab_brush) - self.plotmarker.setLabel(txt) - self.plotmarker.setLabelAlignment(Qt.AlignBottom | Qt.AlignRight) - - def checkSelected(self): - """Checks the src.selected attribute, resets marker if it has changed. - Returns True is something has changed.""" - sel = getattr(self.src, 'selected', False) - if self._selected == sel: - return False - self._selected = sel - self.resetStyle() - return True - - def changeStyle(self, group): - if group.func(self.src): - self.resetStyle() - return True - return False - - -class ImageSourceMarker(SourceMarker): - """This auguments SourceMarker with a FITS image.""" - - def __init__(self, src, l, m, size, model, imgman): - # load image if needed - self.imgman = imgman - dprint(2, "loading Image source", src.shape.filename) - self.imagecon = imgman.loadImage(src.shape.filename, duplicate=False, to_top=False, model=src.name) - # this will return None if the image fails to load, in which case we still produce a marker, - # but nothing else - if self.imagecon: - self.imagecon.setMarkersZ(Z_Source) - # init base class - SourceMarker.__init__(self, src, l, m, size, model) - - def attach(self, plot): - SourceMarker.attach(self, plot) - if self.imagecon: - self.imagecon.attachToPlot(plot) - - def _setupMarker(self, style, label): - SourceMarker._setupMarker(self, style, label) - if not style: - return - symbol_color = QColor(style.symbol_color) - label_color = QColor(style.label_color) - if self.imagecon: - self.imagecon.setPlotBorderStyle(border_color=symbol_color, label_color=label_color) - - -def makeSourceMarker(src, l, m, size, model, imgman): - """Creates source marker based on source type""" - shape = getattr(src, 'shape', None) - # print type(shape),isinstance(shape,ModelClasses.FITSImage),shape.__class__,ModelClasses.FITSImage - if isinstance(shape, ModelClasses.FITSImage): - return ImageSourceMarker(src, l, m, size, model, imgman) - else: - return SourceMarker(src, l, m, size, model) - - -def makeDualColorPen(color1, color2, width=3): - c1, c2 = QColor(color1).rgb(), QColor(color2).rgb() - texture = QImage(2, 2, QImage.Format_RGB32) - texture.setPixel(0, 0, c1) - texture.setPixel(1, 1, c1) - texture.setPixel(0, 1, c2) - texture.setPixel(1, 0, c2) - return QPen(QBrush(texture), width) - - -class ToolDialog(QDialog): - signalIsVisible = pyqtSignal(bool) - - def __init__(self, parent, mainwin, configname, menuname, show_shortcut=None): - QDialog.__init__(self, parent) - self.setModal(False) - self.setFocusPolicy(Qt.NoFocus) - self.mainwin = mainwin - self.hide() - self._configname = configname - self._geometry = None - # make hide/show qaction - self._qa_show = qa = QAction("Show %s" % menuname.replace("&", "&&"), self) - if show_shortcut: - qa.setShortcut(show_shortcut) - qa.setCheckable(True) - qa.setChecked(Config.getbool("%s-show" % configname, False)) - qa.setVisible(False) - qa.setToolTip("""

The quick zoom & cross-sections window shows a zoom of the current image area - under the mose pointer, and X/Y cross-sections through that area.

""") - qa.triggered[bool].connect(self.setVisible) - self._closing = False - self._write_config = curry(Config.set, "%s-show" % configname) - qa.triggered[bool].connect(self._write_config) - self.signalIsVisible.connect(qa.setChecked) - - def getShowQAction(self): - return self._qa_show - - def makeAvailable(self, available=True): - """Makes the tool available (or unavailable)-- shows/hides the "show" control, - and shows/hides the dialog according to this control.""" - self._qa_show.setVisible(available) - self.setVisible(self._qa_show.isChecked() if available else False) - - def initGeometry(self): - x0 = Config.getint('%s-x0' % self._configname, 0) - y0 = Config.getint('%s-y0' % self._configname, 0) - w = Config.getint('%s-width' % self._configname, 0) - h = Config.getint('%s-height' % self._configname, 0) - if w and h: - self.resize(w, h) - self.move(x0, y0) - return True - return False - - def _saveGeometry(self): - Config.set('%s-x0' % self._configname, self.pos().x()) - Config.set('%s-y0' % self._configname, self.pos().y()) - Config.set('%s-width' % self._configname, self.width()) - Config.set('%s-height' % self._configname, self.height()) - - def close(self): - self._closing = True - QDialog.close(self) - - def closeEvent(self, event): - QDialog.closeEvent(self, event) - if not self._closing: - self._write_config(False) - - def moveEvent(self, event): - self._saveGeometry() - QDialog.moveEvent(self, event) - - def resizeEvent(self, event): - self._saveGeometry() - QDialog.resizeEvent(self, event) - - def setVisible(self, visible, emit=True): - if not visible: - self._geometry = self.geometry() - else: - if self._geometry: - self.setGeometry(self._geometry) - if emit: - self.signalIsVisible.emit(visible) - QDialog.setVisible(self, visible) - # This section aligns the dockwidget with its subqwidget's visibility - if visible and not self.parent().isVisible(): - self.parent().setGeometry(self.geometry()) - self.parent().setVisible(True) - _area = self.mainwin.dockWidgetArea(self.parent()) # in right dock area - if self.mainwin.windowState() != Qt.WindowMaximized: - if not self.get_docked_widget_size(self.parent(), _area): - geo = self.mainwin.geometry() - geo.setWidth(self.mainwin.width() + self.parent().width()) - center = geo.center() - if self.mainwin.dockWidgetArea(self.parent()) == 2: # in right dock area - geo.moveCenter(QPoint(center.x() + self.parent().width(), geo.y())) - elif self.mainwin.dockWidgetArea(self.parent()) == 1: - geo.moveCenter(QPoint(center.x() - self.width(), geo.y())) - self.mainwin.setGeometry(geo) - if _area == 2 and isinstance(self.parent().bind_widget, TigGUI.Plot.SkyModelPlot.LiveImageZoom): - self.mainwin.addDockWidgetToArea(self.parent(), _area) - else: - self.mainwin.addDockWidgetToArea(self.parent(), _area) - elif not visible and self.parent().isVisible(): - _area = self.mainwin.dockWidgetArea(self.parent()) # in right dock area - if self.mainwin.windowState() != Qt.WindowMaximized: - if not self.get_docked_widget_size(self.parent(), _area): - geo = self.mainwin.geometry() - geo.setWidth(self.mainwin.width() - self.parent().width()) - center = geo.center() - if self.mainwin.dockWidgetArea(self.parent()) == 1: # in left dock area - geo.moveCenter(QPoint(center.x() + self.parent().width(), geo.y())) - self.mainwin.setGeometry(geo) - self.parent().setVisible(False) - self.mainwin.restoreDockArea(_area) - - def get_docked_widget_size(self, _dockable, _area): - widget_list = self.mainwin.findChildren(QDockWidget) - size_list = [] - if _dockable: - for widget in widget_list: - if self.mainwin.dockWidgetArea(widget) == _area: - if widget is not _dockable: - if (not widget.isWindow() and not widget.isFloating() - and widget.isVisible()): - size_list.append(widget.bind_widget.width()) - if size_list: - return max(size_list) - else: - return size_list - - -class LiveImageZoom(ToolDialog): - livezoom_resize_signal = pyqtSignal(QSize) - - def __init__(self, parent, mainwin, radius=10, factor=12): - ToolDialog.__init__(self, parent, mainwin, configname="livezoom", menuname="live zoom & cross-sections", - show_shortcut=Qt.Key_F2) - self.setWindowTitle("Zoom & Cross-sections") - radius = Config.getint("livezoom-radius", radius) - # create size polixy for livezoom - livezoom_policy = QSizePolicy() - livezoom_policy.setWidthForHeight(True) - livezoom_policy.setHeightForWidth(True) - self.setSizePolicy(livezoom_policy) - # add plots - self._lo0 = lo0 = QVBoxLayout(self) - self._lo0.setSizeConstraint(QLayout.SetFixedSize) - lo1 = QHBoxLayout() - lo1.setContentsMargins(0, 0, 0, 0) - lo1.setSpacing(0) - lo0.addLayout(lo1) - # control checkboxes - self._showzoom = QCheckBox("show zoom", self) - self._showcs = QCheckBox("show cross-sections", self) - self._showzoom.setChecked(True) - self._showcs.setChecked(True) - self._showzoom.toggled[bool].connect(self._showZoom) - self._showcs.toggled[bool].connect(self._showCrossSections) - lo1.addWidget(self._showzoom, 0) - lo1.addSpacing(5) - lo1.addWidget(self._showcs, 0) - lo1.addStretch(1) - self._smaller = QToolButton(self) - self._smaller.setIcon(pixmaps.window_smaller.icon()) - self._smaller.clicked.connect(self._shrink) - self._larger = QToolButton(self) - self._larger.setIcon(pixmaps.window_larger.icon()) - self._larger.clicked.connect(self._enlarge) - lo1.addWidget(self._smaller) - lo1.addWidget(self._larger) - self._has_zoom = self._has_xcs = self._has_ycs = False - # setup zoom plot - font = QApplication.font() - font.setPointSize(8) - axis_font = QApplication.font() - axis_font.setBold(True) - axis_font.setPointSize(10) - self._zoomplot = QwtPlot(self) - self._zoomplot.setContentsMargins(5, 5, 5, 5) - axes = {QwtPlot.xBottom: "X pixel coordinate", - QwtPlot.yLeft: "Y pixel coordinate", - QwtPlot.xTop: "X cross-section value", - QwtPlot.yRight: "Y cross-section value"} - for axis, title in axes.items(): - self._zoomplot.enableAxis(True) - self._zoomplot.setAxisScale(axis, 0, 1) - self._zoomplot.setAxisFont(axis, font) - self._zoomplot.setAxisMaxMajor(axis, 3) - self._zoomplot.axisWidget(axis).setMinBorderDist(5, 5) - self._zoomplot.axisWidget(axis).show() - text = QwtText(title) - text.setFont(font) - self._zoomplot.axisWidget(axis).setTitle(text.text()) - axis_text = QwtText(title) - axis_text.setFont(axis_font) - self._zoomplot.setAxisTitle(axis, axis_text) - self._zoomplot.setAxisLabelRotation(QwtPlot.yLeft, -90) - self._zoomplot.setAxisLabelAlignment(QwtPlot.yLeft, Qt.AlignVCenter) - self._zoomplot.setAxisLabelRotation(QwtPlot.yRight, 90) - self._zoomplot.setAxisLabelAlignment(QwtPlot.yRight, Qt.AlignVCenter) - # self._zoomplot.plotLayout().setAlignCanvasToScales(True) - lo0.addWidget(self._zoomplot, 0) - # setup ZoomItem for zoom plot - self._zi = self.ImageItem() - self._zi.attach(self._zoomplot) - self._zi.setZ(0) - # setup targeting reticule for zoom plot - self._reticules = TiggerPlotCurve(), TiggerPlotCurve() - for curve in self._reticules: - curve.setRenderHint(QwtPlotItem.RenderAntialiased) - curve.setPen(QPen(QColor("green"))) - curve.setStyle(QwtPlotCurve.Lines) - curve.attach(self._zoomplot) - curve.setZ(1) - # setup cross-section curves - self._xcs = TiggerPlotCurve() - self._xcs.setRenderHint(QwtPlotItem.RenderAntialiased) - self._ycs = TiggerPlotCurve() - self._ycs.setRenderHint(QwtPlotItem.RenderAntialiased) - self._xcs.setPen(makeDualColorPen("navy", "yellow")) - self._ycs.setPen(makeDualColorPen("black", "cyan")) - for curve in self._xcs, self._ycs: - curve.setStyle(QwtPlotCurve.Steps) - curve.attach(self._zoomplot) - curve.setZ(2) - self._xcs.setXAxis(QwtPlot.xBottom) - self._xcs.setYAxis(QwtPlot.yRight) - self._ycs.setXAxis(QwtPlot.xTop) - self._ycs.setYAxis(QwtPlot.yLeft) - # self._ycs.setCurveType(QwtPlotCurve.Xfy) # old qwt5 - self._ycs.setOrientation(Qt.Vertical) # Qwt 6 version - self._xcs.setOrientation(Qt.Horizontal) # Qwt 6 version - # make QTransform for flipping images upside-down - self._xform = QTransform() - self._xform.scale(1, -1) - # init geometry - self.setPlotSize(radius, factor) - self.initGeometry() - - def _showZoom(self, show): - if not show: - self._zi.setVisible(False) - - def _showCrossSections(self, show): - self._zoomplot.enableAxis(QwtPlot.xTop, show) - self._zoomplot.enableAxis(QwtPlot.yRight, show) - if not show: - self._xcs.setVisible(False) - self._ycs.setVisible(False) - - def _enlarge(self): - self.setPlotSize(int(self._radius * 2), self._magfac) - - def _shrink(self): - self.setPlotSize(int(self._radius / 2), self._magfac) - - def setPlotSize(self, radius, factor): - Config.set('livezoom-radius', radius) - self._radius = radius - # enable smaller/larger buttons based on radius - self._smaller.setEnabled(radius > 5) - self._larger.setEnabled(radius < 40) - # compute other sizes - self._npix = radius * 2 + 1 - self._magfac = factor - width = height = self._npix * self._magfac - self._zoomplot.setMinimumHeight(height + 80) - self._zoomplot.setMinimumWidth(width + 80) - # set data array - self._data = numpy.ma.masked_array(numpy.zeros((self._npix, self._npix), float), - numpy.zeros((self._npix, self._npix), bool)) - # reset window size - self._lo0.update() - self.resize(self._lo0.minimumSize()) - self.livezoom_resize_signal.emit(self._lo0.minimumSize()) - - def _getZoomSlice(self, ix, nx): - ix0, ix1 = ix - self._radius, ix + self._radius + 1 - zx0 = -min(ix0, 0) - ix0 = max(ix0, 0) - zx1 = self._npix - max(ix1, nx - 1) + (nx - 1) - ix1 = min(ix1, nx - 1) - return ix0, ix1, zx0, zx1 - - class ImageItem(QwtPlotItem): - """ImageItem subclass used by LiveZoomer to display zoomed-in images""" - - def __init__(self): - QwtPlotItem.__init__(self) - self._qimg = None - self.RenderAntialiased - - def setImage(self, qimg): - self._qimg = qimg - - def draw(self, painter, xmap, ymap, rect): - """Implements QwtPlotItem.draw(), to render the image on the given painter.""" - # drawImage expects QRectF - self._qimg and painter.drawImage(QRectF(xmap.p1(), ymap.p2(), xmap.pDist(), ymap.pDist()), self._qimg) - - def trackImage(self, image, ix, iy): - if not self.isVisible(): - return - # update zoomed image - # find overlap of zoom window with image, mask invisible pixels - nx, ny = image.imageDims() - ix0, ix1, zx0, zx1 = self._getZoomSlice(ix, nx) - iy0, iy1, zy0, zy1 = self._getZoomSlice(iy, ny) - if ix0 < nx and ix1 >= 0 and iy0 < ny and iy1 >= 0: - if self._showzoom.isChecked(): - # There was an error here when using zoom window zoom buttons - # (TypeError: slice indices must be integers or None or have an __index__ method). - # Therefore indexes have been cast as int() - # 16/05/2022: the error no longer occurs, therefore code has been reverted. - self._data.mask[...] = False - self._data.mask[:zx0, ...] = True - self._data.mask[zx1:, ...] = True - self._data.mask[..., :zy0] = True - self._data.mask[..., zy1:] = True - # copy & colorize region - self._data[zx0:zx1, zy0:zy1] = image.image()[ix0:ix1, iy0:iy1] - intensity = image.intensityMap().remap(self._data) - self._zi.setImage( - image.colorMap().colorize(image.intensityMap().remap(self._data)).transformed(self._xform)) - self._zi.setVisible(True) - # set cross-sections - if self._showcs.isChecked(): - if iy >= 0 and iy < ny and ix1 > ix0: - # added fix for masked arrays and mosaic images - xcs = [float(x) for x in numpy.ma.filled(image.image()[ix0:ix1, iy], fill_value=0.0)] - self._xcs.setData(numpy.arange(ix0 - 1, ix1) + .5, [xcs[0]] + xcs) - self._xcs.setVisible(True) - self._zoomplot.setAxisAutoScale(QwtPlot.yRight) - self._has_xcs = True - else: - self._xcs.setVisible(False) - self._zoomplot.setAxisScale(QwtPlot.yRight, 0, 1) - if ix >= 0 and ix < nx and iy1 > iy0: - # added fix for masked arrays and mosaic images - ycs = [float(y) for y in numpy.ma.filled(image.image()[ix, iy0:iy1], fill_value=0.0)] - self._ycs.setData([ycs[0]] + ycs, numpy.arange(iy0 - 1, iy1) + .5) - self._ycs.setVisible(True) - self._zoomplot.setAxisAutoScale(QwtPlot.xTop) - self._has_ycs = True - else: - self._ycs.setVisible(False) - self._zoomplot.setAxisScale(QwtPlot.xTop, 0, 1) - else: - for plotitem in self._zi, self._xcs, self._ycs: - plotitem.setVisible(False) - # set zoom plot scales - x0, x1 = ix - self._radius - .5, ix + self._radius + .5 - y0, y1 = iy - self._radius - .5, iy + self._radius + .5 - self._reticules[0].setData([ix, ix], [y0, y1]) - self._reticules[1].setData([x0, x1], [iy, iy]) - self._zoomplot.setAxisScale(QwtPlot.xBottom, x0, x1) - self._zoomplot.setAxisScale(QwtPlot.yLeft, y0, y1) - self._zoomplot.enableAxis(QwtPlot.xTop, self._showcs.isChecked()) - # update plots - self._zoomplot.replot() - - -class LiveProfile(ToolDialog): - def __init__(self, parent, mainwin, configname="liveprofile", menuname="profiles", show_shortcut=Qt.Key_F3): - ToolDialog.__init__(self, parent, mainwin, configname=configname, menuname=menuname, show_shortcut=show_shortcut) - self.setWindowTitle("Profiles") - self._profplot = None - self._setupLayout() - self._axes = [] - self._lastsel = None - self._image_id = None - self._image_hnd = None - self._last_x = None - self._last_y = None - self._parent_picker = None - self._last_data_x = None - self._last_data_y = None - self._selaxis = None - - def _setupAxisSelectorLayout(self, lo1): - lo1.setContentsMargins(0, 0, 0, 0) - lab = QLabel("Axis: ", self) - self._wprofile_axis = QComboBox(self) - self._wprofile_axis.activated[int].connect(self.selectAxis) - lo1.addWidget(lab, 0) - lo1.addWidget(self._wprofile_axis, 0) - lo1.addStretch(1) - - def _setupPlot(self): - lo0 = self._lo0 - liveprofile_policy = self._liveprofile_policy - self._font = font = QApplication.font() - - # detach and release plots if already initialized - if self._profplot is not None: - self._profcurve.setData([0, 0], [0, 0]) - self._profcurve.setVisible(True) - self._profplot.replot() - self._profplot.setMaximumHeight(256) - self._profplot.setMinimumHeight(256) - else: - self._profplot = QwtPlot(self) - self._profplot.setContentsMargins(0, 0, 0, 0) - self._profplot.enableAxis(QwtPlot.xBottom) - self._profplot.enableAxis(QwtPlot.yLeft) - self._profplot.setAxisFont(QwtPlot.xBottom, font) - self._profplot.setAxisFont(QwtPlot.yLeft, font) - # self._profplot.setAxisMaxMajor(QwtPlot.xBottom,3) - self._profplot.setAxisAutoScale(QwtPlot.yLeft) - self._profplot.setAxisMaxMajor(QwtPlot.yLeft, 3) - self._profplot.axisWidget(QwtPlot.yLeft).setMinBorderDist(16, 16) - self._profplot.setAxisLabelRotation(QwtPlot.yLeft, -90) - self._profplot.setAxisLabelAlignment(QwtPlot.yLeft, Qt.AlignVCenter) - self._profplot.plotLayout().setAlignCanvasToScales(True) - self._profplot.setMaximumHeight(256) - self._profplot.setMinimumHeight(56) - # self._profplot.setMinimumWidth(256) - # self._profplot.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Minimum) - self._profplot.setSizePolicy(liveprofile_policy) - lo0.addWidget(self._profplot, 0) - # and new profile curve - self._profcurve = TiggerPlotCurve("Active") - self._profcurve.setRenderHint(QwtPlotItem.RenderAntialiased) - self._ycs = TiggerPlotCurve() - self._ycs.setRenderHint(QwtPlotItem.RenderAntialiased) - self._profcurve.setPen(QPen(QColor("white"))) - self._profcurve.setStyle(QwtPlotCurve.Lines) - self._profcurve.setOrientation(Qt.Horizontal) - self._profcurve.attach(self._profplot) - - def _setupLayout(self): - # create size policy for live profile - liveprofile_policy = QSizePolicy() - liveprofile_policy.setHorizontalPolicy(QSizePolicy.MinimumExpanding) - liveprofile_policy.setVerticalPolicy(QSizePolicy.Fixed) - self._liveprofile_policy = liveprofile_policy - self.setSizePolicy(liveprofile_policy) - # add plots - lo0 = QVBoxLayout(self) - lo0.setSpacing(0) - self._lo0 = lo0 - - lo1 = QHBoxLayout() - lo1.setContentsMargins(0, 0, 0, 0) - lo0.addLayout(lo1) - self._setupAxisSelectorLayout(lo1) - # add profile plot - self._setupPlot() - # config geometry - if not self.initGeometry(): - self.resize(300, 192) - - def setImage(self, image, force_repopulate=False): - if image is None: - return - if id(image) == self._image_id and not force_repopulate: - return - self._image_id = id(image) - self._image_hnd = image - - # build list of axes -- first X and Y - self._axes = [] - for n, label in enumerate(("X", "Y")): - iaxis, np = image.getSkyAxis(n) - self._axes.append((label, iaxis, list(range(np)), "pixels")) - self._xaxis = self._axes[0][1] - self._yaxis = self._axes[1][1] - # then, extra axes - for i in range(image.numExtraAxes()): - iaxis, name, labels = image.extraAxisNumberNameLabels(i) - if len(labels) > 1 and name.upper() not in ("STOKES", "COMPLEX"): - values = image.extraAxisValues(i) - unit, scale = image.extraAxisUnitScale(i) - self._axes.append((name, iaxis, [x / scale for x in values], unit)) - # put them into the selector - names = [name for name, iaxis, vals, unit in self._axes] - self._wprofile_axis.addItems(names) - if self._lastsel in names: - axis = names.index(self._lastsel) - elif len(self._axes) > 2: - axis = 2 - else: - axis = 0 - self._wprofile_axis.setCurrentIndex(axis) - self.selectAxis(axis, remember=False) - - def selectAxis(self, i, remember=True): - if i is None: - return - if i < len(self._axes): - name, iaxis, values, unit = self._axes[i] - self._selaxis = iaxis, values - self._profplot.setAxisScale(QwtPlot.xBottom, min(values), max(values)) - title = QwtText("%s, %s" % (name, unit) if unit else name) - title.setFont(self._font) - self._profplot.setAxisTitle(QwtPlot.xBottom, title) - # save selection - if remember: - self._lastsel = name - - def trackImage(self, image, ix, iy, il, im): - if not self.isVisible(): - return - if ix is None or iy is None: - return - - nx, ny = image.imageDims() - inrange = ix < nx and ix >= 0 and iy < ny and iy >= 0 - if inrange: - # check if image has changed - self.setImage(image) - # make profile slice - iaxis, xval = self._selaxis - slicer = image.currentSlice() - slicer[self._xaxis] = ix - slicer[self._yaxis] = iy - slicer[iaxis] = slice(None) - yval = image.data()[tuple(slicer)] - i0, i1 = 0, len(xval) - # if X or Y profile, set axis scale to match that of window - if iaxis == 0: - rect = image.currentRectPix() - i0 = rect.topLeft().x() - i1 = i0 + rect.width() - self._profplot.setAxisScale(QwtPlot.xBottom, xval[i0], xval[i1 - 1]) - elif iaxis == 1: - rect = image.currentRectPix() - i0 = rect.topLeft().y() - i1 = i0 + rect.height() - self._profplot.setAxisScale(QwtPlot.xBottom, xval[i0], xval[i1 - 1]) - # added fix for masked arrays and mosaic images - yval = numpy.ma.filled(yval[i0:i1], fill_value=0.0) - xval = numpy.ma.filled(xval[i0:i1], fill_value=0.0) - self._profcurve.setData(xval, yval) - # store the data slice for the last pixel coordinate - self._last_data_x = xval - self._last_data_y = yval - # store profile last update coordinates - self._last_x = ix - self._last_y = iy - self._last_l = il - self._last_m = im - self._profcurve.setVisible(inrange) - # update plots - self._profplot.replot() - - -class SelectedProfile(LiveProfile): - """ 'Freezed' profile showing profile for axis at selected cube pierce point """ - def __init__(self, - parent, - mainwin, - configname="liveprofile", - menuname="profiles", - show_shortcut=Qt.Key_F4, - picker_parent=None): - self.profiles_info = {} - self._legend = None - self._numprofiles = 0 - self._currentprofile = 0 - self._parent_picker = None - self._current_profile_name = None - self._export_profile_dialog = None - self._load_profile_dialog = None - self._overlay_static_profiles = None - self._lastxmin = None - self._lastxmax = None - self._lastxtitle = None - LiveProfile.__init__(self, parent, mainwin, configname, menuname, show_shortcut) - self.addProfile() - self._parent_picker = picker_parent - - def _setupAxisSelectorLayout(self, lo1): - """ Adds controls for freeze pane profile dialog """ - lo2 = QGridLayout() - self._menu = QMenu("Selected Profile", self) - - def __inputNewName(): - text, ok = QInputDialog.getText( - self, - "Set profile name", - "

Enter new name for profile:

", - text=self._current_profile_name) - if text: - self.setProfileName(text) - self._menu.addAction("Clear profile", self.clearProfile) - self._menu.addAction("Set profile name", __inputNewName) - self._menu.addAction("Save active profile as", self.saveProfile) - self._menu.addAction("Overlay TigProf static profile from file", self.loadProfile) - self._menu_opt_paste = self._menu.addAction("Overlay another active profile as static profile", self.pasteActiveProfileAsStatic) - self._profile_ctrl_btn = QToolButton() - self._profile_ctrl_btn.setMenu(self._menu) - self._profile_ctrl_btn.setToolTip("

Click to show options for this profile

") - self._profile_ctrl_btn.setIcon(pixmaps.raise_up.icon()) - lo2.addWidget(self._profile_ctrl_btn, 0, 0, 1, 1) - - lab = QLabel("Selected profile: ") - lo2.addWidget(lab, 0, 1, 1, 1) - self._static_profile_select = QComboBox(self) - lo2.addWidget(self._static_profile_select, 0, 2, 1, 1) - - self._static_profile_select.activated[int].connect(self.selectProfile) - self._add_profile_btn = QToolButton() - self._add_profile_btn.setIcon(pixmaps.big_plus.icon()) - self._add_profile_btn.setToolTip("

Click to add another freezed profile

") - lo2.addWidget(self._add_profile_btn, 0, 3, 1, 1) - self._add_profile_btn.clicked.connect(self.addProfile) - - lo3 = QHBoxLayout() - lo3.setContentsMargins(0, 0, 0, 0) - lab = QLabel("Axis: ", self) - self._wprofile_axis = QComboBox(self) - self._wprofile_axis.activated[int].connect(self.selectAxis) - lo3.addWidget(lab, 0) - lo3.addWidget(self._wprofile_axis, 0) - - lo2.addLayout(lo3, 0, 4, 1, 2, alignment=Qt.AlignRight) - - lo1.setContentsMargins(0, 0, 0, 0) - lo1.addLayout(lo2) - - def selectProfile(self, i): - """ event handler for switching profiles """ - self._storeSelectedProfileInfos() - # detach overlay profiles for previous profile - if self._overlay_static_profiles is not None: - for p in self._overlay_static_profiles: - p.detach() - # pick new state from the stack and restore - self._currentprofile = i - self._restoreSelectedProfileInfos() - self._wprofile_axis.clear() - # switch to corresponding image and set the axis - self.setImage(self._image_hnd, force_repopulate=True) - if self._lastsel is not None: - names = [name for name, iaxis, vals, unit in self._axes] - axisno = names.index(self._lastsel) - # select axis, which in turn redraws the plot - self.selectAxis(axisno) - self._wprofile_axis.setCurrentIndex(axisno) - else: - self.clearProfile(keep_overlays=True) - # update marker on the SkyPlot - if self._parent_picker is not None: - self._parent_picker.setSelectedProfileIndex(i) - # overlay other static plots for current profile - if self._overlay_static_profiles is not None: - for p in self._overlay_static_profiles: - p.attach() - # restore temporary vmin, vmax and xtitle - # if the profile has no active profile - # -- other static profiles may have been loaded - # to an empty profile - if (self._last_data_x is None or \ - self._last_data_y is None) and \ - len(self._overlay_static_profiles) > 0: - if self._lastxmin is not None and \ - self._lastxmax is not None: - self._profplot.setAxisScale( - QwtPlot.xBottom, self._lastxmin, self._lastxmax) - self._profplot.replot() - if self._lastxtitle is not None: - title = QwtText(self._lastxtitle) - title.setFont(self._font) - self._profplot.setAxisTitle(QwtPlot.xBottom, title) - self._profplot.replot() - - def _profileInfosKeys(self): - return ["_lastsel", "_image_id", "_image_hnd", - "_last_x", "_last_y", - "_last_l", "_last_m", - "_last_data_x", "_last_data_y", - "_lastxmin", "_lastxmax", "_lastxtitle", - "_current_profile_name", "_overlay_static_profiles", - "_axes", "_selaxis"] - - def _restoreSelectedProfileInfos(self): - """ restores the profile infos for the currently selected profile """ - profiles_info_keys = self._profileInfosKeys() - for k in profiles_info_keys: - setattr(self, k, self.profiles_info[self._currentprofile].get(k)) - - def _storeSelectedProfileInfos(self): - """ store the profile infos for selected profile switching """ - profiles_info_keys = self._profileInfosKeys() - self.profiles_info[self._currentprofile] = dict(zip(profiles_info_keys, - map(lambda k: getattr(self, k, None), - profiles_info_keys))) - - def clearProfile(self, keep_overlays=False): - self._last_x = None - self._last_y = None - self._last_data_x = None - self._last_data_y = None - if not keep_overlays: - if self._overlay_static_profiles is not None: - for p in self._overlay_static_profiles: - p.detach() - self._overlay_static_profiles = None - if self._legend is not None: - self._legend = None - - self._setupPlot() - if self._parent_picker is not None: - if not keep_overlays: - self._parent_picker.removeSelectedProfileMarkings(self._currentprofile, - purge_history=True) - - def setProfileName(self, name): - self._current_profile_name = name - self._static_profile_select.setItemText(self._currentprofile, name) - - def addProfile(self): - """ event handler for adding new selected profiles """ - self._numprofiles += 1 - self._static_profile_select.addItems(["tmp"]) - - profiles_info_keys = self._profileInfosKeys() - self.profiles_info[self._numprofiles-1] = dict(zip(profiles_info_keys, - [None] * len(profiles_info_keys))) - - # switch to newly created profile - self.selectProfile(self._numprofiles-1) - self._static_profile_select.setCurrentIndex(self._numprofiles-1) - self._wprofile_axis.clear() - # refresh profile for blank profile - self.clearProfile() - self._overlay_static_profiles = None - # reinitialize axes - self.setImage(self._image_hnd, force_repopulate=False) - - self.setProfileName(f"Profile {self._numprofiles}") - - def selectAxis(self, i, remember=True): - LiveProfile.selectAxis(self, i, remember=True) - self.trackImage(self._image_hnd, self._last_x, self._last_y, self._last_l, self._last_m) - # clear profile if no coordinate is set - if self._last_y is None or self._last_x is None: - self.clearProfile(keep_overlays=True) - - def setImage(self, image, force_repopulate=False): - if self._image_id != id(image): - self._wprofile_axis.clear() - LiveProfile.setImage(self, image, force_repopulate=force_repopulate) - - def setVisible(self, visible, emit=True): - LiveProfile.setVisible(self, visible, emit=emit) - if self._parent_picker is not None: - if visible: - self._parent_picker.setSelectedProfileIndex(self._currentprofile) - else: - self._parent_picker.removeAllSelectedProfileMarkings() - - def saveProfile(self, filename=None): - """ Saves current profile to disk """ - if self._selaxis and self._selaxis[0] < len(self._axes) and \ - self._last_x is not None and \ - self._last_y is not None and \ - self._last_data_x is not None and \ - self._last_data_y is not None: - if filename is None: - if not self._export_profile_dialog: - dialog = self._export_profile_dialog = QFileDialog( - self, "Export profile data", ".", "*.tigprof") - dialog.setDefaultSuffix("tigprof") - dialog.setFileMode(QFileDialog.AnyFile) - dialog.setAcceptMode(QFileDialog.AcceptSave) - dialog.setNameFilter("Tigger profile files (*.tigprof)") - dialog.setModal(True) - dialog.filesSelected.connect(self.saveProfile) - return self._export_profile_dialog.exec_() == QDialog.Accepted - - axisname, axisindx, axisvals, axisunit = self._axes[self._selaxis[0]] - - if isinstance(filename, QStringList): - filename = filename[0] - if os.path.exists(filename): - ret = QMessageBox.question( - self, "Overwrite file?", - f"File {filename} exists. Overwrite?", - QMessageBox.Yes | QMessageBox.No) - if ret == QMessageBox.No: - return - prof = TiggerProfile(self._current_profile_name, - axisname, - axisunit, - self._last_data_x, - self._last_data_y) - try: - prof.saveProfile(filename) - except IOError: - QMessageBox.critical( - self, "Could not store profile to disk", - "

An IO error occurred while trying to write out profile. " - "Check that the location is writable and you have sufficient space

" - ) - - else: # no axes selected yet - QMessageBox.critical( - self, "Nothing to save", - "

Profile is empty. Capture profile using CTRL+ALT+LeftClick " - "somewhere on the image first!

") - - def loadProfile(self, filename=None): - """ Loads TigProf profile from disk """ - if filename is None: - if not self._load_profile_dialog: - dialog = self._load_profile_dialog = QFileDialog( - self, "Load TigProf profile data", ".", "*.tigprof") - dialog.setDefaultSuffix("tigprof") - dialog.setFileMode(QFileDialog.ExistingFile) - dialog.setAcceptMode(QFileDialog.AcceptOpen) - dialog.setNameFilter("Tigger profile files (*.tigprof)") - dialog.setModal(True) - dialog.filesSelected.connect(self.loadProfile) - return self._load_profile_dialog.exec_() == QDialog.Accepted - - if isinstance(filename, QStringList): - filename = filename[0] - - try: - prof = TiggerProfileFactory.load(filename) - except IOError as e: - QMessageBox.critical( - self, - "Error loading TigProf profile", - f"Loading failed with message '{str(e)}'" - ) - self.addStaticProfile(prof) - - def addStaticProfile(self, prof, curvecol=None, coord=None): - pastedname, ok = QInputDialog.getText(self, - "Set pasted profile name", - "

Set name of pasted profile

", - text=prof.profileName) - plottableprof = PlottableTiggerProfile(pastedname, - prof.axisName, - prof.axisUnit, - prof.xdata, - prof.ydata, - qwtplot=self._profplot, - profilecoord=coord) - if curvecol is None: - curvecol = QColorDialog.getColor(initial=Qt.white, - parent=self, - title="Select color for loaded TigProf profile",) - if curvecol.isValid(): - plottableprof.setCurveColor(curvecol) - - if self._overlay_static_profiles is None: - self._overlay_static_profiles = [] - self._overlay_static_profiles.append(plottableprof) - if (self._last_data_x is None or - self._last_data_y is None or - self._last_data_x is None or - self._last_data_y is None) and \ - len(self._overlay_static_profiles) > 0: - # temporary titles and labels from pasted profiles - xmin = numpy.nanmin(list(map(lambda x: numpy.nanmin(x.xdata), - self._overlay_static_profiles))) - xmax = numpy.nanmax(list(map(lambda x: numpy.nanmax(x.xdata), - self._overlay_static_profiles))) - self._lastxmin = xmin - self._lastxmax = xmax - self._profplot.setAxisScale(QwtPlot.xBottom, self._lastxmin, self._lastxmax) - # set custom label if first loaded profile - if len(self._overlay_static_profiles) == 1: - name, ok = QInputDialog.getText(self, - "Set custom axis name", - "

Set custom axis name for loaded static profile

", - text=self._overlay_static_profiles[0].axisName) - unit, ok = QInputDialog.getText(self, - "Set custom axis unit", - "

Set custom axis unit for loaded static profile

", - text=self._overlay_static_profiles[0].axisUnit) - self._lastxtitle = "%s, %s" % (name, unit) if unit else name - title = QwtText(self._lastxtitle) - title.setFont(self._font) - self._profplot.setAxisTitle(QwtPlot.xBottom, title) - if self._legend is None: - self._legend = QwtLegend() - self._profplot.insertLegend(self._legend, QwtPlot.BottomLegend) - - if self._parent_picker is not None and coord is not None: - self._parent_picker.addOverlayMarkerToCurrentProfile( - pastedname, coord, plottableprof.createPen(), index=self._currentprofile) - - def pasteActiveProfileAsStatic(self): - def __constructProfileIndex(i): - last_x = self.profiles_info.get(i, {}).get("_last_x", None) - last_y = self.profiles_info.get(i, {}).get("_last_y", None) - last_l = self.profiles_info.get(i, {}).get("_last_l", None) - last_m = self.profiles_info.get(i, {}).get("_last_m", None) - last_data_x = self.profiles_info.get(i, {}).get("_last_data_x", None) - last_data_y = self.profiles_info.get(i, {}).get("_last_data_y", None) - selaxis = self.profiles_info.get(i, {}).get("_selaxis", None) - axes = self.profiles_info.get(i, {}).get("_axes", None) - - if selaxis and selaxis[0] < len(axes) and \ - last_x is not None and \ - last_y is not None and \ - last_l is not None and \ - last_m is not None and \ - last_data_x is not None and \ - last_data_y is not None and \ - selaxis is not None and \ - axes is not None: - - profname = self.profiles_info.get(i, {}).get("_current_profile_name", "Unnamed") - axisname, axisindx, axisvals, axisunit = axes[selaxis[0]] - prof = TiggerProfile(profname, axisname, axisunit, last_data_x, last_data_y) - return (prof, i, (last_l, last_m)) - return None - - avail_profs = list(filter(lambda x: x is not None, - map(lambda i: __constructProfileIndex(i), - filter(lambda i: i != self._currentprofile, - range(self._static_profile_select.count()))))) - if len(avail_profs) == 0: - QMessageBox.critical(self, - "No active profiles available", - "

There are currently no active profiles available for selection in any other profile. " - "You need to use CTRL+ALT+leftclick on another profile to first in order to paste

") - return - - selitem, ok = QInputDialog.getItem(self, - "Paste active profile from", - "

Select from currently defined profiles:

", - list(map(lambda x: x[0].profileName, avail_profs)), - current=0, - editable=False) - if ok: - selprof, iselitem, coord = list(filter(lambda x: x[0].profileName == selitem, avail_profs))[0] - dprint(0, f"Pasting active profile from '{selprof.profileName}'") - self.addStaticProfile(selprof, coord=coord) - class SkyModelPlotter(QWidget): # Selection modes for the various selector functions below. # Default is usually Clear+Add diff --git a/TigGUI/Widgets.py b/TigGUI/Widgets.py index b0f8321..c2d293f 100644 --- a/TigGUI/Widgets.py +++ b/TigGUI/Widgets.py @@ -337,8 +337,8 @@ def __init__(self, title="", parent=None, flags=Qt.WindowFlags(), bind_widget=No self.title_stylesheet = "QWidget {background: rgb(68,68,68);}" self.button_style = "QPushButton:hover:!pressed {background: grey;}" from TigGUI.Images.ControlDialog import ImageControlDialog - from TigGUI.Plot.SkyModelPlot import ToolDialog - from TigGUI.Plot.SkyModelPlot import LiveImageZoom + from TigGUI.Plot.ToolDialogs import ToolDialog + from TigGUI.Plot.ToolDialogs import LiveImageZoom if bind_widget is not None: self.bind_widget = bind_widget if bind_widget is not None: diff --git a/TigGUI/kitties/plottable_profiles.py b/TigGUI/kitties/plottable_profiles.py deleted file mode 100644 index 87751aa..0000000 --- a/TigGUI/kitties/plottable_profiles.py +++ /dev/null @@ -1,94 +0,0 @@ -from PyQt5.Qt import QColor, QPen -from PyQt5.QtCore import Qt -from PyQt5.Qwt import QwtPlotCurve, QwtPlotItem - -from TigGUI.kitties.profiles import MutableTiggerProfile -from TigGUI.Widgets import TiggerPlotCurve - -class PlottableTiggerProfile(MutableTiggerProfile): - def __init__(self, profilename, axisname, axisunit, xdata, ydata, - qwtplot=None, - profilecoord=None): - """ - Plottable (Mutable) Tigger Profile - profilename: A name for this profile - axisname: Name for the axis - axisunit: Unit for the axis (as taken from FITS CUNIT) - xdata: profile x axis data (1D ndarray of shape of ydata) - ydata: profile y axis data (1D ndarray) - qwtplot: parent plot to which this curve should be added - profilecoord: coordinate (world) coord tuple to from which this profile is drawn, optional - use None to leave unset - """ - MutableTiggerProfile.__init__(self, profilename, axisname, axisunit, xdata, ydata) - self._curve_color = QColor("white") - self._curve_pen = self.createPen() - self._curve_pen.setStyle(Qt.DashDotLine) - self._profcurve = TiggerPlotCurve(profilename) - self._profcurve.setRenderHint(QwtPlotItem.RenderAntialiased) - self._ycs = TiggerPlotCurve() - self._ycs.setRenderHint(QwtPlotItem.RenderAntialiased) - self._profcurve.setPen(self._curve_pen) - self._profcurve.setStyle(QwtPlotCurve.Lines) - self._profcurve.setOrientation(Qt.Horizontal) - self._parentPlot = qwtplot - self._profilecoord = None - self.profileAssociatedCoord = profilecoord - - self._profcurve.setData(xdata, ydata) - self._profcurve.setVisible(True) - self._attached = False - self.attach() - - def createPen(self): - return QPen(self._curve_color) - - @property - def hasAssociatedCoord(self): - return self._profilecoord is not None - - @property - def profileAssociatedCoord(self): - return (self._profilecoord[0], - self._profilecoord[1]) - - @profileAssociatedCoord.setter - def profileAssociatedCoord(self, profilecoord): - if profilecoord is not None: - if not (isinstance(profilecoord, tuple) and - len(profilecoord) == 2 and - all(map(lambda x: isinstance(x, float), profilecoord))): - raise TypeError("profilecoord should be 2-element world coord tuple") - self._profilecoord = profilecoord - - def setCurveColor(self, color): - if not isinstance(color, QColor): - raise TypeError("Color must be QColor object") - self._curve_color = color - self._curve_pen = QPen(self._curve_color) - self._curve_pen.setStyle(Qt.DashDotLine) - self._profcurve.setPen(self._curve_pen) - if self._parentPlot is not None: - if self._attached: - self._parentPlot.replot() - - def attach(self): - self.detach() - if self._parentPlot is not None: - self._profcurve.attach(self._parentPlot) - self._parentPlot.replot() - self._attached = True - - def detach(self): - if self._attached: - self._attached = False - self._profcurve.detach() - - def setAxesData(self, xdata, ydata, shouldSetVisible=True): - self.__verifyArrs(xdata, ydata) - self._xdata = xdata.copy() - self._ydata = ydata.copy() - if shouldSetVisible: - self._profcurve.setData(xdata, ydata) - self._profcurve.setVisible(shouldSetVisible) - self.attach() \ No newline at end of file diff --git a/TigGUI/kitties/profiles.py b/TigGUI/kitties/profiles.py index f46e927..4872a5c 100644 --- a/TigGUI/kitties/profiles.py +++ b/TigGUI/kitties/profiles.py @@ -1,3 +1,24 @@ +# Copyright (C) 2002-2022 +# The MeqTree Foundation & +# ASTRON (Netherlands Foundation for Research in Astronomy) +# P.O.Box 2, 7990 AA Dwingeloo, The Netherlands +# +# 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 2 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; if not, see , +# or write to the Free Software Foundation, Inc., +# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# + import numpy as np import json From 2e34241d293e50d3ac8b1706d16a6f6bcb9a04e0 Mon Sep 17 00:00:00 2001 From: bennahugo Date: Tue, 26 Jul 2022 16:16:05 +0200 Subject: [PATCH 02/28] add new split out files --- TigGUI/Plot/ToolDialogs.py | 980 +++++++++++++++++++++++++++++++ TigGUI/Plot/Utils.py | 212 +++++++ TigGUI/Plot/plottableProfiles.py | 94 +++ 3 files changed, 1286 insertions(+) create mode 100644 TigGUI/Plot/ToolDialogs.py create mode 100644 TigGUI/Plot/Utils.py create mode 100644 TigGUI/Plot/plottableProfiles.py diff --git a/TigGUI/Plot/ToolDialogs.py b/TigGUI/Plot/ToolDialogs.py new file mode 100644 index 0000000..741b2c8 --- /dev/null +++ b/TigGUI/Plot/ToolDialogs.py @@ -0,0 +1,980 @@ +# Copyright (C) 2002-2022 +# The MeqTree Foundation & +# ASTRON (Netherlands Foundation for Research in Astronomy) +# P.O.Box 2, 7990 AA Dwingeloo, The Netherlands +# +# 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 2 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; if not, see , +# or write to the Free Software Foundation, Inc., +# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# + +from PyQt5.Qt import (QAction, QApplication, QCheckBox, + QColor, QComboBox, QDialog, + QFileDialog, QHBoxLayout, QInputDialog, + QLabel, QMenu, QMessageBox, QPen, + QPoint, QRectF, QSize, QSizePolicy, + QToolButton, QTransform, QVBoxLayout, + QGridLayout, QColorDialog) +from PyQt5.QtCore import Qt +from PyQt5.QtCore import pyqtSignal +from PyQt5.QtWidgets import QDockWidget, QLayout +from PyQt5.Qwt import (QwtPlot, QwtPlotCurve, QwtPlotItem, QwtText, + QwtLegend) +QStringList = list + +from TigGUI.Widgets import TiggerPlotCurve +from TigGUI.init import Config, pixmaps +from TigGUI.Plot.plottableProfiles import PlottableTiggerProfile +import TigGUI.kitties.utils +from TigGUI.kitties.utils import curry +from TigGUI.kitties.profiles import TiggerProfile, TiggerProfileFactory +from TigGUI.Plot.Utils import makeSourceMarker, makeDualColorPen +import numpy +import os + +import TigGUI +_verbosity = TigGUI.kitties.utils.verbosity(name="plot") +dprint = _verbosity.dprint +dprintf = _verbosity.dprintf + +class ToolDialog(QDialog): + signalIsVisible = pyqtSignal(bool) + + def __init__(self, parent, mainwin, configname, menuname, show_shortcut=None): + QDialog.__init__(self, parent) + self.setModal(False) + self.setFocusPolicy(Qt.NoFocus) + self.mainwin = mainwin + self.hide() + self._configname = configname + self._geometry = None + # make hide/show qaction + self._qa_show = qa = QAction("Show %s" % menuname.replace("&", "&&"), self) + if show_shortcut: + qa.setShortcut(show_shortcut) + qa.setCheckable(True) + qa.setChecked(Config.getbool("%s-show" % configname, False)) + qa.setVisible(False) + qa.setToolTip("""

The quick zoom & cross-sections window shows a zoom of the current image area + under the mose pointer, and X/Y cross-sections through that area.

""") + qa.triggered[bool].connect(self.setVisible) + self._closing = False + self._write_config = curry(Config.set, "%s-show" % configname) + qa.triggered[bool].connect(self._write_config) + self.signalIsVisible.connect(qa.setChecked) + + def getShowQAction(self): + return self._qa_show + + def makeAvailable(self, available=True): + """Makes the tool available (or unavailable)-- shows/hides the "show" control, + and shows/hides the dialog according to this control.""" + self._qa_show.setVisible(available) + self.setVisible(self._qa_show.isChecked() if available else False) + + def initGeometry(self): + x0 = Config.getint('%s-x0' % self._configname, 0) + y0 = Config.getint('%s-y0' % self._configname, 0) + w = Config.getint('%s-width' % self._configname, 0) + h = Config.getint('%s-height' % self._configname, 0) + if w and h: + self.resize(w, h) + self.move(x0, y0) + return True + return False + + def _saveGeometry(self): + Config.set('%s-x0' % self._configname, self.pos().x()) + Config.set('%s-y0' % self._configname, self.pos().y()) + Config.set('%s-width' % self._configname, self.width()) + Config.set('%s-height' % self._configname, self.height()) + + def close(self): + self._closing = True + QDialog.close(self) + + def closeEvent(self, event): + QDialog.closeEvent(self, event) + if not self._closing: + self._write_config(False) + + def moveEvent(self, event): + self._saveGeometry() + QDialog.moveEvent(self, event) + + def resizeEvent(self, event): + self._saveGeometry() + QDialog.resizeEvent(self, event) + + def setVisible(self, visible, emit=True): + if not visible: + self._geometry = self.geometry() + else: + if self._geometry: + self.setGeometry(self._geometry) + if emit: + self.signalIsVisible.emit(visible) + QDialog.setVisible(self, visible) + # This section aligns the dockwidget with its subqwidget's visibility + if visible and not self.parent().isVisible(): + self.parent().setGeometry(self.geometry()) + self.parent().setVisible(True) + _area = self.mainwin.dockWidgetArea(self.parent()) # in right dock area + if self.mainwin.windowState() != Qt.WindowMaximized: + if not self.get_docked_widget_size(self.parent(), _area): + geo = self.mainwin.geometry() + geo.setWidth(self.mainwin.width() + self.parent().width()) + center = geo.center() + if self.mainwin.dockWidgetArea(self.parent()) == 2: # in right dock area + geo.moveCenter(QPoint(center.x() + self.parent().width(), geo.y())) + elif self.mainwin.dockWidgetArea(self.parent()) == 1: + geo.moveCenter(QPoint(center.x() - self.width(), geo.y())) + self.mainwin.setGeometry(geo) + if _area == 2 and isinstance(self.parent().bind_widget, TigGUI.Plot.SkyModelPlot.LiveImageZoom): + self.mainwin.addDockWidgetToArea(self.parent(), _area) + else: + self.mainwin.addDockWidgetToArea(self.parent(), _area) + elif not visible and self.parent().isVisible(): + _area = self.mainwin.dockWidgetArea(self.parent()) # in right dock area + if self.mainwin.windowState() != Qt.WindowMaximized: + if not self.get_docked_widget_size(self.parent(), _area): + geo = self.mainwin.geometry() + geo.setWidth(self.mainwin.width() - self.parent().width()) + center = geo.center() + if self.mainwin.dockWidgetArea(self.parent()) == 1: # in left dock area + geo.moveCenter(QPoint(center.x() + self.parent().width(), geo.y())) + self.mainwin.setGeometry(geo) + self.parent().setVisible(False) + self.mainwin.restoreDockArea(_area) + + def get_docked_widget_size(self, _dockable, _area): + widget_list = self.mainwin.findChildren(QDockWidget) + size_list = [] + if _dockable: + for widget in widget_list: + if self.mainwin.dockWidgetArea(widget) == _area: + if widget is not _dockable: + if (not widget.isWindow() and not widget.isFloating() + and widget.isVisible()): + size_list.append(widget.bind_widget.width()) + if size_list: + return max(size_list) + else: + return size_list + + +class LiveImageZoom(ToolDialog): + livezoom_resize_signal = pyqtSignal(QSize) + + def __init__(self, parent, mainwin, radius=10, factor=12): + ToolDialog.__init__(self, parent, mainwin, configname="livezoom", menuname="live zoom & cross-sections", + show_shortcut=Qt.Key_F2) + self.setWindowTitle("Zoom & Cross-sections") + radius = Config.getint("livezoom-radius", radius) + # create size polixy for livezoom + livezoom_policy = QSizePolicy() + livezoom_policy.setWidthForHeight(True) + livezoom_policy.setHeightForWidth(True) + self.setSizePolicy(livezoom_policy) + # add plots + self._lo0 = lo0 = QVBoxLayout(self) + self._lo0.setSizeConstraint(QLayout.SetFixedSize) + lo1 = QHBoxLayout() + lo1.setContentsMargins(0, 0, 0, 0) + lo1.setSpacing(0) + lo0.addLayout(lo1) + # control checkboxes + self._showzoom = QCheckBox("show zoom", self) + self._showcs = QCheckBox("show cross-sections", self) + self._showzoom.setChecked(True) + self._showcs.setChecked(True) + self._showzoom.toggled[bool].connect(self._showZoom) + self._showcs.toggled[bool].connect(self._showCrossSections) + lo1.addWidget(self._showzoom, 0) + lo1.addSpacing(5) + lo1.addWidget(self._showcs, 0) + lo1.addStretch(1) + self._smaller = QToolButton(self) + self._smaller.setIcon(pixmaps.window_smaller.icon()) + self._smaller.clicked.connect(self._shrink) + self._larger = QToolButton(self) + self._larger.setIcon(pixmaps.window_larger.icon()) + self._larger.clicked.connect(self._enlarge) + lo1.addWidget(self._smaller) + lo1.addWidget(self._larger) + self._has_zoom = self._has_xcs = self._has_ycs = False + # setup zoom plot + font = QApplication.font() + font.setPointSize(8) + axis_font = QApplication.font() + axis_font.setBold(True) + axis_font.setPointSize(10) + self._zoomplot = QwtPlot(self) + self._zoomplot.setContentsMargins(5, 5, 5, 5) + axes = {QwtPlot.xBottom: "X pixel coordinate", + QwtPlot.yLeft: "Y pixel coordinate", + QwtPlot.xTop: "X cross-section value", + QwtPlot.yRight: "Y cross-section value"} + for axis, title in axes.items(): + self._zoomplot.enableAxis(True) + self._zoomplot.setAxisScale(axis, 0, 1) + self._zoomplot.setAxisFont(axis, font) + self._zoomplot.setAxisMaxMajor(axis, 3) + self._zoomplot.axisWidget(axis).setMinBorderDist(5, 5) + self._zoomplot.axisWidget(axis).show() + text = QwtText(title) + text.setFont(font) + self._zoomplot.axisWidget(axis).setTitle(text.text()) + axis_text = QwtText(title) + axis_text.setFont(axis_font) + self._zoomplot.setAxisTitle(axis, axis_text) + self._zoomplot.setAxisLabelRotation(QwtPlot.yLeft, -90) + self._zoomplot.setAxisLabelAlignment(QwtPlot.yLeft, Qt.AlignVCenter) + self._zoomplot.setAxisLabelRotation(QwtPlot.yRight, 90) + self._zoomplot.setAxisLabelAlignment(QwtPlot.yRight, Qt.AlignVCenter) + # self._zoomplot.plotLayout().setAlignCanvasToScales(True) + lo0.addWidget(self._zoomplot, 0) + # setup ZoomItem for zoom plot + self._zi = self.ImageItem() + self._zi.attach(self._zoomplot) + self._zi.setZ(0) + # setup targeting reticule for zoom plot + self._reticules = TiggerPlotCurve(), TiggerPlotCurve() + for curve in self._reticules: + curve.setRenderHint(QwtPlotItem.RenderAntialiased) + curve.setPen(QPen(QColor("green"))) + curve.setStyle(QwtPlotCurve.Lines) + curve.attach(self._zoomplot) + curve.setZ(1) + # setup cross-section curves + self._xcs = TiggerPlotCurve() + self._xcs.setRenderHint(QwtPlotItem.RenderAntialiased) + self._ycs = TiggerPlotCurve() + self._ycs.setRenderHint(QwtPlotItem.RenderAntialiased) + self._xcs.setPen(makeDualColorPen("navy", "yellow")) + self._ycs.setPen(makeDualColorPen("black", "cyan")) + for curve in self._xcs, self._ycs: + curve.setStyle(QwtPlotCurve.Steps) + curve.attach(self._zoomplot) + curve.setZ(2) + self._xcs.setXAxis(QwtPlot.xBottom) + self._xcs.setYAxis(QwtPlot.yRight) + self._ycs.setXAxis(QwtPlot.xTop) + self._ycs.setYAxis(QwtPlot.yLeft) + # self._ycs.setCurveType(QwtPlotCurve.Xfy) # old qwt5 + self._ycs.setOrientation(Qt.Vertical) # Qwt 6 version + self._xcs.setOrientation(Qt.Horizontal) # Qwt 6 version + # make QTransform for flipping images upside-down + self._xform = QTransform() + self._xform.scale(1, -1) + # init geometry + self.setPlotSize(radius, factor) + self.initGeometry() + + def _showZoom(self, show): + if not show: + self._zi.setVisible(False) + + def _showCrossSections(self, show): + self._zoomplot.enableAxis(QwtPlot.xTop, show) + self._zoomplot.enableAxis(QwtPlot.yRight, show) + if not show: + self._xcs.setVisible(False) + self._ycs.setVisible(False) + + def _enlarge(self): + self.setPlotSize(int(self._radius * 2), self._magfac) + + def _shrink(self): + self.setPlotSize(int(self._radius / 2), self._magfac) + + def setPlotSize(self, radius, factor): + Config.set('livezoom-radius', radius) + self._radius = radius + # enable smaller/larger buttons based on radius + self._smaller.setEnabled(radius > 5) + self._larger.setEnabled(radius < 40) + # compute other sizes + self._npix = radius * 2 + 1 + self._magfac = factor + width = height = self._npix * self._magfac + self._zoomplot.setMinimumHeight(height + 80) + self._zoomplot.setMinimumWidth(width + 80) + # set data array + self._data = numpy.ma.masked_array(numpy.zeros((self._npix, self._npix), float), + numpy.zeros((self._npix, self._npix), bool)) + # reset window size + self._lo0.update() + self.resize(self._lo0.minimumSize()) + self.livezoom_resize_signal.emit(self._lo0.minimumSize()) + + def _getZoomSlice(self, ix, nx): + ix0, ix1 = ix - self._radius, ix + self._radius + 1 + zx0 = -min(ix0, 0) + ix0 = max(ix0, 0) + zx1 = self._npix - max(ix1, nx - 1) + (nx - 1) + ix1 = min(ix1, nx - 1) + return ix0, ix1, zx0, zx1 + + class ImageItem(QwtPlotItem): + """ImageItem subclass used by LiveZoomer to display zoomed-in images""" + + def __init__(self): + QwtPlotItem.__init__(self) + self._qimg = None + self.RenderAntialiased + + def setImage(self, qimg): + self._qimg = qimg + + def draw(self, painter, xmap, ymap, rect): + """Implements QwtPlotItem.draw(), to render the image on the given painter.""" + # drawImage expects QRectF + self._qimg and painter.drawImage(QRectF(xmap.p1(), ymap.p2(), xmap.pDist(), ymap.pDist()), self._qimg) + + def trackImage(self, image, ix, iy): + if not self.isVisible(): + return + # update zoomed image + # find overlap of zoom window with image, mask invisible pixels + nx, ny = image.imageDims() + ix0, ix1, zx0, zx1 = self._getZoomSlice(ix, nx) + iy0, iy1, zy0, zy1 = self._getZoomSlice(iy, ny) + if ix0 < nx and ix1 >= 0 and iy0 < ny and iy1 >= 0: + if self._showzoom.isChecked(): + # There was an error here when using zoom window zoom buttons + # (TypeError: slice indices must be integers or None or have an __index__ method). + # Therefore indexes have been cast as int() + # 16/05/2022: the error no longer occurs, therefore code has been reverted. + self._data.mask[...] = False + self._data.mask[:zx0, ...] = True + self._data.mask[zx1:, ...] = True + self._data.mask[..., :zy0] = True + self._data.mask[..., zy1:] = True + # copy & colorize region + self._data[zx0:zx1, zy0:zy1] = image.image()[ix0:ix1, iy0:iy1] + intensity = image.intensityMap().remap(self._data) + self._zi.setImage( + image.colorMap().colorize(image.intensityMap().remap(self._data)).transformed(self._xform)) + self._zi.setVisible(True) + # set cross-sections + if self._showcs.isChecked(): + if iy >= 0 and iy < ny and ix1 > ix0: + # added fix for masked arrays and mosaic images + xcs = [float(x) for x in numpy.ma.filled(image.image()[ix0:ix1, iy], fill_value=0.0)] + self._xcs.setData(numpy.arange(ix0 - 1, ix1) + .5, [xcs[0]] + xcs) + self._xcs.setVisible(True) + self._zoomplot.setAxisAutoScale(QwtPlot.yRight) + self._has_xcs = True + else: + self._xcs.setVisible(False) + self._zoomplot.setAxisScale(QwtPlot.yRight, 0, 1) + if ix >= 0 and ix < nx and iy1 > iy0: + # added fix for masked arrays and mosaic images + ycs = [float(y) for y in numpy.ma.filled(image.image()[ix, iy0:iy1], fill_value=0.0)] + self._ycs.setData([ycs[0]] + ycs, numpy.arange(iy0 - 1, iy1) + .5) + self._ycs.setVisible(True) + self._zoomplot.setAxisAutoScale(QwtPlot.xTop) + self._has_ycs = True + else: + self._ycs.setVisible(False) + self._zoomplot.setAxisScale(QwtPlot.xTop, 0, 1) + else: + for plotitem in self._zi, self._xcs, self._ycs: + plotitem.setVisible(False) + # set zoom plot scales + x0, x1 = ix - self._radius - .5, ix + self._radius + .5 + y0, y1 = iy - self._radius - .5, iy + self._radius + .5 + self._reticules[0].setData([ix, ix], [y0, y1]) + self._reticules[1].setData([x0, x1], [iy, iy]) + self._zoomplot.setAxisScale(QwtPlot.xBottom, x0, x1) + self._zoomplot.setAxisScale(QwtPlot.yLeft, y0, y1) + self._zoomplot.enableAxis(QwtPlot.xTop, self._showcs.isChecked()) + # update plots + self._zoomplot.replot() + + +class LiveProfile(ToolDialog): + def __init__(self, parent, mainwin, configname="liveprofile", menuname="profiles", show_shortcut=Qt.Key_F3): + ToolDialog.__init__(self, parent, mainwin, configname=configname, menuname=menuname, show_shortcut=show_shortcut) + self.setWindowTitle("Profiles") + self._profplot = None + self._setupLayout() + self._axes = [] + self._lastsel = None + self._image_id = None + self._image_hnd = None + self._last_x = None + self._last_y = None + self._parent_picker = None + self._last_data_x = None + self._last_data_y = None + self._selaxis = None + + def _setupAxisSelectorLayout(self, lo1): + lo1.setContentsMargins(0, 0, 0, 0) + lab = QLabel("Axis: ", self) + self._wprofile_axis = QComboBox(self) + self._wprofile_axis.activated[int].connect(self.selectAxis) + lo1.addWidget(lab, 0) + lo1.addWidget(self._wprofile_axis, 0) + lo1.addStretch(1) + + def _setupPlot(self): + lo0 = self._lo0 + liveprofile_policy = self._liveprofile_policy + self._font = font = QApplication.font() + + # detach and release plots if already initialized + if self._profplot is not None: + self._profcurve.setData([0, 0], [0, 0]) + self._profcurve.setVisible(True) + self._profplot.replot() + self._profplot.setMaximumHeight(256) + self._profplot.setMinimumHeight(256) + else: + self._profplot = QwtPlot(self) + self._profplot.setContentsMargins(0, 0, 0, 0) + self._profplot.enableAxis(QwtPlot.xBottom) + self._profplot.enableAxis(QwtPlot.yLeft) + self._profplot.setAxisFont(QwtPlot.xBottom, font) + self._profplot.setAxisFont(QwtPlot.yLeft, font) + # self._profplot.setAxisMaxMajor(QwtPlot.xBottom,3) + self._profplot.setAxisAutoScale(QwtPlot.yLeft) + self._profplot.setAxisMaxMajor(QwtPlot.yLeft, 3) + self._profplot.axisWidget(QwtPlot.yLeft).setMinBorderDist(16, 16) + self._profplot.setAxisLabelRotation(QwtPlot.yLeft, -90) + self._profplot.setAxisLabelAlignment(QwtPlot.yLeft, Qt.AlignVCenter) + self._profplot.plotLayout().setAlignCanvasToScales(True) + self._profplot.setMaximumHeight(256) + self._profplot.setMinimumHeight(56) + # self._profplot.setMinimumWidth(256) + # self._profplot.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Minimum) + self._profplot.setSizePolicy(liveprofile_policy) + lo0.addWidget(self._profplot, 0) + # and new profile curve + self._profcurve = TiggerPlotCurve("Active") + self._profcurve.setRenderHint(QwtPlotItem.RenderAntialiased) + self._ycs = TiggerPlotCurve() + self._ycs.setRenderHint(QwtPlotItem.RenderAntialiased) + self._profcurve.setPen(QPen(QColor("white"))) + self._profcurve.setStyle(QwtPlotCurve.Lines) + self._profcurve.setOrientation(Qt.Horizontal) + self._profcurve.attach(self._profplot) + + def _setupLayout(self): + # create size policy for live profile + liveprofile_policy = QSizePolicy() + liveprofile_policy.setHorizontalPolicy(QSizePolicy.MinimumExpanding) + liveprofile_policy.setVerticalPolicy(QSizePolicy.Fixed) + self._liveprofile_policy = liveprofile_policy + self.setSizePolicy(liveprofile_policy) + # add plots + lo0 = QVBoxLayout(self) + lo0.setSpacing(0) + self._lo0 = lo0 + + lo1 = QHBoxLayout() + lo1.setContentsMargins(0, 0, 0, 0) + lo0.addLayout(lo1) + self._setupAxisSelectorLayout(lo1) + # add profile plot + self._setupPlot() + # config geometry + if not self.initGeometry(): + self.resize(300, 192) + + def setImage(self, image, force_repopulate=False): + if image is None: + return + if id(image) == self._image_id and not force_repopulate: + return + self._image_id = id(image) + self._image_hnd = image + + # build list of axes -- first X and Y + self._axes = [] + for n, label in enumerate(("X", "Y")): + iaxis, np = image.getSkyAxis(n) + self._axes.append((label, iaxis, list(range(np)), "pixels")) + self._xaxis = self._axes[0][1] + self._yaxis = self._axes[1][1] + # then, extra axes + for i in range(image.numExtraAxes()): + iaxis, name, labels = image.extraAxisNumberNameLabels(i) + if len(labels) > 1 and name.upper() not in ("STOKES", "COMPLEX"): + values = image.extraAxisValues(i) + unit, scale = image.extraAxisUnitScale(i) + self._axes.append((name, iaxis, [x / scale for x in values], unit)) + # put them into the selector + names = [name for name, iaxis, vals, unit in self._axes] + self._wprofile_axis.addItems(names) + if self._lastsel in names: + axis = names.index(self._lastsel) + elif len(self._axes) > 2: + axis = 2 + else: + axis = 0 + self._wprofile_axis.setCurrentIndex(axis) + self.selectAxis(axis, remember=False) + + def selectAxis(self, i, remember=True): + if i is None: + return + if i < len(self._axes): + name, iaxis, values, unit = self._axes[i] + self._selaxis = iaxis, values + self._profplot.setAxisScale(QwtPlot.xBottom, min(values), max(values)) + title = QwtText("%s, %s" % (name, unit) if unit else name) + title.setFont(self._font) + self._profplot.setAxisTitle(QwtPlot.xBottom, title) + # save selection + if remember: + self._lastsel = name + + def trackImage(self, image, ix, iy, il, im): + if not self.isVisible(): + return + if ix is None or iy is None: + return + + nx, ny = image.imageDims() + inrange = ix < nx and ix >= 0 and iy < ny and iy >= 0 + if inrange: + # check if image has changed + self.setImage(image) + # make profile slice + iaxis, xval = self._selaxis + slicer = image.currentSlice() + slicer[self._xaxis] = ix + slicer[self._yaxis] = iy + slicer[iaxis] = slice(None) + yval = image.data()[tuple(slicer)] + i0, i1 = 0, len(xval) + # if X or Y profile, set axis scale to match that of window + if iaxis == 0: + rect = image.currentRectPix() + i0 = rect.topLeft().x() + i1 = i0 + rect.width() + self._profplot.setAxisScale(QwtPlot.xBottom, xval[i0], xval[i1 - 1]) + elif iaxis == 1: + rect = image.currentRectPix() + i0 = rect.topLeft().y() + i1 = i0 + rect.height() + self._profplot.setAxisScale(QwtPlot.xBottom, xval[i0], xval[i1 - 1]) + # added fix for masked arrays and mosaic images + yval = numpy.ma.filled(yval[i0:i1], fill_value=0.0) + xval = numpy.ma.filled(xval[i0:i1], fill_value=0.0) + self._profcurve.setData(xval, yval) + # store the data slice for the last pixel coordinate + self._last_data_x = xval + self._last_data_y = yval + # store profile last update coordinates + self._last_x = ix + self._last_y = iy + self._last_l = il + self._last_m = im + self._profcurve.setVisible(inrange) + # update plots + self._profplot.replot() + + +class SelectedProfile(LiveProfile): + """ 'Freezed' profile showing profile for axis at selected cube pierce point """ + def __init__(self, + parent, + mainwin, + configname="liveprofile", + menuname="profiles", + show_shortcut=Qt.Key_F4, + picker_parent=None): + self.profiles_info = {} + self._legend = None + self._numprofiles = 0 + self._currentprofile = 0 + self._parent_picker = None + self._current_profile_name = None + self._export_profile_dialog = None + self._load_profile_dialog = None + self._overlay_static_profiles = None + self._lastxmin = None + self._lastxmax = None + self._lastxtitle = None + LiveProfile.__init__(self, parent, mainwin, configname, menuname, show_shortcut) + self.addProfile() + self._parent_picker = picker_parent + + def _setupAxisSelectorLayout(self, lo1): + """ Adds controls for freeze pane profile dialog """ + lo2 = QGridLayout() + self._menu = QMenu("Selected Profile", self) + + def __inputNewName(): + text, ok = QInputDialog.getText( + self, + "Set profile name", + "

Enter new name for profile:

", + text=self._current_profile_name) + if text: + self.setProfileName(text) + self._menu.addAction("Clear profile", self.clearProfile) + self._menu.addAction("Set profile name", __inputNewName) + self._menu.addAction("Save active profile as", self.saveProfile) + self._menu.addAction("Overlay TigProf static profile from file", self.loadProfile) + self._menu_opt_paste = self._menu.addAction("Overlay another active profile as static profile", self.pasteActiveProfileAsStatic) + self._profile_ctrl_btn = QToolButton() + self._profile_ctrl_btn.setMenu(self._menu) + self._profile_ctrl_btn.setToolTip("

Click to show options for this profile

") + self._profile_ctrl_btn.setIcon(pixmaps.raise_up.icon()) + lo2.addWidget(self._profile_ctrl_btn, 0, 0, 1, 1) + + lab = QLabel("Selected profile: ") + lo2.addWidget(lab, 0, 1, 1, 1) + self._static_profile_select = QComboBox(self) + lo2.addWidget(self._static_profile_select, 0, 2, 1, 1) + + self._static_profile_select.activated[int].connect(self.selectProfile) + self._add_profile_btn = QToolButton() + self._add_profile_btn.setIcon(pixmaps.big_plus.icon()) + self._add_profile_btn.setToolTip("

Click to add another freezed profile

") + lo2.addWidget(self._add_profile_btn, 0, 3, 1, 1) + self._add_profile_btn.clicked.connect(self.addProfile) + + lo3 = QHBoxLayout() + lo3.setContentsMargins(0, 0, 0, 0) + lab = QLabel("Axis: ", self) + self._wprofile_axis = QComboBox(self) + self._wprofile_axis.activated[int].connect(self.selectAxis) + lo3.addWidget(lab, 0) + lo3.addWidget(self._wprofile_axis, 0) + + lo2.addLayout(lo3, 0, 4, 1, 2, alignment=Qt.AlignRight) + + lo1.setContentsMargins(0, 0, 0, 0) + lo1.addLayout(lo2) + + def selectProfile(self, i): + """ event handler for switching profiles """ + self._storeSelectedProfileInfos() + # detach overlay profiles for previous profile + if self._overlay_static_profiles is not None: + for p in self._overlay_static_profiles: + p.detach() + # pick new state from the stack and restore + self._currentprofile = i + self._restoreSelectedProfileInfos() + self._wprofile_axis.clear() + # switch to corresponding image and set the axis + self.setImage(self._image_hnd, force_repopulate=True) + if self._lastsel is not None: + names = [name for name, iaxis, vals, unit in self._axes] + axisno = names.index(self._lastsel) + # select axis, which in turn redraws the plot + self.selectAxis(axisno) + self._wprofile_axis.setCurrentIndex(axisno) + else: + self.clearProfile(keep_overlays=True) + # update marker on the SkyPlot + if self._parent_picker is not None: + self._parent_picker.setSelectedProfileIndex(i) + # overlay other static plots for current profile + if self._overlay_static_profiles is not None: + for p in self._overlay_static_profiles: + p.attach() + # restore temporary vmin, vmax and xtitle + # if the profile has no active profile + # -- other static profiles may have been loaded + # to an empty profile + if (self._last_data_x is None or \ + self._last_data_y is None) and \ + len(self._overlay_static_profiles) > 0: + if self._lastxmin is not None and \ + self._lastxmax is not None: + self._profplot.setAxisScale( + QwtPlot.xBottom, self._lastxmin, self._lastxmax) + self._profplot.replot() + if self._lastxtitle is not None: + title = QwtText(self._lastxtitle) + title.setFont(self._font) + self._profplot.setAxisTitle(QwtPlot.xBottom, title) + self._profplot.replot() + + def _profileInfosKeys(self): + return ["_lastsel", "_image_id", "_image_hnd", + "_last_x", "_last_y", + "_last_l", "_last_m", + "_last_data_x", "_last_data_y", + "_lastxmin", "_lastxmax", "_lastxtitle", + "_current_profile_name", "_overlay_static_profiles", + "_axes", "_selaxis"] + + def _restoreSelectedProfileInfos(self): + """ restores the profile infos for the currently selected profile """ + profiles_info_keys = self._profileInfosKeys() + for k in profiles_info_keys: + setattr(self, k, self.profiles_info[self._currentprofile].get(k)) + + def _storeSelectedProfileInfos(self): + """ store the profile infos for selected profile switching """ + profiles_info_keys = self._profileInfosKeys() + self.profiles_info[self._currentprofile] = dict(zip(profiles_info_keys, + map(lambda k: getattr(self, k, None), + profiles_info_keys))) + + def clearProfile(self, keep_overlays=False): + self._last_x = None + self._last_y = None + self._last_data_x = None + self._last_data_y = None + if not keep_overlays: + if self._overlay_static_profiles is not None: + for p in self._overlay_static_profiles: + p.detach() + self._overlay_static_profiles = None + if self._legend is not None: + self._legend = None + + self._setupPlot() + if self._parent_picker is not None: + if not keep_overlays: + self._parent_picker.removeSelectedProfileMarkings(self._currentprofile, + purge_history=True) + + def setProfileName(self, name): + self._current_profile_name = name + self._static_profile_select.setItemText(self._currentprofile, name) + + def addProfile(self): + """ event handler for adding new selected profiles """ + self._numprofiles += 1 + self._static_profile_select.addItems(["tmp"]) + + profiles_info_keys = self._profileInfosKeys() + self.profiles_info[self._numprofiles-1] = dict(zip(profiles_info_keys, + [None] * len(profiles_info_keys))) + + # switch to newly created profile + self.selectProfile(self._numprofiles-1) + self._static_profile_select.setCurrentIndex(self._numprofiles-1) + self._wprofile_axis.clear() + # refresh profile for blank profile + self.clearProfile() + self._overlay_static_profiles = None + # reinitialize axes + self.setImage(self._image_hnd, force_repopulate=False) + + self.setProfileName(f"Profile {self._numprofiles}") + + def selectAxis(self, i, remember=True): + LiveProfile.selectAxis(self, i, remember=True) + self.trackImage(self._image_hnd, self._last_x, self._last_y, self._last_l, self._last_m) + # clear profile if no coordinate is set + if self._last_y is None or self._last_x is None: + self.clearProfile(keep_overlays=True) + + def setImage(self, image, force_repopulate=False): + if self._image_id != id(image): + self._wprofile_axis.clear() + LiveProfile.setImage(self, image, force_repopulate=force_repopulate) + + def setVisible(self, visible, emit=True): + LiveProfile.setVisible(self, visible, emit=emit) + if self._parent_picker is not None: + if visible: + self._parent_picker.setSelectedProfileIndex(self._currentprofile) + else: + self._parent_picker.removeAllSelectedProfileMarkings() + + def saveProfile(self, filename=None): + """ Saves current profile to disk """ + if self._selaxis and self._selaxis[0] < len(self._axes) and \ + self._last_x is not None and \ + self._last_y is not None and \ + self._last_data_x is not None and \ + self._last_data_y is not None: + if filename is None: + if not self._export_profile_dialog: + dialog = self._export_profile_dialog = QFileDialog( + self, "Export profile data", ".", "*.tigprof") + dialog.setDefaultSuffix("tigprof") + dialog.setFileMode(QFileDialog.AnyFile) + dialog.setAcceptMode(QFileDialog.AcceptSave) + dialog.setNameFilter("Tigger profile files (*.tigprof)") + dialog.setModal(True) + dialog.filesSelected.connect(self.saveProfile) + return self._export_profile_dialog.exec_() == QDialog.Accepted + + axisname, axisindx, axisvals, axisunit = self._axes[self._selaxis[0]] + + if isinstance(filename, QStringList): + filename = filename[0] + if os.path.exists(filename): + ret = QMessageBox.question( + self, "Overwrite file?", + f"File {filename} exists. Overwrite?", + QMessageBox.Yes | QMessageBox.No) + if ret == QMessageBox.No: + return + prof = TiggerProfile(self._current_profile_name, + axisname, + axisunit, + self._last_data_x, + self._last_data_y) + try: + prof.saveProfile(filename) + except IOError: + QMessageBox.critical( + self, "Could not store profile to disk", + "

An IO error occurred while trying to write out profile. " + "Check that the location is writable and you have sufficient space

" + ) + + else: # no axes selected yet + QMessageBox.critical( + self, "Nothing to save", + "

Profile is empty. Capture profile using CTRL+ALT+LeftClick " + "somewhere on the image first!

") + + def loadProfile(self, filename=None): + """ Loads TigProf profile from disk """ + if filename is None: + if not self._load_profile_dialog: + dialog = self._load_profile_dialog = QFileDialog( + self, "Load TigProf profile data", ".", "*.tigprof") + dialog.setDefaultSuffix("tigprof") + dialog.setFileMode(QFileDialog.ExistingFile) + dialog.setAcceptMode(QFileDialog.AcceptOpen) + dialog.setNameFilter("Tigger profile files (*.tigprof)") + dialog.setModal(True) + dialog.filesSelected.connect(self.loadProfile) + return self._load_profile_dialog.exec_() == QDialog.Accepted + + if isinstance(filename, QStringList): + filename = filename[0] + + try: + prof = TiggerProfileFactory.load(filename) + except IOError as e: + QMessageBox.critical( + self, + "Error loading TigProf profile", + f"Loading failed with message '{str(e)}'" + ) + self.addStaticProfile(prof) + + def addStaticProfile(self, prof, curvecol=None, coord=None): + pastedname, ok = QInputDialog.getText(self, + "Set pasted profile name", + "

Set name of pasted profile

", + text=prof.profileName) + plottableprof = PlottableTiggerProfile(pastedname, + prof.axisName, + prof.axisUnit, + prof.xdata, + prof.ydata, + qwtplot=self._profplot, + profilecoord=coord) + if curvecol is None: + curvecol = QColorDialog.getColor(initial=Qt.white, + parent=self, + title="Select color for loaded TigProf profile",) + if curvecol.isValid(): + plottableprof.setCurveColor(curvecol) + + if self._overlay_static_profiles is None: + self._overlay_static_profiles = [] + self._overlay_static_profiles.append(plottableprof) + if (self._last_data_x is None or + self._last_data_y is None or + self._last_data_x is None or + self._last_data_y is None) and \ + len(self._overlay_static_profiles) > 0: + # temporary titles and labels from pasted profiles + xmin = numpy.nanmin(list(map(lambda x: numpy.nanmin(x.xdata), + self._overlay_static_profiles))) + xmax = numpy.nanmax(list(map(lambda x: numpy.nanmax(x.xdata), + self._overlay_static_profiles))) + self._lastxmin = xmin + self._lastxmax = xmax + self._profplot.setAxisScale(QwtPlot.xBottom, self._lastxmin, self._lastxmax) + # set custom label if first loaded profile + if len(self._overlay_static_profiles) == 1: + name, ok = QInputDialog.getText(self, + "Set custom axis name", + "

Set custom axis name for loaded static profile

", + text=self._overlay_static_profiles[0].axisName) + unit, ok = QInputDialog.getText(self, + "Set custom axis unit", + "

Set custom axis unit for loaded static profile

", + text=self._overlay_static_profiles[0].axisUnit) + self._lastxtitle = "%s, %s" % (name, unit) if unit else name + title = QwtText(self._lastxtitle) + title.setFont(self._font) + self._profplot.setAxisTitle(QwtPlot.xBottom, title) + if self._legend is None: + self._legend = QwtLegend() + self._profplot.insertLegend(self._legend, QwtPlot.BottomLegend) + + if self._parent_picker is not None and coord is not None: + self._parent_picker.addOverlayMarkerToCurrentProfile( + pastedname, coord, plottableprof.createPen(), index=self._currentprofile) + + def pasteActiveProfileAsStatic(self): + def __constructProfileIndex(i): + last_x = self.profiles_info.get(i, {}).get("_last_x", None) + last_y = self.profiles_info.get(i, {}).get("_last_y", None) + last_l = self.profiles_info.get(i, {}).get("_last_l", None) + last_m = self.profiles_info.get(i, {}).get("_last_m", None) + last_data_x = self.profiles_info.get(i, {}).get("_last_data_x", None) + last_data_y = self.profiles_info.get(i, {}).get("_last_data_y", None) + selaxis = self.profiles_info.get(i, {}).get("_selaxis", None) + axes = self.profiles_info.get(i, {}).get("_axes", None) + + if selaxis and selaxis[0] < len(axes) and \ + last_x is not None and \ + last_y is not None and \ + last_l is not None and \ + last_m is not None and \ + last_data_x is not None and \ + last_data_y is not None and \ + selaxis is not None and \ + axes is not None: + + profname = self.profiles_info.get(i, {}).get("_current_profile_name", "Unnamed") + axisname, axisindx, axisvals, axisunit = axes[selaxis[0]] + prof = TiggerProfile(profname, axisname, axisunit, last_data_x, last_data_y) + return (prof, i, (last_l, last_m)) + return None + + avail_profs = list(filter(lambda x: x is not None, + map(lambda i: __constructProfileIndex(i), + filter(lambda i: i != self._currentprofile, + range(self._static_profile_select.count()))))) + if len(avail_profs) == 0: + QMessageBox.critical(self, + "No active profiles available", + "

There are currently no active profiles available for selection in any other profile. " + "You need to use CTRL+ALT+leftclick on another profile to first in order to paste

") + return + + selitem, ok = QInputDialog.getItem(self, + "Paste active profile from", + "

Select from currently defined profiles:

", + list(map(lambda x: x[0].profileName, avail_profs)), + current=0, + editable=False) + if ok: + selprof, iselitem, coord = list(filter(lambda x: x[0].profileName == selitem, avail_profs))[0] + dprint(0, f"Pasting active profile from '{selprof.profileName}'") + self.addStaticProfile(selprof, coord=coord) \ No newline at end of file diff --git a/TigGUI/Plot/Utils.py b/TigGUI/Plot/Utils.py new file mode 100644 index 0000000..3621efc --- /dev/null +++ b/TigGUI/Plot/Utils.py @@ -0,0 +1,212 @@ +# Copyright (C) 2002-2022 +# The MeqTree Foundation & +# ASTRON (Netherlands Foundation for Research in Astronomy) +# P.O.Box 2, 7990 AA Dwingeloo, The Netherlands +# +# 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 2 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; if not, see , +# or write to the Free Software Foundation, Inc., +# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# + +from PyQt5.Qt import (QBrush,QColor, QImage, QPen) +from Tigger.Models import ModelClasses + +from PyQt5.Qt import (QApplication, QBrush, + QColor, QImage, QPen) +from PyQt5.QtCore import Qt +from PyQt5.Qwt import (QwtPlotItem,QwtSymbol, QwtText) + +from TigGUI.Widgets import TiggerPlotMarker +import TigGUI +_verbosity = TigGUI.kitties.utils.verbosity(name="plot") +dprint = _verbosity.dprint +dprintf = _verbosity.dprintf + +# plot Z depths for various classes of objects +Z_Image = 1000 +Z_Grid = 9000 +Z_Source = 10000 +Z_SelectedSource = 10001 +Z_CurrentSource = 10002 +Z_Markup = 10010 +Z_MarkupOverlays = 10011 + +# default stepping of grid circles +DefaultGridStep_ArcSec = 30 * 60 +import math +DEG = math.pi / 180 + +class SourceMarker: + """SourceMarker implements a source marker corresponding to a SkyModel source. + The base class implements a marker at the centre. + """ + QwtSymbolStyles = dict(none=QwtSymbol.NoSymbol, + cross=QwtSymbol.XCross, + plus=QwtSymbol.Cross, + dot=QwtSymbol.Ellipse, + circle=QwtSymbol.Ellipse, + square=QwtSymbol.Rect, + diamond=QwtSymbol.Diamond, + triangle=QwtSymbol.Triangle, + dtriangle=QwtSymbol.DTriangle, + utriangle=QwtSymbol.UTriangle, + ltriangle=QwtSymbol.LTriangle, + rtriangle=QwtSymbol.RTriangle, + hline=QwtSymbol.HLine, + vline=QwtSymbol.VLine, + star1=QwtSymbol.Star1, + star2=QwtSymbol.Star2, + hexagon=QwtSymbol.Hexagon) + + def __init__(self, src, l, m, size, model): + self.src = src + self._lm, self._size = (l, m), size + self.plotmarker = TiggerPlotMarker() + self.plotmarker.setRenderHint(QwtPlotItem.RenderAntialiased) + self.plotmarker.setValue(l, m) + self._symbol = QwtSymbol() + self._font = QApplication.font() + self._model = model + self.resetStyle() + + def lm(self): + """Returns plot coordinates of marker, as an l,m tuple""" + return self._lm + + def lmQPointF(self): + """Returns plot coordinates of marker, as a QPointF""" + return self.plotmarker.value() + + def source(self): + """Returns model source associated with marker""" + return self.src + + def attach(self, plot): + """Attaches to plot""" + self.plotmarker.attach(plot) + + def isVisible(self): + return self.plotmarker.isVisible() + + def setZ(self, z): + self.plotmarker.setZ(z) + + def resetStyle(self): + """Sets the source style based on current model settings""" + self.style, self.label = self._model.getSourcePlotStyle(self.src) + self._selected = getattr(self.src, 'selected', False) + # setup marker components + self._setupMarker(self.style, self.label) + # setup depth + if self._model.currentSource() is self.src: + self.setZ(Z_CurrentSource) + elif self._selected: + self.setZ(Z_SelectedSource) + else: + self.setZ(Z_Source) + + def _setupMarker(self, style, label): + """Sets up the plot marker (self.plotmarker) based on style object and label string. + If style=None, makes marker invisible.""" + if not style: + self.plotmarker.setVisible(False) + return + self.plotmarker.setVisible(True) + self._symbol.setStyle(self.QwtSymbolStyles.get(style.symbol, QwtSymbol.Cross)) + self._font.setPointSize(style.label_size) + symbol_color = QColor(style.symbol_color) + label_color = QColor(style.label_color) + # dots have a fixed size + if style.symbol == "dot": + self._symbol.setSize(2) + else: + self._symbol.setSize(int(self._size)) + self._symbol.setPen(QPen(symbol_color, style.symbol_linewidth)) + self._symbol.setBrush(QBrush(Qt.NoBrush)) + lab_pen = QPen(Qt.NoPen) + lab_brush = QBrush(Qt.NoBrush) + self._label = label or "" + self.plotmarker.setSymbol(self._symbol) + txt = QwtText(self._label) + txt.setColor(label_color) + txt.setFont(self._font) + txt.setBorderPen(lab_pen) + txt.setBackgroundBrush(lab_brush) + self.plotmarker.setLabel(txt) + self.plotmarker.setLabelAlignment(Qt.AlignBottom | Qt.AlignRight) + + def checkSelected(self): + """Checks the src.selected attribute, resets marker if it has changed. + Returns True is something has changed.""" + sel = getattr(self.src, 'selected', False) + if self._selected == sel: + return False + self._selected = sel + self.resetStyle() + return True + + def changeStyle(self, group): + if group.func(self.src): + self.resetStyle() + return True + return False + + +class ImageSourceMarker(SourceMarker): + """This auguments SourceMarker with a FITS image.""" + + def __init__(self, src, l, m, size, model, imgman): + # load image if needed + self.imgman = imgman + dprint(2, "loading Image source", src.shape.filename) + self.imagecon = imgman.loadImage(src.shape.filename, duplicate=False, to_top=False, model=src.name) + # this will return None if the image fails to load, in which case we still produce a marker, + # but nothing else + if self.imagecon: + self.imagecon.setMarkersZ(Z_Source) + # init base class + SourceMarker.__init__(self, src, l, m, size, model) + + def attach(self, plot): + SourceMarker.attach(self, plot) + if self.imagecon: + self.imagecon.attachToPlot(plot) + + def _setupMarker(self, style, label): + SourceMarker._setupMarker(self, style, label) + if not style: + return + symbol_color = QColor(style.symbol_color) + label_color = QColor(style.label_color) + if self.imagecon: + self.imagecon.setPlotBorderStyle(border_color=symbol_color, label_color=label_color) + +def makeSourceMarker(src, l, m, size, model, imgman): + """Creates source marker based on source type""" + shape = getattr(src, 'shape', None) + # print type(shape),isinstance(shape,ModelClasses.FITSImage),shape.__class__,ModelClasses.FITSImage + if isinstance(shape, ModelClasses.FITSImage): + return ImageSourceMarker(src, l, m, size, model, imgman) + else: + return SourceMarker(src, l, m, size, model) + + +def makeDualColorPen(color1, color2, width=3): + c1, c2 = QColor(color1).rgb(), QColor(color2).rgb() + texture = QImage(2, 2, QImage.Format_RGB32) + texture.setPixel(0, 0, c1) + texture.setPixel(1, 1, c1) + texture.setPixel(0, 1, c2) + texture.setPixel(1, 0, c2) + return QPen(QBrush(texture), width) \ No newline at end of file diff --git a/TigGUI/Plot/plottableProfiles.py b/TigGUI/Plot/plottableProfiles.py new file mode 100644 index 0000000..87751aa --- /dev/null +++ b/TigGUI/Plot/plottableProfiles.py @@ -0,0 +1,94 @@ +from PyQt5.Qt import QColor, QPen +from PyQt5.QtCore import Qt +from PyQt5.Qwt import QwtPlotCurve, QwtPlotItem + +from TigGUI.kitties.profiles import MutableTiggerProfile +from TigGUI.Widgets import TiggerPlotCurve + +class PlottableTiggerProfile(MutableTiggerProfile): + def __init__(self, profilename, axisname, axisunit, xdata, ydata, + qwtplot=None, + profilecoord=None): + """ + Plottable (Mutable) Tigger Profile + profilename: A name for this profile + axisname: Name for the axis + axisunit: Unit for the axis (as taken from FITS CUNIT) + xdata: profile x axis data (1D ndarray of shape of ydata) + ydata: profile y axis data (1D ndarray) + qwtplot: parent plot to which this curve should be added + profilecoord: coordinate (world) coord tuple to from which this profile is drawn, optional + use None to leave unset + """ + MutableTiggerProfile.__init__(self, profilename, axisname, axisunit, xdata, ydata) + self._curve_color = QColor("white") + self._curve_pen = self.createPen() + self._curve_pen.setStyle(Qt.DashDotLine) + self._profcurve = TiggerPlotCurve(profilename) + self._profcurve.setRenderHint(QwtPlotItem.RenderAntialiased) + self._ycs = TiggerPlotCurve() + self._ycs.setRenderHint(QwtPlotItem.RenderAntialiased) + self._profcurve.setPen(self._curve_pen) + self._profcurve.setStyle(QwtPlotCurve.Lines) + self._profcurve.setOrientation(Qt.Horizontal) + self._parentPlot = qwtplot + self._profilecoord = None + self.profileAssociatedCoord = profilecoord + + self._profcurve.setData(xdata, ydata) + self._profcurve.setVisible(True) + self._attached = False + self.attach() + + def createPen(self): + return QPen(self._curve_color) + + @property + def hasAssociatedCoord(self): + return self._profilecoord is not None + + @property + def profileAssociatedCoord(self): + return (self._profilecoord[0], + self._profilecoord[1]) + + @profileAssociatedCoord.setter + def profileAssociatedCoord(self, profilecoord): + if profilecoord is not None: + if not (isinstance(profilecoord, tuple) and + len(profilecoord) == 2 and + all(map(lambda x: isinstance(x, float), profilecoord))): + raise TypeError("profilecoord should be 2-element world coord tuple") + self._profilecoord = profilecoord + + def setCurveColor(self, color): + if not isinstance(color, QColor): + raise TypeError("Color must be QColor object") + self._curve_color = color + self._curve_pen = QPen(self._curve_color) + self._curve_pen.setStyle(Qt.DashDotLine) + self._profcurve.setPen(self._curve_pen) + if self._parentPlot is not None: + if self._attached: + self._parentPlot.replot() + + def attach(self): + self.detach() + if self._parentPlot is not None: + self._profcurve.attach(self._parentPlot) + self._parentPlot.replot() + self._attached = True + + def detach(self): + if self._attached: + self._attached = False + self._profcurve.detach() + + def setAxesData(self, xdata, ydata, shouldSetVisible=True): + self.__verifyArrs(xdata, ydata) + self._xdata = xdata.copy() + self._ydata = ydata.copy() + if shouldSetVisible: + self._profcurve.setData(xdata, ydata) + self._profcurve.setVisible(shouldSetVisible) + self.attach() \ No newline at end of file From 4d50c74e06bf1563aad12dc21abffbd456447f7b Mon Sep 17 00:00:00 2001 From: bennahugo Date: Tue, 26 Jul 2022 16:21:26 +0200 Subject: [PATCH 03/28] Fix naming plottableProfiles --- TigGUI/Plot/{plottableProfiles.py => PlottableProfiles.py} | 0 TigGUI/Plot/SkyModelPlot.py | 2 +- TigGUI/Plot/ToolDialogs.py | 2 +- 3 files changed, 2 insertions(+), 2 deletions(-) rename TigGUI/Plot/{plottableProfiles.py => PlottableProfiles.py} (100%) diff --git a/TigGUI/Plot/plottableProfiles.py b/TigGUI/Plot/PlottableProfiles.py similarity index 100% rename from TigGUI/Plot/plottableProfiles.py rename to TigGUI/Plot/PlottableProfiles.py diff --git a/TigGUI/Plot/SkyModelPlot.py b/TigGUI/Plot/SkyModelPlot.py index 18e2a76..d5b07e9 100644 --- a/TigGUI/Plot/SkyModelPlot.py +++ b/TigGUI/Plot/SkyModelPlot.py @@ -47,7 +47,7 @@ from TigGUI.Widgets import (TDockWidget, TigToolTip, TiggerPlotCurve, TiggerPlotMarker) from TigGUI.init import Config, pixmaps -from TigGUI.Plot.plottableProfiles import PlottableTiggerProfile +from TigGUI.Plot.PlottableProfiles import PlottableTiggerProfile import TigGUI.kitties.utils from TigGUI.kitties.utils import PersistentCurrier, curry from TigGUI.kitties.widgets import BusyIndicator diff --git a/TigGUI/Plot/ToolDialogs.py b/TigGUI/Plot/ToolDialogs.py index 741b2c8..f549863 100644 --- a/TigGUI/Plot/ToolDialogs.py +++ b/TigGUI/Plot/ToolDialogs.py @@ -35,7 +35,7 @@ from TigGUI.Widgets import TiggerPlotCurve from TigGUI.init import Config, pixmaps -from TigGUI.Plot.plottableProfiles import PlottableTiggerProfile +from TigGUI.Plot.PlottableProfiles import PlottableTiggerProfile import TigGUI.kitties.utils from TigGUI.kitties.utils import curry from TigGUI.kitties.profiles import TiggerProfile, TiggerProfileFactory From f5148982960883f2f601add0c5c00ffdd8d9d976 Mon Sep 17 00:00:00 2001 From: bennahugo Date: Tue, 26 Jul 2022 16:43:32 +0200 Subject: [PATCH 04/28] Make active and inactive selected profile marker colour user selectable --- TigGUI/Plot/SkyModelPlot.py | 36 ++++++++++++++++++++++++++++++------ TigGUI/Plot/ToolDialogs.py | 27 ++++++++++++++++++++++++++- 2 files changed, 56 insertions(+), 7 deletions(-) diff --git a/TigGUI/Plot/SkyModelPlot.py b/TigGUI/Plot/SkyModelPlot.py index d5b07e9..fbac7e9 100644 --- a/TigGUI/Plot/SkyModelPlot.py +++ b/TigGUI/Plot/SkyModelPlot.py @@ -491,11 +491,14 @@ def __init__(self, parent, mainwin, *args): self._markup_color = QColor("cyan") self._markup_pen = QPen(self._markup_color, 1) self._markup_pen.setStyle(Qt.DotLine) - self._markup_symbol_active_pen = QPen(self._stats_color, 1) - self._markup_symbol_inactive_pen = QPen(self._markup_color, 1) + self._markup_profile_active_color = QColor("red") + self._markup_profile_inactive_color = QColor("cyan") + self._markup_profile_active_pen = QPen(self._markup_profile_active_color, 1) + self._markup_profile_inactive_pen = QPen(self._markup_profile_inactive_color, 1) + self._markup_brush = QBrush(Qt.NoBrush) - self._markup_xsymbol = QwtSymbol(QwtSymbol.XCross, self._markup_brush, self._markup_symbol_inactive_pen, QSize(16, 16)) - self._markup_absymbol = QwtSymbol(QwtSymbol.Ellipse, self._markup_brush, self._markup_symbol_inactive_pen, QSize(4, 4)) + self._markup_xsymbol = QwtSymbol(QwtSymbol.XCross, self._markup_brush, self._markup_pen, QSize(16, 16)) + self._markup_absymbol = QwtSymbol(QwtSymbol.Ellipse, self._markup_brush, self._markup_pen, QSize(4, 4)) self._markup_a_label = QwtText("A") self._markup_a_label.setColor(self._markup_color) self._markup_b_label = QwtText("B") @@ -649,11 +652,11 @@ def __init__(self, parent, mainwin, *args): def _create_profile_marker_symbol(self, active=True, isoverlay=False, custompen=None): sym = QwtSymbol.Star1 if not isoverlay else QwtSymbol.Ellipse if active: - pen = custompen if custompen is not None else self._markup_symbol_active_pen + pen = custompen if custompen is not None else self._markup_profile_active_pen size = QSize(20, 20) if isoverlay else QSize(20, 20) return QwtSymbol(sym, self._markup_brush, pen, size) else: - pen = custompen if custompen is not None else self._markup_symbol_inactive_pen + pen = custompen if custompen is not None else self._markup_profile_inactive_pen size = QSize(20, 20) if isoverlay else QSize(16, 16) return QwtSymbol(sym, self._markup_brush, pen, size) @@ -779,6 +782,27 @@ def removeSelectedProfileMarkings(self, index, purge_history=False): else: self.deactivateAllOverlayMarkersFromCurrentProfile(index) + @property + def activeSelectedProfileMarkerColor(self): + return self._markup_profile_active_color + + @property + def inactiveSelectedProfileMarkerColor(self): + return self._markup_profile_inactive_color + + @activeSelectedProfileMarkerColor.setter + def activeSelectedProfileMarkerColor(self, color): + self._markup_profile_active_color = color + self._markup_profile_active_pen = QPen(self._markup_profile_active_color, 1) + self.setSelectedProfileIndex(self._selected_profile_index) + + + @inactiveSelectedProfileMarkerColor.setter + def inactiveSelectedProfileMarkerColor(self, color): + self._markup_profile_inactive_color = color + self._markup_profile_inactive_pen = QPen(self._markup_profile_inactive_color, 1) + self.setSelectedProfileIndex(self._selected_profile_index) + def close(self): self._menu.clear() self._wtoolbar.clear() diff --git a/TigGUI/Plot/ToolDialogs.py b/TigGUI/Plot/ToolDialogs.py index f549863..7d5d107 100644 --- a/TigGUI/Plot/ToolDialogs.py +++ b/TigGUI/Plot/ToolDialogs.py @@ -633,6 +633,8 @@ def __inputNewName(): self._menu.addAction("Save active profile as", self.saveProfile) self._menu.addAction("Overlay TigProf static profile from file", self.loadProfile) self._menu_opt_paste = self._menu.addAction("Overlay another active profile as static profile", self.pasteActiveProfileAsStatic) + self._menu.addAction("Set active selected profile marker colour", self.setSelProfileMarkerColour) + self._menu.addAction("Set inactive selected profile marker colour", self.setUnselProfileMarkerColour) self._profile_ctrl_btn = QToolButton() self._profile_ctrl_btn.setMenu(self._menu) self._profile_ctrl_btn.setToolTip("

Click to show options for this profile

") @@ -977,4 +979,27 @@ def __constructProfileIndex(i): if ok: selprof, iselitem, coord = list(filter(lambda x: x[0].profileName == selitem, avail_profs))[0] dprint(0, f"Pasting active profile from '{selprof.profileName}'") - self.addStaticProfile(selprof, coord=coord) \ No newline at end of file + self.addStaticProfile(selprof, coord=coord) + + def setSelProfileMarkerColour(self, color=None): + """ Set marker colour for the selected profile active profile curve """ + if self._parent_picker is not None: + default = self._parent_picker.activeSelectedProfileMarkerColor + if color is None: + color = QColorDialog.getColor(initial=default, + parent=self, + title="Select color for selected active profile marker",) + if color.isValid(): + self._parent_picker.activeSelectedProfileMarkerColor = color + + def setUnselProfileMarkerColour(self, color=None): + """ Set marker colour for the non-selected profile active profile curve """ + if self._parent_picker is not None: + default = self._parent_picker.inactiveSelectedProfileMarkerColor + if color is None: + color = QColorDialog.getColor(initial=default, + parent=self, + title="Select color for selected active profile marker",) + if color.isValid(): + self._parent_picker.inactiveSelectedProfileMarkerColor = color + \ No newline at end of file From 518a94ab19631412f746a1e33cbb8cb4e293b3b9 Mon Sep 17 00:00:00 2001 From: bennahugo Date: Tue, 26 Jul 2022 16:52:27 +0200 Subject: [PATCH 05/28] Add space before global profile marker settings --- TigGUI/Plot/ToolDialogs.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/TigGUI/Plot/ToolDialogs.py b/TigGUI/Plot/ToolDialogs.py index 7d5d107..26b0a0b 100644 --- a/TigGUI/Plot/ToolDialogs.py +++ b/TigGUI/Plot/ToolDialogs.py @@ -633,6 +633,8 @@ def __inputNewName(): self._menu.addAction("Save active profile as", self.saveProfile) self._menu.addAction("Overlay TigProf static profile from file", self.loadProfile) self._menu_opt_paste = self._menu.addAction("Overlay another active profile as static profile", self.pasteActiveProfileAsStatic) + def __sepaction(): pass + self._menu.addAction("-- Global settings --", __sepaction).setEnabled(False) self._menu.addAction("Set active selected profile marker colour", self.setSelProfileMarkerColour) self._menu.addAction("Set inactive selected profile marker colour", self.setUnselProfileMarkerColour) self._profile_ctrl_btn = QToolButton() From c228e7f04c6ae67cbc6a277ec07ad97d6cf9df18 Mon Sep 17 00:00:00 2001 From: Raz Date: Thu, 4 Aug 2022 13:07:24 +0100 Subject: [PATCH 06/28] Organised imports --- TigGUI/Plot/ToolDialogs.py | 33 ++++++++++++++++----------------- 1 file changed, 16 insertions(+), 17 deletions(-) diff --git a/TigGUI/Plot/ToolDialogs.py b/TigGUI/Plot/ToolDialogs.py index 26b0a0b..2ffde46 100644 --- a/TigGUI/Plot/ToolDialogs.py +++ b/TigGUI/Plot/ToolDialogs.py @@ -19,31 +19,30 @@ # 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # -from PyQt5.Qt import (QAction, QApplication, QCheckBox, - QColor, QComboBox, QDialog, - QFileDialog, QHBoxLayout, QInputDialog, - QLabel, QMenu, QMessageBox, QPen, - QPoint, QRectF, QSize, QSizePolicy, - QToolButton, QTransform, QVBoxLayout, - QGridLayout, QColorDialog) -from PyQt5.QtCore import Qt -from PyQt5.QtCore import pyqtSignal +import os +import numpy + +from PyQt5.Qt import (QAction, QApplication, QCheckBox, QColor, QColorDialog, + QComboBox, QDialog, QFileDialog, QGridLayout, + QHBoxLayout, QInputDialog, QLabel, QMenu, QMessageBox, + QPen, QPoint, QRectF, QSize, QSizePolicy, QToolButton, + QTransform, QVBoxLayout) +from PyQt5.QtCore import Qt, pyqtSignal from PyQt5.QtWidgets import QDockWidget, QLayout -from PyQt5.Qwt import (QwtPlot, QwtPlotCurve, QwtPlotItem, QwtText, - QwtLegend) -QStringList = list +from PyQt5.Qwt import QwtLegend, QwtPlot, QwtPlotCurve, QwtPlotItem, QwtText +import TigGUI from TigGUI.Widgets import TiggerPlotCurve from TigGUI.init import Config, pixmaps -from TigGUI.Plot.PlottableProfiles import PlottableTiggerProfile import TigGUI.kitties.utils from TigGUI.kitties.utils import curry + +from TigGUI.Plot.PlottableProfiles import PlottableTiggerProfile +from TigGUI.Plot.Utils import makeDualColorPen from TigGUI.kitties.profiles import TiggerProfile, TiggerProfileFactory -from TigGUI.Plot.Utils import makeSourceMarker, makeDualColorPen -import numpy -import os +QStringList = list + -import TigGUI _verbosity = TigGUI.kitties.utils.verbosity(name="plot") dprint = _verbosity.dprint dprintf = _verbosity.dprintf From e8a37e74e2001a4edd000d87c1172a222a240d1c Mon Sep 17 00:00:00 2001 From: Raz Date: Thu, 4 Aug 2022 13:10:02 +0100 Subject: [PATCH 07/28] Refactored setting plot axis title --- TigGUI/Plot/ToolDialogs.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/TigGUI/Plot/ToolDialogs.py b/TigGUI/Plot/ToolDialogs.py index 2ffde46..f196d6c 100644 --- a/TigGUI/Plot/ToolDialogs.py +++ b/TigGUI/Plot/ToolDialogs.py @@ -708,9 +708,7 @@ def selectProfile(self, i): QwtPlot.xBottom, self._lastxmin, self._lastxmax) self._profplot.replot() if self._lastxtitle is not None: - title = QwtText(self._lastxtitle) - title.setFont(self._font) - self._profplot.setAxisTitle(QwtPlot.xBottom, title) + self.setPlotAxisTitle() self._profplot.replot() def _profileInfosKeys(self): @@ -922,9 +920,7 @@ def addStaticProfile(self, prof, curvecol=None, coord=None): "

Set custom axis unit for loaded static profile

", text=self._overlay_static_profiles[0].axisUnit) self._lastxtitle = "%s, %s" % (name, unit) if unit else name - title = QwtText(self._lastxtitle) - title.setFont(self._font) - self._profplot.setAxisTitle(QwtPlot.xBottom, title) + self.setPlotAxisTitle() if self._legend is None: self._legend = QwtLegend() self._profplot.insertLegend(self._legend, QwtPlot.BottomLegend) @@ -932,7 +928,12 @@ def addStaticProfile(self, prof, curvecol=None, coord=None): if self._parent_picker is not None and coord is not None: self._parent_picker.addOverlayMarkerToCurrentProfile( pastedname, coord, plottableprof.createPen(), index=self._currentprofile) - + + def setPlotAxisTitle(self): + title = QwtText(self._lastxtitle) + title.setFont(self._font) + self._profplot.setAxisTitle(QwtPlot.xBottom, title) + def pasteActiveProfileAsStatic(self): def __constructProfileIndex(i): last_x = self.profiles_info.get(i, {}).get("_last_x", None) From 0e6ff44c18d40a197954d55e4d40e3ea333756be Mon Sep 17 00:00:00 2001 From: Raz Date: Thu, 4 Aug 2022 13:11:55 +0100 Subject: [PATCH 08/28] Fixed formatting --- TigGUI/Plot/ToolDialogs.py | 84 ++++++++++++++++++-------------------- 1 file changed, 40 insertions(+), 44 deletions(-) diff --git a/TigGUI/Plot/ToolDialogs.py b/TigGUI/Plot/ToolDialogs.py index f196d6c..67874e9 100644 --- a/TigGUI/Plot/ToolDialogs.py +++ b/TigGUI/Plot/ToolDialogs.py @@ -47,6 +47,7 @@ dprint = _verbosity.dprint dprintf = _verbosity.dprintf + class ToolDialog(QDialog): signalIsVisible = pyqtSignal(bool) @@ -631,8 +632,12 @@ def __inputNewName(): self._menu.addAction("Set profile name", __inputNewName) self._menu.addAction("Save active profile as", self.saveProfile) self._menu.addAction("Overlay TigProf static profile from file", self.loadProfile) - self._menu_opt_paste = self._menu.addAction("Overlay another active profile as static profile", self.pasteActiveProfileAsStatic) - def __sepaction(): pass + self._menu_opt_paste = self._menu.addAction( + "Overlay another active profile as static profile", + self.pasteActiveProfileAsStatic) + + def __sepaction(): + pass self._menu.addAction("-- Global settings --", __sepaction).setEnabled(False) self._menu.addAction("Set active selected profile marker colour", self.setSelProfileMarkerColour) self._menu.addAction("Set inactive selected profile marker colour", self.setUnselProfileMarkerColour) @@ -699,18 +704,16 @@ def selectProfile(self, i): # if the profile has no active profile # -- other static profiles may have been loaded # to an empty profile - if (self._last_data_x is None or \ - self._last_data_y is None) and \ - len(self._overlay_static_profiles) > 0: - if self._lastxmin is not None and \ - self._lastxmax is not None: - self._profplot.setAxisScale( - QwtPlot.xBottom, self._lastxmin, self._lastxmax) + if ((self._last_data_x is None or self._last_data_y is None) + and len(self._overlay_static_profiles) > 0): + if self._lastxmin is not None and self._lastxmax is not None: + self._profplot.setAxisScale(QwtPlot.xBottom, + self._lastxmin, self._lastxmax) self._profplot.replot() if self._lastxtitle is not None: self.setPlotAxisTitle() self._profplot.replot() - + def _profileInfosKeys(self): return ["_lastsel", "_image_id", "_image_hnd", "_last_x", "_last_y", @@ -875,8 +878,8 @@ def loadProfile(self, filename=None): self.addStaticProfile(prof) def addStaticProfile(self, prof, curvecol=None, coord=None): - pastedname, ok = QInputDialog.getText(self, - "Set pasted profile name", + pastedname, ok = QInputDialog.getText(self, + "Set pasted profile name", "

Set name of pasted profile

", text=prof.profileName) plottableprof = PlottableTiggerProfile(pastedname, @@ -896,11 +899,9 @@ def addStaticProfile(self, prof, curvecol=None, coord=None): if self._overlay_static_profiles is None: self._overlay_static_profiles = [] self._overlay_static_profiles.append(plottableprof) - if (self._last_data_x is None or - self._last_data_y is None or - self._last_data_x is None or - self._last_data_y is None) and \ - len(self._overlay_static_profiles) > 0: + if ((self._last_data_x is None or self._last_data_y is None + or self._last_data_x is None or self._last_data_y is None) + and len(self._overlay_static_profiles) > 0): # temporary titles and labels from pasted profiles xmin = numpy.nanmin(list(map(lambda x: numpy.nanmin(x.xdata), self._overlay_static_profiles))) @@ -911,19 +912,19 @@ def addStaticProfile(self, prof, curvecol=None, coord=None): self._profplot.setAxisScale(QwtPlot.xBottom, self._lastxmin, self._lastxmax) # set custom label if first loaded profile if len(self._overlay_static_profiles) == 1: - name, ok = QInputDialog.getText(self, - "Set custom axis name", + name, ok = QInputDialog.getText(self, + "Set custom axis name", "

Set custom axis name for loaded static profile

", text=self._overlay_static_profiles[0].axisName) - unit, ok = QInputDialog.getText(self, - "Set custom axis unit", + unit, ok = QInputDialog.getText(self, + "Set custom axis unit", "

Set custom axis unit for loaded static profile

", text=self._overlay_static_profiles[0].axisUnit) self._lastxtitle = "%s, %s" % (name, unit) if unit else name self.setPlotAxisTitle() if self._legend is None: self._legend = QwtLegend() - self._profplot.insertLegend(self._legend, QwtPlot.BottomLegend) + self._profplot.insertLegend(self._legend, QwtPlot.BottomLegend) if self._parent_picker is not None and coord is not None: self._parent_picker.addOverlayMarkerToCurrentProfile( @@ -945,33 +946,29 @@ def __constructProfileIndex(i): selaxis = self.profiles_info.get(i, {}).get("_selaxis", None) axes = self.profiles_info.get(i, {}).get("_axes", None) - if selaxis and selaxis[0] < len(axes) and \ - last_x is not None and \ - last_y is not None and \ - last_l is not None and \ - last_m is not None and \ - last_data_x is not None and \ - last_data_y is not None and \ - selaxis is not None and \ - axes is not None: - - profname = self.profiles_info.get(i, {}).get("_current_profile_name", "Unnamed") - axisname, axisindx, axisvals, axisunit = axes[selaxis[0]] - prof = TiggerProfile(profname, axisname, axisunit, last_data_x, last_data_y) - return (prof, i, (last_l, last_m)) + if (selaxis and selaxis[0] < len(axes) and last_x is not None + and last_y is not None and last_l is not None + and last_m is not None and last_data_x is not None + and last_data_y is not None and selaxis is not None + and axes is not None): + profname = self.profiles_info.get(i, {}).get("_current_profile_name", "Unnamed") + axisname, axisindx, axisvals, axisunit = axes[selaxis[0]] + prof = TiggerProfile(profname, axisname, axisunit, last_data_x, last_data_y) + return (prof, i, (last_l, last_m)) return None - + avail_profs = list(filter(lambda x: x is not None, map(lambda i: __constructProfileIndex(i), filter(lambda i: i != self._currentprofile, range(self._static_profile_select.count()))))) if len(avail_profs) == 0: - QMessageBox.critical(self, - "No active profiles available", - "

There are currently no active profiles available for selection in any other profile. " - "You need to use CTRL+ALT+leftclick on another profile to first in order to paste

") + QMessageBox.critical( + self, "No active profiles available", + "

There are currently no active profiles available for selection in any other profile. " + "You need to use CTRL+ALT+leftclick on another profile to first in order to paste

" + ) return - + selitem, ok = QInputDialog.getItem(self, "Paste active profile from", "

Select from currently defined profiles:

", @@ -993,7 +990,7 @@ def setSelProfileMarkerColour(self, color=None): title="Select color for selected active profile marker",) if color.isValid(): self._parent_picker.activeSelectedProfileMarkerColor = color - + def setUnselProfileMarkerColour(self, color=None): """ Set marker colour for the non-selected profile active profile curve """ if self._parent_picker is not None: @@ -1004,4 +1001,3 @@ def setUnselProfileMarkerColour(self, color=None): title="Select color for selected active profile marker",) if color.isValid(): self._parent_picker.inactiveSelectedProfileMarkerColor = color - \ No newline at end of file From ff82bd8acc78e223617a61f1afd89ca2382c0024 Mon Sep 17 00:00:00 2001 From: Raz Date: Thu, 4 Aug 2022 13:12:31 +0100 Subject: [PATCH 09/28] Fixed unbound variable --- TigGUI/Plot/ToolDialogs.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/TigGUI/Plot/ToolDialogs.py b/TigGUI/Plot/ToolDialogs.py index 67874e9..61aace8 100644 --- a/TigGUI/Plot/ToolDialogs.py +++ b/TigGUI/Plot/ToolDialogs.py @@ -875,7 +875,8 @@ def loadProfile(self, filename=None): "Error loading TigProf profile", f"Loading failed with message '{str(e)}'" ) - self.addStaticProfile(prof) + else: + self.addStaticProfile(prof) def addStaticProfile(self, prof, curvecol=None, coord=None): pastedname, ok = QInputDialog.getText(self, From 103b5629d5d283eeec75cfc5625f50b3f659cfd6 Mon Sep 17 00:00:00 2001 From: Raz Date: Thu, 4 Aug 2022 13:15:18 +0100 Subject: [PATCH 10/28] Organised imports --- TigGUI/Plot/Utils.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/TigGUI/Plot/Utils.py b/TigGUI/Plot/Utils.py index 3621efc..cb8b7e8 100644 --- a/TigGUI/Plot/Utils.py +++ b/TigGUI/Plot/Utils.py @@ -19,16 +19,16 @@ # 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # -from PyQt5.Qt import (QBrush,QColor, QImage, QPen) -from Tigger.Models import ModelClasses +import math -from PyQt5.Qt import (QApplication, QBrush, - QColor, QImage, QPen) +from PyQt5.Qt import QApplication, QBrush, QColor, QImage, QPen from PyQt5.QtCore import Qt -from PyQt5.Qwt import (QwtPlotItem,QwtSymbol, QwtText) +from PyQt5.Qwt import QwtPlotItem, QwtSymbol, QwtText -from TigGUI.Widgets import TiggerPlotMarker import TigGUI +from TigGUI.Widgets import TiggerPlotMarker +from Tigger.Models import ModelClasses + _verbosity = TigGUI.kitties.utils.verbosity(name="plot") dprint = _verbosity.dprint dprintf = _verbosity.dprintf @@ -44,7 +44,7 @@ # default stepping of grid circles DefaultGridStep_ArcSec = 30 * 60 -import math + DEG = math.pi / 180 class SourceMarker: From b72a6d2671db7a59ddc5fa9ceaaf7edc2be2895f Mon Sep 17 00:00:00 2001 From: Raz Date: Thu, 4 Aug 2022 13:15:49 +0100 Subject: [PATCH 11/28] Fixed formatting --- TigGUI/Plot/Utils.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/TigGUI/Plot/Utils.py b/TigGUI/Plot/Utils.py index cb8b7e8..f802fe7 100644 --- a/TigGUI/Plot/Utils.py +++ b/TigGUI/Plot/Utils.py @@ -47,6 +47,7 @@ DEG = math.pi / 180 + class SourceMarker: """SourceMarker implements a source marker corresponding to a SkyModel source. The base class implements a marker at the centre. @@ -192,6 +193,7 @@ def _setupMarker(self, style, label): if self.imagecon: self.imagecon.setPlotBorderStyle(border_color=symbol_color, label_color=label_color) + def makeSourceMarker(src, l, m, size, model, imgman): """Creates source marker based on source type""" shape = getattr(src, 'shape', None) @@ -209,4 +211,4 @@ def makeDualColorPen(color1, color2, width=3): texture.setPixel(1, 1, c1) texture.setPixel(0, 1, c2) texture.setPixel(1, 0, c2) - return QPen(QBrush(texture), width) \ No newline at end of file + return QPen(QBrush(texture), width) From 5042cb0880726af846e3f15bc0ef9d104c132eac Mon Sep 17 00:00:00 2001 From: Raz Date: Thu, 4 Aug 2022 16:10:42 +0100 Subject: [PATCH 12/28] Organised imports --- TigGUI/Plot/SkyModelPlot.py | 36 ++++++++++++++---------------------- 1 file changed, 14 insertions(+), 22 deletions(-) diff --git a/TigGUI/Plot/SkyModelPlot.py b/TigGUI/Plot/SkyModelPlot.py index fbac7e9..23cd47a 100644 --- a/TigGUI/Plot/SkyModelPlot.py +++ b/TigGUI/Plot/SkyModelPlot.py @@ -20,18 +20,16 @@ # import math -from multiprocessing import set_start_method import re import time +import numpy -from PyQt5.Qt import (QActionGroup, QApplication, QBrush, - QClipboard, QColor, QCoreApplication, QDialog, - QEvent, QFileDialog, QHBoxLayout, QImage, QInputDialog, - QMenu, QMessageBox, QPainter, QPen, QPixmap, - QPoint, QPointF, QRectF, QSize, QSizePolicy, QTimer, - QToolBar, QWidget) -from PyQt5.QtCore import Qt -from PyQt5.QtCore import pyqtSignal +from PyQt5.Qt import (QActionGroup, QApplication, QBrush, QClipboard, QColor, + QCoreApplication, QDialog, QEvent, QFileDialog, + QHBoxLayout, QInputDialog, QMenu, QMessageBox, + QPainter, QPen, QPixmap, QPoint, QPointF, QRectF, QSize, + QSizePolicy, QTimer, QToolBar, QWidget) +from PyQt5.QtCore import Qt, pyqtSignal from PyQt5.QtWidgets import QDockWidget from PyQt5.Qwt import (QwtEventPattern, QwtPicker, QwtPickerClickPointMachine, QwtPickerClickRectMachine, QwtPickerDragLineMachine, @@ -39,28 +37,22 @@ QwtPlot, QwtPlotCurve, QwtPlotItem, QwtPlotPicker, QwtPlotZoomer, QwtScaleEngine, QwtSymbol, QwtText) -from TigGUI.Plot.ToolDialogs import (LiveImageZoom, - LiveProfile, SelectedProfile) - -from TigGUI.Images.ControlDialog import ImageControlDialog -from TigGUI.Plot import MouseModes -from TigGUI.Widgets import (TDockWidget, TigToolTip, TiggerPlotCurve, - TiggerPlotMarker) +from TigGUI.Widgets import TDockWidget, TigToolTip, TiggerPlotCurve, TiggerPlotMarker from TigGUI.init import Config, pixmaps -from TigGUI.Plot.PlottableProfiles import PlottableTiggerProfile import TigGUI.kitties.utils from TigGUI.kitties.utils import PersistentCurrier, curry from TigGUI.kitties.widgets import BusyIndicator -from TigGUI.Plot.Utils import makeSourceMarker, makeDualColorPen + from Tigger import Coordinates from Tigger.Coordinates import Projection from Tigger.Models import ModelClasses from Tigger.Models.SkyModel import SkyModel -from TigGUI.Plot.Utils import (Z_CurrentSource, Z_Grid, Z_Image, Z_Markup, - Z_SelectedSource, Z_MarkupOverlays, Z_Source, DefaultGridStep_ArcSec, - DEG) -import numpy +from TigGUI.Plot import MouseModes +from TigGUI.Plot.ToolDialogs import LiveImageZoom, LiveProfile, SelectedProfile +from TigGUI.Plot.Utils import makeDualColorPen, makeSourceMarker +from TigGUI.Plot.Utils import (DEG, DefaultGridStep_ArcSec, Z_Grid, Z_Image, + Z_Markup, Z_MarkupOverlays) QStringList = list From 86372d3fb5b4022452f514949812602e5a96e86e Mon Sep 17 00:00:00 2001 From: Raz Date: Thu, 4 Aug 2022 16:11:01 +0100 Subject: [PATCH 13/28] Fixed formatting --- TigGUI/Plot/SkyModelPlot.py | 1 + 1 file changed, 1 insertion(+) diff --git a/TigGUI/Plot/SkyModelPlot.py b/TigGUI/Plot/SkyModelPlot.py index 23cd47a..bff2d25 100644 --- a/TigGUI/Plot/SkyModelPlot.py +++ b/TigGUI/Plot/SkyModelPlot.py @@ -60,6 +60,7 @@ dprint = _verbosity.dprint dprintf = _verbosity.dprintf + class SkyModelPlotter(QWidget): # Selection modes for the various selector functions below. # Default is usually Clear+Add From ced7f70af1cae62a395a664d020824ca3d057626 Mon Sep 17 00:00:00 2001 From: Raz Date: Thu, 4 Aug 2022 16:16:04 +0100 Subject: [PATCH 14/28] Added copyright notice --- TigGUI/Plot/PlottableProfiles.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/TigGUI/Plot/PlottableProfiles.py b/TigGUI/Plot/PlottableProfiles.py index 87751aa..ef94cda 100644 --- a/TigGUI/Plot/PlottableProfiles.py +++ b/TigGUI/Plot/PlottableProfiles.py @@ -1,3 +1,24 @@ +# Copyright (C) 2002-2022 +# The MeqTree Foundation & +# ASTRON (Netherlands Foundation for Research in Astronomy) +# P.O.Box 2, 7990 AA Dwingeloo, The Netherlands +# +# 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 2 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; if not, see , +# or write to the Free Software Foundation, Inc., +# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# + from PyQt5.Qt import QColor, QPen from PyQt5.QtCore import Qt from PyQt5.Qwt import QwtPlotCurve, QwtPlotItem From ad885d3b8c0994741f946873826b1c1f58639906 Mon Sep 17 00:00:00 2001 From: Raz Date: Thu, 4 Aug 2022 16:16:44 +0100 Subject: [PATCH 15/28] Organised imports --- TigGUI/Plot/PlottableProfiles.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/TigGUI/Plot/PlottableProfiles.py b/TigGUI/Plot/PlottableProfiles.py index ef94cda..5c1e4d2 100644 --- a/TigGUI/Plot/PlottableProfiles.py +++ b/TigGUI/Plot/PlottableProfiles.py @@ -23,9 +23,11 @@ from PyQt5.QtCore import Qt from PyQt5.Qwt import QwtPlotCurve, QwtPlotItem -from TigGUI.kitties.profiles import MutableTiggerProfile from TigGUI.Widgets import TiggerPlotCurve +from TigGUI.kitties.profiles import MutableTiggerProfile + + class PlottableTiggerProfile(MutableTiggerProfile): def __init__(self, profilename, axisname, axisunit, xdata, ydata, qwtplot=None, From 0001e050dcc697561d572e5223a86441d6823782 Mon Sep 17 00:00:00 2001 From: Raz Date: Thu, 4 Aug 2022 16:17:24 +0100 Subject: [PATCH 16/28] Fixed formatting --- TigGUI/Plot/PlottableProfiles.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/TigGUI/Plot/PlottableProfiles.py b/TigGUI/Plot/PlottableProfiles.py index 5c1e4d2..fd02362 100644 --- a/TigGUI/Plot/PlottableProfiles.py +++ b/TigGUI/Plot/PlottableProfiles.py @@ -29,10 +29,10 @@ class PlottableTiggerProfile(MutableTiggerProfile): - def __init__(self, profilename, axisname, axisunit, xdata, ydata, - qwtplot=None, + def __init__(self, profilename, axisname, axisunit, xdata, ydata, + qwtplot=None, profilecoord=None): - """ + """ Plottable (Mutable) Tigger Profile profilename: A name for this profile axisname: Name for the axis @@ -45,7 +45,7 @@ def __init__(self, profilename, axisname, axisunit, xdata, ydata, """ MutableTiggerProfile.__init__(self, profilename, axisname, axisunit, xdata, ydata) self._curve_color = QColor("white") - self._curve_pen = self.createPen() + self._curve_pen = self.createPen() self._curve_pen.setStyle(Qt.DashDotLine) self._profcurve = TiggerPlotCurve(profilename) self._profcurve.setRenderHint(QwtPlotItem.RenderAntialiased) @@ -69,16 +69,16 @@ def createPen(self): @property def hasAssociatedCoord(self): return self._profilecoord is not None - + @property def profileAssociatedCoord(self): - return (self._profilecoord[0], + return (self._profilecoord[0], self._profilecoord[1]) - + @profileAssociatedCoord.setter def profileAssociatedCoord(self, profilecoord): if profilecoord is not None: - if not (isinstance(profilecoord, tuple) and + if not (isinstance(profilecoord, tuple) and len(profilecoord) == 2 and all(map(lambda x: isinstance(x, float), profilecoord))): raise TypeError("profilecoord should be 2-element world coord tuple") @@ -101,7 +101,7 @@ def attach(self): self._profcurve.attach(self._parentPlot) self._parentPlot.replot() self._attached = True - + def detach(self): if self._attached: self._attached = False @@ -114,4 +114,4 @@ def setAxesData(self, xdata, ydata, shouldSetVisible=True): if shouldSetVisible: self._profcurve.setData(xdata, ydata) self._profcurve.setVisible(shouldSetVisible) - self.attach() \ No newline at end of file + self.attach() From 31973f9b8c2b905033ef0a25cd18c940f54b4672 Mon Sep 17 00:00:00 2001 From: Raz Date: Thu, 4 Aug 2022 16:20:37 +0100 Subject: [PATCH 17/28] Fixed formatting --- TigGUI/Plot/SkyModelPlot.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/TigGUI/Plot/SkyModelPlot.py b/TigGUI/Plot/SkyModelPlot.py index bff2d25..4caeec6 100644 --- a/TigGUI/Plot/SkyModelPlot.py +++ b/TigGUI/Plot/SkyModelPlot.py @@ -694,7 +694,7 @@ def _addBackAllSelectedProfileMarkers(self, index=0): self._selected_profile_markup[index]["overlays"][item]["marker"].setZ(Z_MarkupOverlays) markup_items.append(self._selected_profile_markup[index]["overlays"][item]["marker"]) QTimer.singleShot(10, self._currier.curry( - self._addPlotMarkup, + self._addPlotMarkup, markup_items)) def addOverlayMarkerToCurrentProfile(self, name, position, qtpen, index=None): @@ -715,7 +715,7 @@ def __initoverlaymarker(position=position, qtpen=qtpen): # detach any existing markers to set new position or color if name in self._selected_profile_markup[index]["overlays"]: self._selected_profile_markup[index]["overlays"][name]["marker"].detach() - + self._selected_profile_markup[index]["overlays"][name] = \ __initoverlaymarker() self._addBackAllSelectedProfileMarkers(index) @@ -746,7 +746,7 @@ def removeAllOverlayMarkersFromCurrentProfile(self, index=None): for name in list(self._selected_profile_markup.get( index, SkyModelPlotter._giveDefaultSelectedMarkerInfos())["overlays"].keys()): self.removeOverlayMarkerFromCurrentProfile(name, index) - + def deactivateAllOverlayMarkersFromCurrentProfile(self, index=None): """ Detach all overlay markers from current profile """ index = self._selected_profile_index if index is None else index @@ -764,7 +764,7 @@ def removeAllSelectedProfileMarkings(self): def removeSelectedProfileMarkings(self, index, purge_history=False): """ Remove selected profile marking purge_history: remove position and marking from history - """ + """ if index in self._selected_profile_markup: marker = self._selected_profile_markup[index]["active"]['marker'] if marker is not None: @@ -1400,7 +1400,7 @@ def _removePlotMarkup(self, replot=True): """Removes all markup items, and refreshes the plot if replot=True""" for item in self._plot_markup: if item is None: continue - if not item in map(lambda k: self._selected_profile_markup[k]["active"]["marker"], + if not item in map(lambda k: self._selected_profile_markup[k]["active"]["marker"], self._selected_profile_markup): item.detach() if self._plot_markup and replot: @@ -1471,7 +1471,7 @@ def __initMarker(markerindex): marker.setSymbol(self._create_profile_marker_symbol(active=True)) return marker if self._selected_profile_markup.setdefault( - self._selected_profile_index, + self._selected_profile_index, SkyModelPlotter._giveDefaultSelectedMarkerInfos())["active"]["marker"] is None: self._selected_profile_markup[self._selected_profile_index]["active"]["marker"] = \ __initMarker(self._selected_profile_index) From 309e32046d6c4683d63fe91d1a063092dbfef417 Mon Sep 17 00:00:00 2001 From: Raz Date: Thu, 4 Aug 2022 16:36:16 +0100 Subject: [PATCH 18/28] Organised imports --- TigGUI/kitties/profiles.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/TigGUI/kitties/profiles.py b/TigGUI/kitties/profiles.py index 4872a5c..ab5b805 100644 --- a/TigGUI/kitties/profiles.py +++ b/TigGUI/kitties/profiles.py @@ -19,10 +19,11 @@ # 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # -import numpy as np import json +import numpy as np from TigGUI.kitties.utils import verbosity + _verbosity = verbosity(name="profiles") dprint = _verbosity.dprint dprintf = _verbosity.dprintf From 9f99e4cfe85329a68a757ebc96727954cce42d1c Mon Sep 17 00:00:00 2001 From: Raz Date: Thu, 4 Aug 2022 16:37:25 +0100 Subject: [PATCH 19/28] Fix exception for not implemented --- TigGUI/kitties/profiles.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/TigGUI/kitties/profiles.py b/TigGUI/kitties/profiles.py index ab5b805..6cb80cb 100644 --- a/TigGUI/kitties/profiles.py +++ b/TigGUI/kitties/profiles.py @@ -144,8 +144,8 @@ def axisUnit(self, unit): class TiggerProfileFactory: def __init__(self, filename): - raise NotImplemented("Factory cannot be instantiated!") - + raise NotImplementedError("Factory cannot be instantiated!") + @classmethod def load(cls, filename): """ Loads a TigProf profile from file """ From b60c7672bf669558ae6d367703dbd98b23b55302 Mon Sep 17 00:00:00 2001 From: Raz Date: Thu, 4 Aug 2022 16:37:55 +0100 Subject: [PATCH 20/28] Explicitly raise from previous error --- TigGUI/kitties/profiles.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/TigGUI/kitties/profiles.py b/TigGUI/kitties/profiles.py index 6cb80cb..55e548b 100644 --- a/TigGUI/kitties/profiles.py +++ b/TigGUI/kitties/profiles.py @@ -153,7 +153,10 @@ def load(cls, filename): try: prof = json.load(fprof) except json.JSONDecodeError as e: - raise IOError(f"TigProf profile '{filename}' corrupted. Not valid json.") + raise IOError( + f"TigProf profile '{filename}' corrupted. Not valid json." + ) from e + __mandatory = set(["version", "profile_name", "axis", "units", "x_data", "y_data"]) for c in __mandatory: From 9707e0566715f39e9e799a7f31b19369c73f8f03 Mon Sep 17 00:00:00 2001 From: Raz Date: Thu, 4 Aug 2022 16:38:43 +0100 Subject: [PATCH 21/28] Removed bare except clause --- TigGUI/kitties/profiles.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/TigGUI/kitties/profiles.py b/TigGUI/kitties/profiles.py index 55e548b..b310761 100644 --- a/TigGUI/kitties/profiles.py +++ b/TigGUI/kitties/profiles.py @@ -172,9 +172,9 @@ def load(cls, filename): f"than supported profile version {supvstr}. " \ f"Attempting to convert to version {supvstr}." dprint(0, msg) - except: - raise IOError("Error parsing TigProf file version") - + except Exception as exc: + raise IOError("Error parsing TigProf file version") from exc + profname = prof["profile_name"] axisname = prof["axis"] axisunits = prof["units"] From 6dcfa4fc1f1f61a4b59545fbd3f555106510c5e9 Mon Sep 17 00:00:00 2001 From: Raz Date: Thu, 4 Aug 2022 16:39:50 +0100 Subject: [PATCH 22/28] Fixed formatting --- TigGUI/kitties/profiles.py | 34 +++++++++++++++++++--------------- 1 file changed, 19 insertions(+), 15 deletions(-) diff --git a/TigGUI/kitties/profiles.py b/TigGUI/kitties/profiles.py index b310761..bd916f4 100644 --- a/TigGUI/kitties/profiles.py +++ b/TigGUI/kitties/profiles.py @@ -28,12 +28,14 @@ dprint = _verbosity.dprint dprintf = _verbosity.dprintf + class TiggerProfile: __VER_MAJ__ = 1 __VER_MIN__ = 0 + def __init__(self, profilename, axisname, axisunit, xdata, ydata): - """ - Immutable Tigger profile + """ + Immutable Tigger profile profilename: A name for this profile axisname: Name for the axis axisunit: Unit for the axis (as taken from FITS CUNIT) @@ -66,7 +68,7 @@ def __verifyArrs(self, xdata, ydata): @property def xdata(self): return self._xdata.copy() - + @property def ydata(self): return self._ydata.copy() @@ -96,14 +98,15 @@ def saveProfile(self, filename): "x_data": list(self._xdata), "y_data": list(self._ydata) } - with open(filename, "w+") as fprof: + with open(filename, "w+") as fprof: fprof.write(json.dumps(prof)) - + dprint(0, f"Saved current selected profile as {filename}") + class MutableTiggerProfile(TiggerProfile): - """ - Mutable Tigger profile + """ + Mutable Tigger profile profilename: A name for this profile axisname: Name for the axis axisunit: Unit for the axis (as taken from FITS CUNIT) @@ -142,6 +145,7 @@ def axisName(self, name): def axisUnit(self, unit): self._axisunit = unit + class TiggerProfileFactory: def __init__(self, filename): raise NotImplementedError("Factory cannot be instantiated!") @@ -162,7 +166,7 @@ def load(cls, filename): for c in __mandatory: if c not in prof: print(f"Profile file '{filename}' is missing field '{c}'") - + try: vmaj, vmin = prof.get("version", "").split(".") vstr = f"{vmaj}.{vmin}" @@ -179,13 +183,13 @@ def load(cls, filename): axisname = prof["axis"] axisunits = prof["units"] - if not isinstance(prof["x_data"], list) and \ - not all(map(lambda x: isinstance(x, float), prof["x_data"])): + if (not isinstance(prof["x_data"], list) and not all( + map(lambda x: isinstance(x, float), prof["x_data"]))): raise IOError("Stored X data is not list of floats") xdata = np.array(prof["x_data"]) - - if not isinstance(prof["y_data"], list) and \ - not all(map(lambda x: isinstance(x, float), prof["y_data"])): + + if (not isinstance(prof["y_data"], list) and not all( + map(lambda x: isinstance(x, float), prof["y_data"]))): raise IOError("Stored Y data is not list of floats") ydata = np.array(prof["y_data"]) @@ -197,8 +201,8 @@ def load(cls, filename): if xdata.size != ydata.size: raise IOError("Stored X data not the same shape as Y data") - + # Success tigprof = TiggerProfile(profname, axisname, axisunits, xdata, ydata) dprint(0, f"Loaded profile from {filename}") - return tigprof \ No newline at end of file + return tigprof From 6fa6531cc2df237ba69d77a8d1b5b4578aafc09f Mon Sep 17 00:00:00 2001 From: Raz Date: Thu, 4 Aug 2022 17:40:55 +0100 Subject: [PATCH 23/28] Fixed saving and loading of JSON profiles from file --- TigGUI/kitties/profiles.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/TigGUI/kitties/profiles.py b/TigGUI/kitties/profiles.py index bd916f4..1198e28 100644 --- a/TigGUI/kitties/profiles.py +++ b/TigGUI/kitties/profiles.py @@ -95,11 +95,11 @@ def saveProfile(self, filename): "profile_name": self._profilename, "axis": self._axisname, "units": self._axisunit, - "x_data": list(self._xdata), - "y_data": list(self._ydata) + "x_data": self._xdata.tolist(), + "y_data": self._ydata.tolist() } with open(filename, "w+") as fprof: - fprof.write(json.dumps(prof)) + json.dump(prof, fprof, indent=4) dprint(0, f"Saved current selected profile as {filename}") @@ -155,7 +155,8 @@ def load(cls, filename): """ Loads a TigProf profile from file """ with open(filename, "r") as fprof: try: - prof = json.load(fprof) + fprof_content = fprof.read() + prof = json.loads(fprof_content) except json.JSONDecodeError as e: raise IOError( f"TigProf profile '{filename}' corrupted. Not valid json." From addce94b6fa07d7871e6b36e1a6036a41b0e3d5f Mon Sep 17 00:00:00 2001 From: Raz Date: Mon, 15 Aug 2022 11:19:57 +0100 Subject: [PATCH 24/28] Changed dashed lines for solid lines for clearer display --- TigGUI/Plot/PlottableProfiles.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/TigGUI/Plot/PlottableProfiles.py b/TigGUI/Plot/PlottableProfiles.py index fd02362..70dd76c 100644 --- a/TigGUI/Plot/PlottableProfiles.py +++ b/TigGUI/Plot/PlottableProfiles.py @@ -46,7 +46,7 @@ def __init__(self, profilename, axisname, axisunit, xdata, ydata, MutableTiggerProfile.__init__(self, profilename, axisname, axisunit, xdata, ydata) self._curve_color = QColor("white") self._curve_pen = self.createPen() - self._curve_pen.setStyle(Qt.DashDotLine) + self._curve_pen.setStyle(Qt.SolidLine) self._profcurve = TiggerPlotCurve(profilename) self._profcurve.setRenderHint(QwtPlotItem.RenderAntialiased) self._ycs = TiggerPlotCurve() @@ -89,7 +89,7 @@ def setCurveColor(self, color): raise TypeError("Color must be QColor object") self._curve_color = color self._curve_pen = QPen(self._curve_color) - self._curve_pen.setStyle(Qt.DashDotLine) + self._curve_pen.setStyle(Qt.SolidLine) self._profcurve.setPen(self._curve_pen) if self._parentPlot is not None: if self._attached: From ed4fa4ac57e4f0c93c3e5ce050e87538d2f3e2f1 Mon Sep 17 00:00:00 2001 From: Raz Date: Mon, 15 Aug 2022 12:42:51 +0100 Subject: [PATCH 25/28] Fixed formatting --- TigGUI/Plot/SkyModelPlot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/TigGUI/Plot/SkyModelPlot.py b/TigGUI/Plot/SkyModelPlot.py index 4caeec6..7d33a47 100644 --- a/TigGUI/Plot/SkyModelPlot.py +++ b/TigGUI/Plot/SkyModelPlot.py @@ -656,7 +656,7 @@ def _create_profile_marker_symbol(self, active=True, isoverlay=False, custompen= @classmethod def _giveDefaultSelectedMarkerInfos(cls): return {"active": {"marker": None, "position": None}, - "overlays" : {}} + "overlays": {}} def setSelectedProfileIndex(self, index=0): # Invalidate other profile markers From cef4a37d0c89896aa8f03fe9034698bcb7d47413 Mon Sep 17 00:00:00 2001 From: Raz Date: Mon, 15 Aug 2022 13:04:00 +0100 Subject: [PATCH 26/28] Simplify logical expression using De-Morgan identities --- TigGUI/Plot/SkyModelPlot.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/TigGUI/Plot/SkyModelPlot.py b/TigGUI/Plot/SkyModelPlot.py index 7d33a47..569f2f8 100644 --- a/TigGUI/Plot/SkyModelPlot.py +++ b/TigGUI/Plot/SkyModelPlot.py @@ -685,8 +685,10 @@ def setSelectedProfileIndex(self, index=0): def _addBackAllSelectedProfileMarkers(self, index=0): """ Remove and add back all overlays associated to profiles """ - markup_items = [self._selected_profile_markup[k]["active"]["marker"] - for k in self._selected_profile_markup] + markup_items = [ + self._selected_profile_markup[k]["active"]["marker"] + for k in self._selected_profile_markup + ] for item in markup_items: if item is not None: item.setZ(Z_Markup) @@ -1399,9 +1401,12 @@ def _addPlotMarkup(self, items): def _removePlotMarkup(self, replot=True): """Removes all markup items, and refreshes the plot if replot=True""" for item in self._plot_markup: - if item is None: continue - if not item in map(lambda k: self._selected_profile_markup[k]["active"]["marker"], - self._selected_profile_markup): + if item is None: + continue + if item not in map( + lambda k: self._selected_profile_markup[k]["active"]["marker"], + self._selected_profile_markup, + ): item.detach() if self._plot_markup and replot: self.tigToolTip.hideText() From 5f4e2102362f44bead7bffc131967a184c27c0d6 Mon Sep 17 00:00:00 2001 From: Raz Date: Mon, 15 Aug 2022 13:05:36 +0100 Subject: [PATCH 27/28] Fixed formatting --- TigGUI/Plot/SkyModelPlot.py | 33 ++++++++++++++++++--------------- 1 file changed, 18 insertions(+), 15 deletions(-) diff --git a/TigGUI/Plot/SkyModelPlot.py b/TigGUI/Plot/SkyModelPlot.py index 569f2f8..28c4cd2 100644 --- a/TigGUI/Plot/SkyModelPlot.py +++ b/TigGUI/Plot/SkyModelPlot.py @@ -437,7 +437,6 @@ def __init__(self, parent, mainwin, *args): # will contain "marker" object and "position" (l, m) tupple # on the first click to set position self._selected_profile_markup = {} - self._mainwin = mainwin self.tigToolTip = TigToolTip() self._ruler_timer = QTimer() @@ -488,7 +487,6 @@ def __init__(self, parent, mainwin, *args): self._markup_profile_inactive_color = QColor("cyan") self._markup_profile_active_pen = QPen(self._markup_profile_active_color, 1) self._markup_profile_inactive_pen = QPen(self._markup_profile_inactive_color, 1) - self._markup_brush = QBrush(Qt.NoBrush) self._markup_xsymbol = QwtSymbol(QwtSymbol.XCross, self._markup_brush, self._markup_pen, QSize(16, 16)) self._markup_absymbol = QwtSymbol(QwtSymbol.Ellipse, self._markup_brush, self._markup_pen, QSize(4, 4)) @@ -542,7 +540,6 @@ def __init__(self, parent, mainwin, *args): self._dockable_livezoom.setVisible(False) self._dockable_liveprofile.setVisible(False) self._dockable_liveprofile_selected.setVisible(False) - # other internal init self.projection = None self.model = None @@ -702,7 +699,9 @@ def _addBackAllSelectedProfileMarkers(self, index=0): def addOverlayMarkerToCurrentProfile(self, name, position, qtpen, index=None): """ Add (or update) named overlay marker for current selected profile """ index = self._selected_profile_index if index is None else index - if index is None: return + if index is None: + return + def __initoverlaymarker(position=position, qtpen=qtpen): marker = TiggerPlotMarker() marker.setRenderHint(QwtPlotItem.RenderAntialiased) @@ -725,9 +724,10 @@ def __initoverlaymarker(position=position, qtpen=qtpen): def removeOverlayMarkerFromCurrentProfile(self, name, index=None): """ Remove named overlay marker from current profile """ index = self._selected_profile_index if index is None else index - if index is None: return + if index is None: + return if name in self._selected_profile_markup.get( - index, SkyModelPlotter._giveDefaultSelectedMarkerInfos())["overlays"]: + index, SkyModelPlotter._giveDefaultSelectedMarkerInfos())["overlays"]: self._selected_profile_markup[index]["overlays"][name]["marker"].detach() del self._selected_profile_markup[index]["overlays"][name] self._replot() @@ -735,26 +735,29 @@ def removeOverlayMarkerFromCurrentProfile(self, name, index=None): def deactivateOverlayMarkerFromCurrentProfile(self, name, index=None): """ Detach named overlay marker from current profile """ index = self._selected_profile_index if index is None else index - if index is None: return + if index is None: + return if name in self._selected_profile_markup.get( - index, SkyModelPlotter._giveDefaultSelectedMarkerInfos())["overlays"]: + index, SkyModelPlotter._giveDefaultSelectedMarkerInfos())["overlays"]: self._selected_profile_markup[index]["overlays"][name]["marker"].detach() self._replot() def removeAllOverlayMarkersFromCurrentProfile(self, index=None): """ Remove all overlay markers from current profile """ index = self._selected_profile_index if index is None else index - if index is None: return + if index is None: + return for name in list(self._selected_profile_markup.get( - index, SkyModelPlotter._giveDefaultSelectedMarkerInfos())["overlays"].keys()): + index, SkyModelPlotter._giveDefaultSelectedMarkerInfos())["overlays"].keys()): self.removeOverlayMarkerFromCurrentProfile(name, index) def deactivateAllOverlayMarkersFromCurrentProfile(self, index=None): """ Detach all overlay markers from current profile """ index = self._selected_profile_index if index is None else index - if index is None: return + if index is None: + return for name in self._selected_profile_markup.get( - index, SkyModelPlotter._giveDefaultSelectedMarkerInfos())["overlays"]: + index, SkyModelPlotter._giveDefaultSelectedMarkerInfos())["overlays"]: self.deactivateOverlayMarkerFromCurrentProfile(name, index) def removeAllSelectedProfileMarkings(self): @@ -791,7 +794,6 @@ def activeSelectedProfileMarkerColor(self, color): self._markup_profile_active_pen = QPen(self._markup_profile_active_color, 1) self.setSelectedProfileIndex(self._selected_profile_index) - @inactiveSelectedProfileMarkerColor.setter def inactiveSelectedProfileMarkerColor(self, color): self._markup_profile_inactive_color = color @@ -1475,9 +1477,10 @@ def __initMarker(markerindex): marker.setLabel(QwtText(str(markerindex + 1))) marker.setSymbol(self._create_profile_marker_symbol(active=True)) return marker + if self._selected_profile_markup.setdefault( - self._selected_profile_index, - SkyModelPlotter._giveDefaultSelectedMarkerInfos())["active"]["marker"] is None: + self._selected_profile_index, + SkyModelPlotter._giveDefaultSelectedMarkerInfos())["active"]["marker"] is None: self._selected_profile_markup[self._selected_profile_index]["active"]["marker"] = \ __initMarker(self._selected_profile_index) sel_marker = self._selected_profile_markup[self._selected_profile_index]["active"] From d655b4e6c1ff0494f090d7049e23c67d52bc3676 Mon Sep 17 00:00:00 2001 From: Raz Date: Mon, 15 Aug 2022 16:31:13 +0100 Subject: [PATCH 28/28] Fixed loading of profile file coordinates and plot marker --- TigGUI/Plot/PlottableProfiles.py | 15 +++++++++------ TigGUI/Plot/ToolDialogs.py | 7 +++++-- TigGUI/kitties/profiles.py | 23 +++++++++++++++++++---- 3 files changed, 33 insertions(+), 12 deletions(-) diff --git a/TigGUI/Plot/PlottableProfiles.py b/TigGUI/Plot/PlottableProfiles.py index 70dd76c..93572b4 100644 --- a/TigGUI/Plot/PlottableProfiles.py +++ b/TigGUI/Plot/PlottableProfiles.py @@ -43,7 +43,7 @@ def __init__(self, profilename, axisname, axisunit, xdata, ydata, profilecoord: coordinate (world) coord tuple to from which this profile is drawn, optional use None to leave unset """ - MutableTiggerProfile.__init__(self, profilename, axisname, axisunit, xdata, ydata) + MutableTiggerProfile.__init__(self, profilename, axisname, axisunit, xdata, ydata, profilecoord) self._curve_color = QColor("white") self._curve_pen = self.createPen() self._curve_pen.setStyle(Qt.SolidLine) @@ -55,7 +55,7 @@ def __init__(self, profilename, axisname, axisunit, xdata, ydata, self._profcurve.setStyle(QwtPlotCurve.Lines) self._profcurve.setOrientation(Qt.Horizontal) self._parentPlot = qwtplot - self._profilecoord = None + self._profilecoord = profilecoord self.profileAssociatedCoord = profilecoord self._profcurve.setData(xdata, ydata) @@ -77,11 +77,14 @@ def profileAssociatedCoord(self): @profileAssociatedCoord.setter def profileAssociatedCoord(self, profilecoord): + # JSON does not handle tuples, + # instead they are converted to lists if profilecoord is not None: - if not (isinstance(profilecoord, tuple) and - len(profilecoord) == 2 and - all(map(lambda x: isinstance(x, float), profilecoord))): - raise TypeError("profilecoord should be 2-element world coord tuple") + if not (isinstance(profilecoord, tuple) or isinstance( + profilecoord, list)) and len(profilecoord) == 2 and all( + map(lambda x: isinstance(x, float), profilecoord)): + raise TypeError( + "profilecoord should be 2-element world coord tuple or list") self._profilecoord = profilecoord def setCurveColor(self, color): diff --git a/TigGUI/Plot/ToolDialogs.py b/TigGUI/Plot/ToolDialogs.py index 61aace8..3a7f925 100644 --- a/TigGUI/Plot/ToolDialogs.py +++ b/TigGUI/Plot/ToolDialogs.py @@ -834,7 +834,8 @@ def saveProfile(self, filename=None): axisname, axisunit, self._last_data_x, - self._last_data_y) + self._last_data_y, + self._parent_picker._selected_profile_markup[self._currentprofile]['active']['position']) try: prof.saveProfile(filename) except IOError: @@ -879,6 +880,8 @@ def loadProfile(self, filename=None): self.addStaticProfile(prof) def addStaticProfile(self, prof, curvecol=None, coord=None): + if coord is None and prof.profilecoord is not None: + coord = prof.profilecoord pastedname, ok = QInputDialog.getText(self, "Set pasted profile name", "

Set name of pasted profile

", @@ -954,7 +957,7 @@ def __constructProfileIndex(i): and axes is not None): profname = self.profiles_info.get(i, {}).get("_current_profile_name", "Unnamed") axisname, axisindx, axisvals, axisunit = axes[selaxis[0]] - prof = TiggerProfile(profname, axisname, axisunit, last_data_x, last_data_y) + prof = TiggerProfile(profname, axisname, axisunit, last_data_x, last_data_y, (last_l, last_m)) return (prof, i, (last_l, last_m)) return None diff --git a/TigGUI/kitties/profiles.py b/TigGUI/kitties/profiles.py index 1198e28..16d03f4 100644 --- a/TigGUI/kitties/profiles.py +++ b/TigGUI/kitties/profiles.py @@ -33,7 +33,7 @@ class TiggerProfile: __VER_MAJ__ = 1 __VER_MIN__ = 0 - def __init__(self, profilename, axisname, axisunit, xdata, ydata): + def __init__(self, profilename, axisname, axisunit, xdata, ydata, profilecoord): """ Immutable Tigger profile profilename: A name for this profile @@ -52,6 +52,7 @@ def __init__(self, profilename, axisname, axisunit, xdata, ydata): self.__verifyArrs(xdata, ydata) self._xdata = xdata.copy() self._ydata = ydata.copy() + self._profilecoord = profilecoord def __verifyArrs(self, xdata, ydata): if not isinstance(xdata, np.ndarray): @@ -89,10 +90,15 @@ def axisUnit(self): def version(self): return f"{self._version_maj}.{self._version_min}" + @property + def profilecoord(self): + return self._profilecoord + def saveProfile(self, filename): prof = { "version": self.version, "profile_name": self._profilename, + "position": self._profilecoord, "axis": self._axisname, "units": self._axisunit, "x_data": self._xdata.tolist(), @@ -113,8 +119,8 @@ class MutableTiggerProfile(TiggerProfile): xdata: profile x axis data (1D ndarray of shape of ydata) ydata: profile y axis data (1D ndarray) """ - def __init__(self, profilename, axisname, axisunit, xdata, ydata): - TiggerProfile.__init__(self, profilename, axisname, axisunit, xdata, ydata) + def __init__(self, profilename, axisname, axisunit, xdata, ydata, profilecoord): + TiggerProfile.__init__(self, profilename, axisname, axisunit, xdata, ydata, profilecoord) def setAxesData(self, xdata, ydata): self.__verifyArrs(xdata, ydata) @@ -133,6 +139,10 @@ def axisName(self): def axisUnit(self): return self._axisunit + @property + def profileCoord(self): + return self._profilecoord + @profileName.setter def profileName(self, name): self._profilename = name @@ -145,6 +155,10 @@ def axisName(self, name): def axisUnit(self, unit): self._axisunit = unit + @profileCoord.setter + def profileCoord(self, coords): + self._profilecoord = coords + class TiggerProfileFactory: def __init__(self, filename): @@ -183,6 +197,7 @@ def load(cls, filename): profname = prof["profile_name"] axisname = prof["axis"] axisunits = prof["units"] + profcoord = prof["position"] if (not isinstance(prof["x_data"], list) and not all( map(lambda x: isinstance(x, float), prof["x_data"]))): @@ -204,6 +219,6 @@ def load(cls, filename): raise IOError("Stored X data not the same shape as Y data") # Success - tigprof = TiggerProfile(profname, axisname, axisunits, xdata, ydata) + tigprof = TiggerProfile(profname, axisname, axisunits, xdata, ydata, profcoord) dprint(0, f"Loaded profile from {filename}") return tigprof