From a59d2cabbc74126a50a102a40fd8c78a45b05da7 Mon Sep 17 00:00:00 2001 From: Robert Cudmore Date: Wed, 26 Jun 2024 11:36:43 -0700 Subject: [PATCH 01/26] activating workflows --- pymapmanager/analysisParams.py | 2 +- .../annotations/baseAnnotationsCore.py | 42 ++-------- .../annotations/baseAnnotationsMutate.py | 59 ------------- pymapmanager/interface2/__init__.py | 1 - pymapmanager/interface2/appDisplayOptions.py | 2 + .../interface2/mapWidgets/_mapIngest.py | 2 +- .../interface2/mapWidgets/dendrogramWidget.py | 5 +- .../interface2/mapWidgets/mapWidget.py | 18 ++-- pymapmanager/interface2/openFirstWindow.py | 13 ++- pymapmanager/interface2/pyMapManagerApp2.py | 13 +-- .../stackWidgets/annotationListWidget2.py | 19 +++-- .../stackWidgets/annotationPlotWidget2.py | 82 +++++-------------- .../stackWidgets/histogramWidget2.py | 11 +-- .../stackWidgets/imagePlotWidget2.py | 11 +-- .../interface2/stackWidgets/mmWidget2.py | 9 +- .../stackWidgets/pmmScatterPlotWidget.py | 6 +- .../interface2/stackWidgets/searchWidget.py | 9 +- .../stackWidgets/spineInfoWidget.py | 3 +- .../interface2/stackWidgets/stackToolbar.py | 20 +++-- .../interface2/stackWidgets/stackWidget2.py | 21 +++-- .../interface2/stackWidgets/tracingWidget.py | 55 ------------- pymapmanager/mmMap.py | 20 ++--- pymapmanager/stack.py | 10 +-- tests/interface/test_stack_widgets.py | 6 +- 24 files changed, 135 insertions(+), 304 deletions(-) delete mode 100644 pymapmanager/annotations/baseAnnotationsMutate.py diff --git a/pymapmanager/analysisParams.py b/pymapmanager/analysisParams.py index 905b22e4..19888c6c 100644 --- a/pymapmanager/analysisParams.py +++ b/pymapmanager/analysisParams.py @@ -415,7 +415,7 @@ def _getDocs(self) -> str: # Save button will send the signals to backend for whatever changed if __name__ == '__main__': - dp = AnalysisParams() + dp = _old_AnalysisParams() print(dp._getDocs()) # dp.buildAnalysisParamUI() diff --git a/pymapmanager/annotations/baseAnnotationsCore.py b/pymapmanager/annotations/baseAnnotationsCore.py index 9f19da16..fb41b332 100644 --- a/pymapmanager/annotations/baseAnnotationsCore.py +++ b/pymapmanager/annotations/baseAnnotationsCore.py @@ -7,12 +7,13 @@ from mapmanagercore import MapAnnotations +from pymapmanager.interface2.stackWidgets.event.spineEvent import EditSpinePropertyEvent + from pymapmanager._logger import logger class AnnotationsCore: def __init__(self, - mapAnnotations : "???", # TODO: update on merge 20240513 - # analysisParams : "AnalysisParams", + mapAnnotations : MapAnnotations, sessionID = 0, ): """ @@ -24,7 +25,7 @@ def __init__(self, self._sessionID = sessionID # full map, multiple session ids (timepoint, t) - self._fullMap : "AnnotationsLayers" = mapAnnotations + self._fullMap : MapAnnotations = mapAnnotations # mapmanagercore.annotations.single_time_point.layers.AnnotationsLayers #filtered down to just sessionID @@ -341,7 +342,7 @@ def deleteAnnotation(self, rowIdx : Union[int, List[int]]) -> None: self._buildDataFrame() - def editSpine(self, editSpineProperty : "EditSpineProperty"): + def editSpine(self, editSpineProperty : EditSpinePropertyEvent): # spineID:117 col:isBad value:True # logger.info(editSpineProperty) logger.info(f"stack widget editSpineProperty {editSpineProperty}") @@ -476,36 +477,3 @@ def getMedianZ(self, segmentID : int): yMedian = np.median(df['y']) zMedian = np.median(df['z']) return (int(xMedian), int(yMedian), int(zMedian) ) - -if __name__ == '__main__': - from pymapmanager._logger import setLogLevel - setLogLevel() - - # _testEditSpineProperty() - - sys.exit(1) - - zarrPath = '../MapManagerCore/data/rr30a_s0us.mmap' - map = MapAnnotations(MMapLoader(zarrPath).cached()) - - sac = SpineAnnotationsCore(map) - - print(sac.getDataFrame().columns) - - segmentID = None - roiTypes = None - zSlice = 20 - zPlusMinus = 5 - - value = sac.getValue('x', 2) - print(f'x:{value}') - value = sac.getValues('y', [2, 3, 4]) - print(f'y:{value}') - print(type(value)) - - row = sac.getRow(2) - print('row') - print(row) - - # spineDf = sac.getSegmentPlot(segmentID, roiTypes, zSlice, zPlusMinus) - # print(spineDf) \ No newline at end of file diff --git a/pymapmanager/annotations/baseAnnotationsMutate.py b/pymapmanager/annotations/baseAnnotationsMutate.py deleted file mode 100644 index 8ce93a84..00000000 --- a/pymapmanager/annotations/baseAnnotationsMutate.py +++ /dev/null @@ -1,59 +0,0 @@ -"""Collection of function to mutate (edit) mapmanagercore. -""" -from typing import List, Union, Optional - -from mapmanagercore import MapAnnotations - -# import pymapmanager - -from pymapmanager._logger import logger - -def editSpineEvent(mmWidget : "mmWidget", - spineID : List[int], - col : str, - value : object): - """Update spine properies (row, col, value) in core. - """ - coreMap = mmWidget.getStackWidget().getStack().getCoreMap() - - for oneEdit in editSpineProperty: - spineID = oneEdit['spineID'] - col = oneEdit['col'] - value = oneEdit['value'] - - value = { - col: value - } - - # map.updateSpine(spineId=id, value={"f": 1}) - coreMap.updateSpine(spineId=spineID, value=value) - - logger.info(f'=== updateSpine spine {spineID} in backend') - -def addSpine(theMap : MapAnnotations, - x : int, y : int, z : int, - segmentID : int - ): - """Add a new spine to backend core. - - Parameters - ---------- - map : - x,y,z : int - segmentID : int - """ - spineID = theMap.addSpine(segmentId=segmentID, x=x, y=y, z=z) - logger.info(f'=== added spine {spineID} to backend') - return id - -def deleteSpine(theMap : MapAnnotations, - spineID - ): - """Delete spine from backend core. - """ - theMap.deleteSpine(spineID) - logger.info(f'=== deleted spine {spineID} to backend') - return True - - - diff --git a/pymapmanager/interface2/__init__.py b/pymapmanager/interface2/__init__.py index 257d26b9..af64e98f 100644 --- a/pymapmanager/interface2/__init__.py +++ b/pymapmanager/interface2/__init__.py @@ -1,7 +1,6 @@ from .pyMapManagerApp2 import PyMapManagerApp from .mainMenus import PyMapManagerMenus - from .mainWindow import MainWindow from .preferences import Preferences diff --git a/pymapmanager/interface2/appDisplayOptions.py b/pymapmanager/interface2/appDisplayOptions.py index 9469040f..2cb69b22 100644 --- a/pymapmanager/interface2/appDisplayOptions.py +++ b/pymapmanager/interface2/appDisplayOptions.py @@ -1,3 +1,5 @@ +from pymapmanager._logger import logger + class AppDisplayOptions(): """Class to encapsulate all display options. diff --git a/pymapmanager/interface2/mapWidgets/_mapIngest.py b/pymapmanager/interface2/mapWidgets/_mapIngest.py index 8c2a7efc..f2e81174 100644 --- a/pymapmanager/interface2/mapWidgets/_mapIngest.py +++ b/pymapmanager/interface2/mapWidgets/_mapIngest.py @@ -88,7 +88,7 @@ def point_addDist(pa : pd.DataFrame, la : pd.DataFrame): pa.loc[rowIdx, 'pDist'] = distance -def addDistance(map : pmm.mmMap): +def addDistance(map : "pmm.mmMap"): """Add distance to both line and point annotations. """ for sessionIdx, stack in enumerate(map.stacks): diff --git a/pymapmanager/interface2/mapWidgets/dendrogramWidget.py b/pymapmanager/interface2/mapWidgets/dendrogramWidget.py index a1ffe3f6..e63b76a3 100644 --- a/pymapmanager/interface2/mapWidgets/dendrogramWidget.py +++ b/pymapmanager/interface2/mapWidgets/dendrogramWidget.py @@ -10,13 +10,12 @@ from qtpy import QtCore, QtWidgets -# import pymapmanager as pmm from pymapmanager.interface2.stackWidgets import mmWidget2 from pymapmanager.interface2.stackWidgets.mmWidget2 import pmmEventType, pmmEvent from .mmMapPlot import getPlotDict from .mmMapPlot import mmMapPlot -from .mapWidget import mapWidget, MapSelection +from .mapWidget import mapWidget from pymapmanager._logger import logger @@ -194,7 +193,7 @@ def _on_pick(self, selDict): # main mapmanager signal/slot - def selectedEvent(self, event : "pymapmanager.interface2.mmWidget2.pmmEvent"): + def selectedEvent(self, event : pmmEvent): """Respond to a user selection. """ # logger.info(f'event:{event}') diff --git a/pymapmanager/interface2/mapWidgets/mapWidget.py b/pymapmanager/interface2/mapWidgets/mapWidget.py index 236b59e7..d5ecad71 100644 --- a/pymapmanager/interface2/mapWidgets/mapWidget.py +++ b/pymapmanager/interface2/mapWidgets/mapWidget.py @@ -99,8 +99,12 @@ def _findStackWidget(self, path): return stackWidget return None - def _findStackWidget2(self, thisStack : pmm.stack): + def _findStackWidget2(self, thisStack): """Find an open stack widget. + + Parameters + ---------- + thisStack : pymapmanager.stack """ for stackWidget in self._stackWidgetList: stack = stackWidget.getStack() @@ -184,7 +188,7 @@ def openStack2(self, session : int) -> "pmm.interface2.stackWidget": def openStack(self, path = None, - stack : pmm.stack = None, + stack = None, session = None, posRect : List[int] = None, ) -> "pmm.interface2.stackWidget": @@ -193,7 +197,7 @@ def openStack(self, Parameters ========== path : str - stack : + stack : pymapmanager.stack session : int postRect : List[int] Position for the window [l, t, w, h] @@ -384,20 +388,20 @@ def closeEvent(self, event): self.getApp().closeMapWindow(self) - def closeStackWindow(self, theWindow : "stackWidget2"): - """Remove theWindow from self._stackWidgetDict. + def closeStackWindow(self, stackWidget): + """Remove stackWidget from self._stackWidgetDict. """ logger.info(' remove stackwidget window from map list of stack') _oldWindow = None for _idx, _window in enumerate(self._stackWidgetList): - if _window == theWindow: + if _window == stackWidget: logger.info('removing from list') _oldWindow = self._stackWidgetList.pop(_idx) if _oldWindow is None: - logger.error(f'did not find stack widget in map widget {theWindow}') + logger.error(f'did not find stack widget in map widget {stackWidget}') logger.error('available windows are') logger.error(self._stackWidgetList) diff --git a/pymapmanager/interface2/openFirstWindow.py b/pymapmanager/interface2/openFirstWindow.py index f94b5b6d..dfcd03be 100644 --- a/pymapmanager/interface2/openFirstWindow.py +++ b/pymapmanager/interface2/openFirstWindow.py @@ -1,10 +1,17 @@ +# circular import for typechecking +# from pymapmanager.interface2 import PyMapManagerApp +# see: https://stackoverflow.com/questions/39740632/python-type-hinting-without-cyclic-imports +from __future__ import annotations +from typing import TYPE_CHECKING +if TYPE_CHECKING: + from .pyMapManagerApp2 import PyMapManagerApp + import os from functools import partial from typing import List from qtpy import QtCore, QtWidgets, QtGui -# from .pyMapManagerApp2 import PyMapManagerApp import pymapmanager from pymapmanager.interface2.mainWindow import MainWindow @@ -15,7 +22,7 @@ class OpenFirstWindow(MainWindow): Open this at app start and close once a file/folder is loaded """ - def __init__(self, pyMapManagerApp : "PyMapManagerApp", parent=None): + def __init__(self, pyMapManagerApp : PyMapManagerApp, parent=None): super().__init__(parent) # self._app = pyMapManagerApp @@ -184,7 +191,7 @@ def test(): app = SanPyApp([]) - of = openFirstWidget(app) + of = OpenFirstWindow(app) of.show() sys.exit(app.exec_()) diff --git a/pymapmanager/interface2/pyMapManagerApp2.py b/pymapmanager/interface2/pyMapManagerApp2.py index 6446d40b..bbb78763 100644 --- a/pymapmanager/interface2/pyMapManagerApp2.py +++ b/pymapmanager/interface2/pyMapManagerApp2.py @@ -14,9 +14,10 @@ # Enable HiDPI. qdarktheme.enable_hi_dpi() -import pymapmanager as pmm import mapmanagercore +import pymapmanager as pmm + import pymapmanager.interface2 import pymapmanager.interface2.stackWidgets @@ -205,7 +206,7 @@ def getFrontWindowType(self): return _windowType - def closeStackWindow(self, theWindow : "stackWidget2"): + def closeStackWindow(self, stackWidget): """Remove theWindow from self._stackWidgetDict. """ @@ -215,7 +216,7 @@ def closeStackWindow(self, theWindow : "stackWidget2"): # theWindow.closeStackWindow() # return - zarrPath = theWindow.getStack().getPath() + zarrPath = stackWidget.getStack().getPath() popThisKey = None for pathKey in self._stackWidgetDict.keys(): if pathKey == zarrPath: @@ -227,7 +228,7 @@ def closeStackWindow(self, theWindow : "stackWidget2"): logger.info(f'popped {_theWindow}') # _theWindow.close() else: - logger.error(f'did not find stack widget in app {theWindow}') + logger.error(f'did not find stack widget in app {stackWidget}') logger.error('available keys are') logger.error(self._stackWidgetDict.keys()) @@ -303,11 +304,11 @@ def toggleMapWidget(self, path : str, visible : bool): return self._mapWidgetDict[path].setVisible(visible) - def closeMapWindow(self, theWindow : "mapWidget"): + def closeMapWindow(self, mapWidget): """Remove theWindow from self._windowList. """ logger.info(' remove _mapWidgetDict window from app list of windows') - mapPath = theWindow.getMap().filePath + mapPath = mapWidget.getMap().filePath popThisKey = None for pathKey in self._mapWidgetDict.keys(): if pathKey == mapPath: diff --git a/pymapmanager/interface2/stackWidgets/annotationListWidget2.py b/pymapmanager/interface2/stackWidgets/annotationListWidget2.py index 8c9cf422..ae1fa5b8 100644 --- a/pymapmanager/interface2/stackWidgets/annotationListWidget2.py +++ b/pymapmanager/interface2/stackWidgets/annotationListWidget2.py @@ -1,13 +1,16 @@ -"""Widgets to display lists of point and line annotations. -""" +# circular import for typechecking +# from pymapmanager.interface2 import PyMapManagerApp +# see: https://stackoverflow.com/questions/39740632/python-type-hinting-without-cyclic-imports +from __future__ import annotations +from typing import TYPE_CHECKING +if TYPE_CHECKING: + from pymapmanager.interface2.stackWidgets import stackWidget2 import sys from typing import List from qtpy import QtGui, QtCore, QtWidgets -from pymapmanager._logger import logger - import pymapmanager.annotations from pymapmanager.interface2.core.search_widget import myQTableView @@ -16,12 +19,12 @@ from pymapmanager.interface2.stackWidgets.mmWidget2 import mmWidget2, pmmEventType, pmmEvent, pmmStates from pymapmanager.interface2.stackWidgets.event.spineEvent import DeleteSpineEvent -# from .mmWidget2 import pmmEventType, pmmEvent, pmmStates +from pymapmanager._logger import logger class annotationListWidget(mmWidget2): def __init__(self, - stackWidget : "StackWidget", + stackWidget : stackWidget2, annotations : "pymapmanager.annotations.baseAnnotations", name : str = None): """ @@ -189,7 +192,7 @@ class pointListWidget(annotationListWidget): _widgetName = 'Point List' - def __init__(self, stackWidget : "pymapmanager.interface2.stackWidget.StackWidget2"): + def __init__(self, stackWidget : stackWidget2): annotations = stackWidget.getStack().getPointAnnotations() logger.info(annotations) @@ -263,7 +266,7 @@ class lineListWidget(annotationListWidget): _widgetName = 'Line List' - def __init__(self, stackWidget : "StackWidget"): + def __init__(self, stackWidget : stackWidget2): annotations = stackWidget.getStack().getLineAnnotations() super().__init__(stackWidget, annotations, name='lineListWidget') diff --git a/pymapmanager/interface2/stackWidgets/annotationPlotWidget2.py b/pymapmanager/interface2/stackWidgets/annotationPlotWidget2.py index 79bb046c..b470a657 100644 --- a/pymapmanager/interface2/stackWidgets/annotationPlotWidget2.py +++ b/pymapmanager/interface2/stackWidgets/annotationPlotWidget2.py @@ -1,3 +1,12 @@ +# circular import for typechecking +# from pymapmanager.interface2 import PyMapManagerApp +# see: https://stackoverflow.com/questions/39740632/python-type-hinting-without-cyclic-imports +from __future__ import annotations +from typing import TYPE_CHECKING +if TYPE_CHECKING: + from pymapmanager.interface2.stackWidgets import stackWidget2 + from pymapmanager.annotations.baseAnnotationsCore import AnnotationsCore, SpineAnnotationsCore, LineAnnotationsCore + import time from typing import List, Optional @@ -7,7 +16,6 @@ from qtpy import QtGui, QtCore, QtWidgets import pyqtgraph as pg -from pymapmanager.annotations.baseAnnotationsCore import SpineAnnotationsCore from .mmWidget2 import mmWidget2, pmmEventType, pmmEvent, pmmStates from pymapmanager.interface2.stackWidgets.event.spineEvent import AddSpineEvent, DeleteSpineEvent, MoveSpineEvent @@ -115,8 +123,8 @@ class annotationPlotWidget(mmWidget2): def __init__( self, - stackWidget: "StackWidget", - annotations: "pymapmanager.annotations.baseAnnotations", + stackWidget: stackWidget2, + annotations: AnnotationsCore, pgView: pg.PlotWidget, displayOptions: dict, ): @@ -299,7 +307,8 @@ def _on_mouse_hover(self, points, event): self._selectAnnotation(dbIdx=dbIdx) - # abb 202405 turned off for now, core will accept a click (in image lpot) and then find the closest point + # abb 202405 turned off for now, + # core will accept a click (in image plot) and then find the closest point # abj def _on_highlighted_mouse_click(self, points, event): """Respond to user click on highlighted scatter plot. @@ -329,7 +338,7 @@ def _on_highlighted_mouse_click(self, points, event): dbIdx = self._highlightedPlotIndex[referenceHighlightedPlotIdx] # logger.info(f"highlightedPlotIdx {dbIdx}") - if isinstance(self._annotations, pymapmanager.annotations.lineAnnotations): + if isinstance(self._annotations, LineAnnotationsCore): eventType = pmmEventType.selection event = pmmEvent(eventType, self) @@ -380,9 +389,11 @@ def _on_mouse_click(self, points, event): eventType = pmmEventType.selection event = pmmEvent(eventType, self) + # abb we need this import here, otherwise we get circular imports + from pymapmanager.annotations.baseAnnotationsCore import SpineAnnotationsCore + if isinstance( self._annotations, - # pymapmanager.annotations.baseAnnotationsCore.SpineAnnotationsCore, SpineAnnotationsCore, ): event.getStackSelection().setPointSelection(dbIdx) @@ -458,29 +469,6 @@ def _selectAnnotation(self, dbIdx: List[int], isAlt: bool = False): # set data calls this? # self._view.update() - def slot_setDisplayType(self, - roiTypeList: List["pymapmanager.annotations.pointTypes"] - ): - """Set the roiTypes to display in the plot. - - Args: - roiTypeList: A list of roiType to display. - - Notes: - This resets our state (_dfPlot) and requires a full refresh from the backend. - """ - if not isinstance(roiTypeList, list): - roiTypeList = [roiTypeList] - - logger.info(f"roiTypeList:{roiTypeList}") - - self._roiTypes = [] - for roiType in roiTypeList: - self._roiTypes.append(roiType.value) - - self._dfPlot = None - self._refreshSlice() - def _refreshSlice(self): # I don't think that the current slice is being updated, it will always pass in 0? # logger.info(f'_currentSlice: {self._currentSlice}') @@ -592,7 +580,7 @@ class pointPlotWidget(annotationPlotWidget): def __init__( self, - stackWidget: "StackWidget", + stackWidget: stackWidget2, #pointAnnotations: "pymapmanager.annotations.pointAnnotations", pgView, # pymapmanager.interface.myPyQtGraphPlotWidget #displayOptions: dict, @@ -1019,38 +1007,6 @@ def _updateItem(self, rowIdx: int): self._pointLabels.updateLabel(rowIdx) return - oneLabel = self._labels[rowIdx] - oneLabel.setPos(QtCore.QPointF(x - 9, y - 9)) - oneLabel.setText(str(rowIdx)) - - # update a spine line - _brightestIndex = self.pointAnnotations.getValue(["brightestIndex"], rowIdx) - xLeft = self.lineAnnotations.getValue(["xLeft"], _brightestIndex) - xRight = self.lineAnnotations.getValue(["xRight"], _brightestIndex) - yLeft = self.lineAnnotations.getValue(["yLeft"], _brightestIndex) - yRight = self.lineAnnotations.getValue(["yRight"], _brightestIndex) - - leftRadiusPoint = (xLeft, yLeft) - rightRadiusPoint = (xRight, yRight) - spinePoint = (x, y) - closestPoint = pymapmanager.utils.getCloserPoint2( - spinePoint, leftRadiusPoint, rightRadiusPoint - ) - - realRow = rowIdx * 2 - - self._xSpineLines[realRow] = x # = np.append(self._xSpineLines, x) - self._xSpineLines[realRow + 1] = closestPoint[ - 0 - ] # = np.append(self._xSpineLines, closestPoint[0]) - - self._ySpineLines[realRow] = y - self._ySpineLines[realRow + 1] = closestPoint[1] - - # no need to update connection 0/1 (for pyqtgraph) - # self._spineLinesConnect = np.append(self._spineLinesConnect, 1) # connect - # self._spineLinesConnect = np.append(self._spineLinesConnect, 0) # don't connect - def _old__newLabel(self, rowIdx, x, y): """Make a new label at (x,y) with text rowIdx. @@ -1181,7 +1137,7 @@ class linePlotWidget(annotationPlotWidget): def __init__( self, - stackWidget: "StackWidget", + stackWidget: stackWidget2, # lineAnnotations: "pymapmanager.annotations.lineAnnotations", pgView, # pymapmanager.interface.myPyQtGraphPlotWidget # displayOptions: dict, diff --git a/pymapmanager/interface2/stackWidgets/histogramWidget2.py b/pymapmanager/interface2/stackWidgets/histogramWidget2.py index 7b64f6e1..31f9198e 100644 --- a/pymapmanager/interface2/stackWidgets/histogramWidget2.py +++ b/pymapmanager/interface2/stackWidgets/histogramWidget2.py @@ -18,14 +18,7 @@ class HistogramWidget(mmWidget2): _widgetName = 'Histogram' # Name of the widget (must be unique) - def __init__(self, stackWidget : "StackWidget"): - # myStack, - # contrastDict : dict, - # sliceNumber:int=0, - # channel:int=1, - # name = '', - # annotations = None, - # pmmParentWidget = None): + def __init__(self, stackWidget): """Histogram widget to show image intensities. """ super().__init__(stackWidget) @@ -302,7 +295,7 @@ def _setSlice(self, sliceNumber, doInit=False): self._sliceNumber = sliceNumber - channel = self._channel + channel = self._channel - 1 # core is 0 based self._sliceImage = self._myStack.getImageSlice(imageSlice=self._sliceNumber, channel=channel) diff --git a/pymapmanager/interface2/stackWidgets/imagePlotWidget2.py b/pymapmanager/interface2/stackWidgets/imagePlotWidget2.py index 7f85b865..4d0a7ae1 100644 --- a/pymapmanager/interface2/stackWidgets/imagePlotWidget2.py +++ b/pymapmanager/interface2/stackWidgets/imagePlotWidget2.py @@ -66,14 +66,9 @@ class ImagePlotWidget(mmWidget2): """To allow linking windows. """ - def __init__(self, stackWidget : "StackWidget"): - # myStack : pymapmanager.stack, - # contrastDict : dict, - # colorLutDict : dict, - # displayOptionsDict : dict, - # name, - # stackWidgetParent : "pymapmanager.interface2.stackWidget2", - # parent=None): + def __init__(self, stackWidget): + """Widget to display an image, points, and lines. + """ super().__init__(stackWidget) self._myStack = stackWidget.getStack() diff --git a/pymapmanager/interface2/stackWidgets/mmWidget2.py b/pymapmanager/interface2/stackWidgets/mmWidget2.py index 744a0f10..f696ec09 100644 --- a/pymapmanager/interface2/stackWidgets/mmWidget2.py +++ b/pymapmanager/interface2/stackWidgets/mmWidget2.py @@ -1,3 +1,10 @@ +# circular import for typechecking +# from pymapmanager.interface2 import PyMapManagerApp +# see: https://stackoverflow.com/questions/39740632/python-type-hinting-without-cyclic-imports +from __future__ import annotations +from typing import TYPE_CHECKING +if TYPE_CHECKING: + from pymapmanager.interface2.stackWidgets import stackWidget2 import copy from enum import Enum, auto @@ -625,7 +632,7 @@ def _addDockWidget(self, widget : "mmWidget2", position : str, name : str = '') self.addDockWidget(position, dockWIdget) return dockWIdget - def getStackWidget(self) -> "StackWidget2": + def getStackWidget(self) -> stackWidget2: return self._stackWidget def getStack(self): diff --git a/pymapmanager/interface2/stackWidgets/pmmScatterPlotWidget.py b/pymapmanager/interface2/stackWidgets/pmmScatterPlotWidget.py index b2bea18e..eca18355 100644 --- a/pymapmanager/interface2/stackWidgets/pmmScatterPlotWidget.py +++ b/pymapmanager/interface2/stackWidgets/pmmScatterPlotWidget.py @@ -22,10 +22,8 @@ class PmmScatterPlotWidget(mmWidget2): _widgetName = 'Scatter Plot' - def __init__(self, stackWidget : "StackWidget"): - """ - Args: - parent: + def __init__(self, stackWidget): + """Widget to display a scatter plot of point annotations. """ super().__init__(stackWidget) self.stackWidget = stackWidget diff --git a/pymapmanager/interface2/stackWidgets/searchWidget.py b/pymapmanager/interface2/stackWidgets/searchWidget.py index 3dbf5545..ff81c46b 100644 --- a/pymapmanager/interface2/stackWidgets/searchWidget.py +++ b/pymapmanager/interface2/stackWidgets/searchWidget.py @@ -1,21 +1,20 @@ from qtpy import QtGui, QtCore, QtWidgets -from pymapmanager._logger import logger - import pymapmanager.annotations from .mmWidget2 import mmWidget2 -# from .mmWidget2 import pmmEventType, pmmEvent, pmmStates from pymapmanager.interface2.core.search_widget import myQTableView +from pymapmanager._logger import logger + class SearchWidget(mmWidget2): _widgetName = 'Search Widget' - def __init__(self, stackWidget : "StackWidget"): - """ + def __init__(self, stackWidget): + """Widget to display a search of point annotations. """ super().__init__(stackWidget) diff --git a/pymapmanager/interface2/stackWidgets/spineInfoWidget.py b/pymapmanager/interface2/stackWidgets/spineInfoWidget.py index bc43b8f5..7a4a0221 100644 --- a/pymapmanager/interface2/stackWidgets/spineInfoWidget.py +++ b/pymapmanager/interface2/stackWidgets/spineInfoWidget.py @@ -7,6 +7,7 @@ from .mmWidget2 import mmWidget2 from .event.spineEvent import EditSpinePropertyEvent +from .stackWidget2 import stackWidget2 """ To add a new display value, like spineLength (not editable) @@ -27,7 +28,7 @@ class SpineInfoWidget(mmWidget2): _widgetName = 'Spine Info Widget' - def __init__(self, stackWidget : "StackWidget"): + def __init__(self, stackWidget : stackWidget2): super().__init__(stackWidget) diff --git a/pymapmanager/interface2/stackWidgets/stackToolbar.py b/pymapmanager/interface2/stackWidgets/stackToolbar.py index 616b692d..55eca82b 100644 --- a/pymapmanager/interface2/stackWidgets/stackToolbar.py +++ b/pymapmanager/interface2/stackWidgets/stackToolbar.py @@ -1,6 +1,6 @@ from qtpy import QtGui, QtCore, QtWidgets -import pymapmanager.stack +# from pymapmanager.stack import stack from pymapmanager._logger import logger @@ -14,11 +14,16 @@ class StackToolBar(QtWidgets.QToolBar): signalRadiusChanged = QtCore.Signal(object) # dict : {checked, upDownSlices} def __init__(self, - myStack :pymapmanager.stack, - displayOptionsDict : dict, parent=None): + myStack, + displayOptionsDict : dict, + parent=None): + """ + Parameters: + myStack : pymapmanager.stack + """ super().__init__(parent) - self._myStack : pymapmanager.stack = myStack + self._myStack = myStack self._displayOptionsDict = displayOptionsDict # list of channel strings 1,2,3,... @@ -48,11 +53,12 @@ def __init__(self, # refresh interface self._setStack(self._myStack) - def _setStack(self, theStack : pymapmanager.stack): + def _setStack(self, theStack): """Set the state of the interface based on a stack. - Args: - theStack (pymapmanager.stack): The stack to dislpay in the widget + Parameters: + theStack :pymapmanager.stack + The stack to dislpay in the widget """ self._myStack = theStack diff --git a/pymapmanager/interface2/stackWidgets/stackWidget2.py b/pymapmanager/interface2/stackWidgets/stackWidget2.py index be9ad909..3cd01bbf 100644 --- a/pymapmanager/interface2/stackWidgets/stackWidget2.py +++ b/pymapmanager/interface2/stackWidgets/stackWidget2.py @@ -1,3 +1,11 @@ +# circular import for typechecking +# from pymapmanager.interface2 import PyMapManagerApp +# see: https://stackoverflow.com/questions/39740632/python-type-hinting-without-cyclic-imports +from __future__ import annotations +from typing import TYPE_CHECKING +if TYPE_CHECKING: + from pymapmanager.interface2 import PyMapManagerApp, AppDisplayOptions + from typing import Optional # List, Union, Tuple import numpy as np @@ -16,6 +24,7 @@ # from .tracingWidget import tracingWidget # from .histogramWidget2 import HistogramWidget # from .searchWidget2 import SearchWidget2 +from pymapmanager.interface2.stackWidgets.event.spineEvent import AddSpineEvent, DeleteSpineEvent, UndoSpineEvent from pymapmanager._logger import logger @@ -84,7 +93,7 @@ def __init__(self, self._buildUI() self._buildMenus() - def getDisplayOptions(self) -> "AppDisplayOptions": + def getDisplayOptions(self) -> AppDisplayOptions: return self._displayOptionsDict def closeEvent(self, event): @@ -116,7 +125,7 @@ def closeStackWindow(self): # """ # return self._timePoint - def getPyMapManagerApp(self) -> Optional["PyMapManagerApp"]: + def getPyMapManagerApp(self) -> Optional[PyMapManagerApp]: """Get the running PyMapManagerApp(QApplication). If not PyMapManagerApp, will return None. @@ -450,7 +459,7 @@ def selectedEvent(self, event : "pmmEvent"): return True - def addedEvent(self, event : "AddSpineEvent") -> bool: + def addedEvent(self, event : AddSpineEvent) -> bool: """Add to backend. Currently only allows adding a spine annotation. @@ -517,7 +526,7 @@ def editedEvent(self, event : pmmEvent) -> bool: logger.info(event) self.getStack().getPointAnnotations().editSpine(event) - def deletedEvent(self, event : "DeleteSpineEvent") -> bool: + def deletedEvent(self, event : DeleteSpineEvent) -> bool: """Delete items from backend. Returns @@ -900,7 +909,8 @@ def _setDefaultContrastDict(self): def zoomToPointAnnotation(self, idx : int, isAlt : bool = False, - select : bool = False): + select : bool = False + ): """Zoom to a point annotation. This should be called externally. For example, @@ -994,7 +1004,6 @@ def _undo_action(self): self.getStack().undo() - from pymapmanager.interface2.stackWidgets.event.spineEvent import UndoSpineEvent undoSpineEvent = UndoSpineEvent(self) self.emitEvent(undoSpineEvent) diff --git a/pymapmanager/interface2/stackWidgets/tracingWidget.py b/pymapmanager/interface2/stackWidgets/tracingWidget.py index 6daddb4a..7de85efb 100644 --- a/pymapmanager/interface2/stackWidgets/tracingWidget.py +++ b/pymapmanager/interface2/stackWidgets/tracingWidget.py @@ -131,61 +131,6 @@ def _buildUI(self) -> QtWidgets.QVBoxLayout: return vControlLayout - def _old__updateTracingButton(self, selectionEvent : "pymapmanager.annotations.SelectionEvent"): - """Turn tracing button on/off depending on state. - """ - # - # trace/cancel button should only be activated when there is - # 1) a point annotation controlPnt selection - # 2) it is not the first control pnt in a segmentID - # Need to run this code every time there is a new point selection - - # refactor aug 24, just emit and will get change in slot_selectAnnotation - # _doEditSegments = self._displayOptionsDict['doEditSegments'] - # logger.info(f'_doEditSegments: {_doEditSegments}') - - rows = selectionEvent.getRows() - isEditSegment = selectionEvent.isEditSegment - - logger.info(f' rows:{rows}') - if not isEditSegment or rows == []: - # no selection, always off - traceState = False - else: - rowIdx = rows[0] - - pa = selectionEvent.getStack().getPointAnnotations() - isControlPnt = pa.rowColIs(rowIdx, 'roiType', 'controlPnt') - logger.info(f' isControlPnt: {isControlPnt} {type(isControlPnt)}') - if not isControlPnt: - traceState = False - else: - logger.info(f' checking if control point is > first in segment') - segmentID = pa.getValue('segmentID', rowIdx) - # if isControl pnt and not the first in a segmentID - logger.info(f' segmentId:{segmentID}') - #la = self._stackWidget.getStack().getLineAnnotations() - # not the correct function, - # we need to determine if it is the first controlPnt in the point annotations - # startRow, _stopRow = la._segmentStartRow(segmentID) - # still not correct, we need just control pnt from one segmentID - # logger.error('fix this !!!') - # _idx = pa.getRoiType_col('index', pymapmanager.annotations.pointTypes.controlPnt) - _controlPnt = pymapmanager.annotations.pointTypes.controlPnt - - # get the first row that is a control pnt - _idx = pa.getTypeAndSegmentColumn('index', _controlPnt, segmentID) - _idx = _idx[0] - - logger.info(f' first controlPnt is _idx: {_idx}') - logger.info(f' user selected rowIdx: {rowIdx}') - - # make sure our rowID is not the first control point - traceState = rowIdx > _idx - # - logger.info(f' traceState: {traceState}') - self._traceCancelButton.setEnabled(traceState) - def on_segment_edit_checkbox(self, state : int): """Respond to user toggling segment edit checkbox. diff --git a/pymapmanager/mmMap.py b/pymapmanager/mmMap.py index 3668e450..a37b247f 100644 --- a/pymapmanager/mmMap.py +++ b/pymapmanager/mmMap.py @@ -14,10 +14,6 @@ import pymapmanager from pymapmanager._logger import logger -# from pymapmanager.mmUtil import newplotdict -# from pymapmanager.mmStack import mmStack -# from pymapmanager.mmio import mmio - """ Utility functions and classes for PyMapManager. """ @@ -131,7 +127,7 @@ def getStackPath(self, sessionIdx : int): stackPath = os.path.join(_folder, stackName) return stackPath - def getStackTimepoint(self, thisStack : pymapmanager.stack) -> int: + def getStackTimepoint(self, thisStack : "pymapmanager.stack") -> int: """Given a stack, find the timepoint. """ for idx, stack in enumerate(self.stacks): @@ -207,13 +203,13 @@ def __init__(self, filePath=None, urlmap=None): self._folder = os.path.dirname(filePath) + '/' self.name = os.path.basename(filePath).strip('.txt') self.table = pd.read_table(filePath, index_col=0) - elif urlmap is not None: - doFile = False - # try loading from url - self.name = urlmap - self.server = mmio() - tmp = self.server.getfile('header', self.name) - self.table = pd.read_table(io.StringIO(tmp.decode('utf-8')), index_col=0) + # elif urlmap is not None: + # doFile = False + # # try loading from url + # self.name = urlmap + # self.server = mmio() + # tmp = self.server.getfile('header', self.name) + # self.table = pd.read_table(io.StringIO(tmp.decode('utf-8')), index_col=0) ############################################################################### # objMap (3d) diff --git a/pymapmanager/stack.py b/pymapmanager/stack.py index 71f81ad1..111722d3 100644 --- a/pymapmanager/stack.py +++ b/pymapmanager/stack.py @@ -4,14 +4,12 @@ import numpy as np -# import pymapmanager -# from pymapmanager.analysisParams import AnalysisParams +import pymapmanager from mapmanagercore import MapAnnotations #, MMapLoader - +from mapmanagercore.lazy_geo_pd_images import Metadata # from mapmanagercore.lazy_geo_pd_images.store import LazyImagesGeoPandas, ImageLoader from pymapmanager.annotations.baseAnnotationsCore import SpineAnnotationsCore, LineAnnotationsCore -# from mapmanagercore.annotations.layers import AnnotationsLayers from pymapmanager._logger import logger @@ -63,11 +61,11 @@ def __init__(self, path : str, _channel += 1 self.loadImages(channel=_channel) - def getMetadata(self) -> "MetaData": + def getMetadata(self) -> Metadata: """Get metadata from the core map. """ return self._fullMap.metadata() - + def _buildSessionMap(self): """Reduce full core map to a single session id. """ diff --git a/tests/interface/test_stack_widgets.py b/tests/interface/test_stack_widgets.py index 19a1815e..019e25d0 100644 --- a/tests/interface/test_stack_widgets.py +++ b/tests/interface/test_stack_widgets.py @@ -7,7 +7,7 @@ from pymapmanager.interface2.pyMapManagerApp2 import PyMapManagerApp from pymapmanager.interface2.stackWidgets import stackWidget2 -# from pymapmanager._logger import logger +from pymapmanager._logger import logger # this makes qapp be our SanPyApp, it is derived from QApplication @pytest.fixture(scope="session") @@ -36,6 +36,7 @@ def test_plugins(qtbot, qapp): mmapPath = mapmanagercore.data.getSingleTimepointMap() + logger.info(f'opening stack widget path {mmapPath}') stackWidgetWindow = stackWidget2(path=mmapPath) # get list of all stack widgets from app, keys are class of plugin @@ -49,7 +50,10 @@ def test_plugins(qtbot, qapp): # stack widget is special continue + logger.info(f'running plugin: {pluginName}') stackWidgetWindow.runPlugin(pluginName) + stackWidgetWindow.zoomToPointAnnotation(5) + if __name__ == '__main__': pass \ No newline at end of file From 76bdca6de9874baa4057d8f865d964e0c63eb4ed Mon Sep 17 00:00:00 2001 From: Robert Cudmore Date: Wed, 26 Jun 2024 11:43:12 -0700 Subject: [PATCH 02/26] activating workflows --- .github/workflows/test.yml | 2 +- tests/interface/test_annotationListWidget.py | 82 -------------------- 2 files changed, 1 insertion(+), 83 deletions(-) delete mode 100644 tests/interface/test_annotationListWidget.py diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 2085d924..42d8a160 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -35,7 +35,7 @@ jobs: run: | # exlude anything that requires large data (it is not included in PyPi) # pytest --cov=./tests --cov-report=xml --ignore=tests/test_gen_example_notebook.py - pytest --cov=./tests --cov-report=xml + pytest -s --cov=./tests --cov-report=xml - name: Upload coverage reports to Codecov uses: codecov/codecov-action@v4.0.1 diff --git a/tests/interface/test_annotationListWidget.py b/tests/interface/test_annotationListWidget.py deleted file mode 100644 index bd6cbd59..00000000 --- a/tests/interface/test_annotationListWidget.py +++ /dev/null @@ -1,82 +0,0 @@ -import pytest - -import mapmanagercore.data - -import pymapmanager -from pymapmanager.interface2.pyMapManagerApp2 import PyMapManagerApp -from pymapmanager.interface2.stackWidgets import stackWidget2 -from pymapmanager.interface2.stackWidgets.annotationListWidget2 import pointListWidget - -from pymapmanager._logger import logger - -# this makes qapp be our SanPyApp, it is derived from QApplication -@pytest.fixture(scope="session") -def qapp_cls(): - return PyMapManagerApp - -@pytest.fixture -def pointListWidgetObject(qtbot): - # path = '../PyMapManager-Data/maps/rr30a/rr30a_s0_ch2.tif' - path = mapmanagercore.data.getSingleTimepointMap() - - sw = stackWidget2(path=path) - # sw.showScatterPlot() - # sw.showAnalysisParams() - - # TODO: annotationListWidget should be given stack, not stack widget - # theStackWidget = sw - - stack = pymapmanager.stack(path) - # pointAnnotations = stack.getPointAnnotations() - - # title = 'xxx' - # displayOptionsDict = {} - - aPointListWidget = pointListWidget(sw) - - return stack, aPointListWidget - - -def test_pointListWidgetObject(pointListWidgetObject): - # logger.info('') - stack = pointListWidgetObject[0] - pointListWidgetObject = pointListWidgetObject[1] - - assert pointListWidgetObject is not None - pointListWidgetObject.show() - - # add a spine point annotation - pa = stack.getPointAnnotations() - z = 30 - y = 100 - x = 100 - selectSegment = 1 - newAnnotationRow = pa.addSpine(selectSegment, x, y, z) - - - # make an add event with the added row - # spineROI = pymapmanager.annotations.pointTypes.spineROI - # addEvent = pymapmanager.annotations.events.AddAnnotationEvent(z=z, - # y=y, - # x=x, - # pointType=spineROI) - # addEvent.setAddedRow(newAnnotationRow) - - # addedRow = addEvent.getAddedRow() - # assert addedRow == newAnnotationRow - - # tell the widget it was added - # pointListWidgetObject.slot_addedAnnotation(addEvent) - - # select the row we just added - # does not generate an error if we select beyond last row? - # pointListWidgetObject.on_table_selection(newAnnotationRow) - - # these do not produce errors? - # pointListWidgetObject.on_table_selection(newAnnotationRow*100) - # pointListWidgetObject.on_table_selection(-newAnnotationRow*100) - -if __name__ == '__main__': - pass - # test_pointListWidgetObject() - From be3a754e4a124b7bb1aaa0bc26ef6fdf722a8a58 Mon Sep 17 00:00:00 2001 From: Robert Cudmore Date: Wed, 26 Jun 2024 12:02:58 -0700 Subject: [PATCH 03/26] activating workflows --- pymapmanager/interface2/runInterfaceBob.py | 8 ++- tests/interface/test_app.py | 9 ++-- tests/interface/test_editEvent.py | 54 -------------------- tests/interface/test_stack_widgets.py | 59 ---------------------- 4 files changed, 13 insertions(+), 117 deletions(-) delete mode 100644 tests/interface/test_editEvent.py delete mode 100644 tests/interface/test_stack_widgets.py diff --git a/pymapmanager/interface2/runInterfaceBob.py b/pymapmanager/interface2/runInterfaceBob.py index cead5f72..f64bdab9 100644 --- a/pymapmanager/interface2/runInterfaceBob.py +++ b/pymapmanager/interface2/runInterfaceBob.py @@ -3,7 +3,10 @@ import sys +import mapmanagercore.data + from pymapmanager.interface2.pyMapManagerApp2 import PyMapManagerApp +from pymapmanager._logger import logger def run(): app = PyMapManagerApp() @@ -28,6 +31,9 @@ def run(): # from trySegment, works! # path = '/Users/cudmore/Desktop/trySeg.mmap' + path = mapmanagercore.data.getSingleTimepointMap() + + logger.info(f'app.loadStackWidgeth: {path}') sw2 = app.loadStackWidget(path) # works @@ -44,7 +50,7 @@ def run(): sw2.zoomToPointAnnotation(0, isAlt=True) - sw2.runPlugin('Tracing', inDock=True) + # sw2.runPlugin('Tracing', inDock=True) # TODO: get this working # _map = sw2.getStack().sessionMap diff --git a/tests/interface/test_app.py b/tests/interface/test_app.py index c9616fef..a5da62c9 100644 --- a/tests/interface/test_app.py +++ b/tests/interface/test_app.py @@ -5,15 +5,18 @@ from pymapmanager.interface2 import PyMapManagerApp from pymapmanager.interface2.stackWidgets import stackWidget2 -# from pymapmanager._logger import logger +from pymapmanager._logger import logger # this makes qapp be our PyMapManagerApp, it is derived from QApplication @pytest.fixture(scope="session") def qapp_cls(): return PyMapManagerApp +def test_app(qtbot, qapp): + logger.info(f'app:{qapp}') + @pytest.fixture -def stackWidgetObject(qtbot, qapp): +def _stackWidgetObject(qtbot, qapp): # path = '../PyMapManager-Data/maps/rr30a/rr30a_s0_ch2.tif' path = mapmanagercore.data.getSingleTimepointMap() sw = stackWidget2(path=path) @@ -27,7 +30,7 @@ def stackWidgetObject(qtbot, qapp): # def test_stackWidget(stackWidgetObject): # assert stackWidgetObject is not None -def test_stackWidget_zoomToPointAnnotation(stackWidgetObject, qapp): +def _test_stackWidget_zoomToPointAnnotation(stackWidgetObject, qapp): # figure out how to set log level # caplog.set_level(logger.ERROR) diff --git a/tests/interface/test_editEvent.py b/tests/interface/test_editEvent.py deleted file mode 100644 index 128c1e2e..00000000 --- a/tests/interface/test_editEvent.py +++ /dev/null @@ -1,54 +0,0 @@ -import pytest - -import mapmanagercore.data - -from pymapmanager.interface2 import PyMapManagerApp -from pymapmanager.interface2.stackWidgets import stackWidget2 -from pymapmanager.interface2.stackWidgets.event.spineEvent import EditSpinePropertyEvent, DeleteSpineEvent -from pymapmanager._logger import logger - -# this makes qapp be our PyMapManagerApp, it is derived from QApplication -@pytest.fixture(scope="session") -def qapp_cls(): - return PyMapManagerApp - -@pytest.fixture -def stackWidgetObject(qtbot, qapp): - # path = '../PyMapManager-Data/maps/rr30a/rr30a_s0_ch2.tif' - path = mapmanagercore.data.getSingleTimepointMap() - sw = stackWidget2(path=path) - - # sw.showScatterPlot2(show=True) - # sw.showAnalysisParams() - - return sw - -def test_deleteSpine(stackWidgetObject, qapp): - spineID = 2 - dse = DeleteSpineEvent(stackWidgetObject, spineID=spineID) - -def test_editSpineProperty(stackWidgetObject, qapp): - logger.info('') - - assert stackWidgetObject is not None - assert isinstance(stackWidgetObject, stackWidget2) - - # path = '../PyMapManager-Data/maps/rr30a/rr30a_s0_ch2.tif' - # sw = stackWidget2(path=path) - - spineID = 2 - col = 'userType' - value = 12 - - esp = EditSpinePropertyEvent(stackWidgetObject, spineID=spineID, col=col, value=value) - - spineID = 5 - col = 'accept' - value = True # str, int, float - esp.addEdit(spineID, col, value) - - for idx, oneEdit in enumerate(esp): - print(' ', idx, oneEdit) - - # print('getList:', esp.getList()) - \ No newline at end of file diff --git a/tests/interface/test_stack_widgets.py b/tests/interface/test_stack_widgets.py deleted file mode 100644 index 019e25d0..00000000 --- a/tests/interface/test_stack_widgets.py +++ /dev/null @@ -1,59 +0,0 @@ -import os -import sys -import pytest - -import mapmanagercore.data - -from pymapmanager.interface2.pyMapManagerApp2 import PyMapManagerApp -from pymapmanager.interface2.stackWidgets import stackWidget2 - -from pymapmanager._logger import logger - -# this makes qapp be our SanPyApp, it is derived from QApplication -@pytest.fixture(scope="session") -def qapp_cls(): - return PyMapManagerApp - -# def test_stack_plugins(): -# # logger.info(f'calling pyMapManagerApp.loadPlugins()') - -# app = PyMapManagerApp() - -# _stack = app.getStackPluginDict() -# _map = app.getMapPluginDict() - -# # for k,v in pluginDict.items(): -# # logger.info(k) -# # # logger.info(v) -# # for k2, v2 in v.items(): -# # logger.info(f' {k2}: {v2}') - -def test_plugins(qtbot, qapp): - """Run all plugins through a number of different tests. - """ - - print('qapp:', qapp) - - mmapPath = mapmanagercore.data.getSingleTimepointMap() - - logger.info(f'opening stack widget path {mmapPath}') - stackWidgetWindow = stackWidget2(path=mmapPath) - - # get list of all stack widgets from app, keys are class of plugin - stackPluginDict = qapp.getStackPluginDict() - - for pluginName, _dict in stackPluginDict.items(): - # if pluginName in ['Point List', 'Line List', 'Histogram', 'Search Widget', 'Selection Widget']: - # stackWidgetWindow.runPlugin(pluginName) - - if pluginName in ['Stack Widget', 'line plot', 'point plot', 'not assigned']: - # stack widget is special - continue - - logger.info(f'running plugin: {pluginName}') - stackWidgetWindow.runPlugin(pluginName) - - stackWidgetWindow.zoomToPointAnnotation(5) - -if __name__ == '__main__': - pass \ No newline at end of file From 47af0295b39d8fc729e6b494c3f1908edf708165 Mon Sep 17 00:00:00 2001 From: Robert Cudmore Date: Wed, 26 Jun 2024 12:05:05 -0700 Subject: [PATCH 04/26] activating workflows --- .github/workflows/test.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 42d8a160..e4cef2cb 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -13,7 +13,8 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - platform: [ubuntu-latest, windows-latest, macos-latest] + # platform: [ubuntu-latest, windows-latest, macos-latest] + platform: [windows-latest, macos-latest] python-version: ["3.11"] steps: - uses: actions/checkout@v3 From 0ee8ac47f61bdef3ab029223e80edd7bc4677d57 Mon Sep 17 00:00:00 2001 From: Robert Cudmore Date: Wed, 26 Jun 2024 13:15:07 -0700 Subject: [PATCH 05/26] activating workflows v4 --- pymapmanager/interface2/pyMapManagerApp2.py | 50 +++------------------ pymapmanager/interface2/runInterfaceBob.py | 2 +- 2 files changed, 7 insertions(+), 45 deletions(-) diff --git a/pymapmanager/interface2/pyMapManagerApp2.py b/pymapmanager/interface2/pyMapManagerApp2.py index bbb78763..2c40d1d1 100644 --- a/pymapmanager/interface2/pyMapManagerApp2.py +++ b/pymapmanager/interface2/pyMapManagerApp2.py @@ -114,7 +114,8 @@ def loadPlugins(verbose=False, pluginType='stack') -> dict: return pluginDict class PyMapManagerApp(QtWidgets.QApplication): - def __init__(self, argv=[''], deferFirstWindow=False): + def __init__(self, argv, deferFirstWindow=False): + super().__init__(argv) self._config = pymapmanager.interface2.Preferences(self) @@ -124,6 +125,8 @@ def __init__(self, argv=[''], deferFirstWindow=False): logLevel = self.getConfigDict()['logLevel'] setLogLevel(logLevel) + logger.info(f'Starting PyMapManagerApp() logLevel:{logLevel} argv:{argv}') + self.setTheme() # set theme to loaded config dict @@ -452,50 +455,9 @@ def main(): This is an entry point specified in setup.py and used by PyInstaller. """ - - # app = PyMapManagerApp() - # abj: previous instantiation created a __main__.PyMapManagerApp. - # so it is classified as part of the main module, which does not allow for isinstance checking - app = pymapmanager.interface2.pyMapManagerApp2.PyMapManagerApp() - sys.exit(app.exec_()) - -def tstSpineRun(): - - path = '../PyMapManager-Data/maps/rr30a/rr30a.txt' - - app = PyMapManagerApp() - _map = app.loadMap(path) - - app.openMapWidget(0) - - # if 0: - # # plot a run for tp 2, annotation 94 - # tp = 2 - # stack = _map.stacks[tp] - # pa = stack.getPointAnnotations() - # selPnt = [43] - # isAlt = True - # selectionEvent = pymapmanager.annotations.SelectionEvent(pa, selPnt, isAlt=isAlt, stack=stack) - - # app.slot_selectAnnotation(selectionEvent, plusMinus=1) - - if 1: - # open one stack for given timepoint - timepoint = 2 - bsw = app.openStack2(_map, timepoint) - - spineIdx = 142 - isAlt = False - bsw.zoomToPointAnnotation(spineIdx, isAlt=isAlt, select=True) - - # slot_setSlice() does nothing - # stack = bsw.getStack() - # pa = stack.getPointAnnotations() - # z = pa.getValue('z', spineIdx) - # bsw.slot_setSlice(20) - + # logger.info('Starting PyMapManagerApp in main()') + app = PyMapManagerApp(sys.argv) sys.exit(app.exec_()) if __name__ == '__main__': - #tstSpineRun() main() \ No newline at end of file diff --git a/pymapmanager/interface2/runInterfaceBob.py b/pymapmanager/interface2/runInterfaceBob.py index f64bdab9..5a15b64a 100644 --- a/pymapmanager/interface2/runInterfaceBob.py +++ b/pymapmanager/interface2/runInterfaceBob.py @@ -9,7 +9,7 @@ from pymapmanager._logger import logger def run(): - app = PyMapManagerApp() + app = PyMapManagerApp(sys.argv) # path = '../PyMapManager-Data/core-map/one-timepoint/oneTimepoint.mmap' From 9992da55d76434a35c58c11bcd3df84d159db005 Mon Sep 17 00:00:00 2001 From: Robert Cudmore Date: Wed, 26 Jun 2024 13:19:48 -0700 Subject: [PATCH 06/26] activating workflows v4 --- .github/workflows/test.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index e4cef2cb..2f6d6e3c 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -26,6 +26,7 @@ jobs: cache-dependency-path: setup.py - name: Install dependencies run: | + pip install . pip install '.[tests]' - name: Run flake8 From e4d6012be4453f9b546ea674300c18e66f4741c0 Mon Sep 17 00:00:00 2001 From: Robert Cudmore Date: Wed, 26 Jun 2024 14:19:02 -0700 Subject: [PATCH 07/26] activating workflows v4 --- pymapmanager/interface2/pyMapManagerApp2.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pymapmanager/interface2/pyMapManagerApp2.py b/pymapmanager/interface2/pyMapManagerApp2.py index 2c40d1d1..6fb8c397 100644 --- a/pymapmanager/interface2/pyMapManagerApp2.py +++ b/pymapmanager/interface2/pyMapManagerApp2.py @@ -116,6 +116,8 @@ def loadPlugins(verbose=False, pluginType='stack') -> dict: class PyMapManagerApp(QtWidgets.QApplication): def __init__(self, argv, deferFirstWindow=False): + print('xxx here xxx') + super().__init__(argv) self._config = pymapmanager.interface2.Preferences(self) From 8b04950c1018f9e5dd820ba804b6e1723bc5ec48 Mon Sep 17 00:00:00 2001 From: Robert Cudmore Date: Wed, 26 Jun 2024 14:20:46 -0700 Subject: [PATCH 08/26] activating workflows v5 --- pymapmanager/interface2/pyMapManagerApp2.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/pymapmanager/interface2/pyMapManagerApp2.py b/pymapmanager/interface2/pyMapManagerApp2.py index 6fb8c397..0c842901 100644 --- a/pymapmanager/interface2/pyMapManagerApp2.py +++ b/pymapmanager/interface2/pyMapManagerApp2.py @@ -114,12 +114,11 @@ def loadPlugins(verbose=False, pluginType='stack') -> dict: return pluginDict class PyMapManagerApp(QtWidgets.QApplication): - def __init__(self, argv, deferFirstWindow=False): - - print('xxx here xxx') - + def __init__(self, argv=[], deferFirstWindow=False): super().__init__(argv) + return + self._config = pymapmanager.interface2.Preferences(self) # util class to save/load app preferences including recent paths From 8f79e4e7a9aaf31479c61d1ffeca02663acd5743 Mon Sep 17 00:00:00 2001 From: Robert Cudmore Date: Wed, 26 Jun 2024 14:26:33 -0700 Subject: [PATCH 09/26] activating workflows v5 --- pymapmanager/interface2/__init__.py | 4 +++- pymapmanager/interface2/pyMapManagerApp2.py | 6 ++++-- tests/interface/test_app.py | 6 ++++-- tests/test_stack.py | 2 +- 4 files changed, 12 insertions(+), 6 deletions(-) diff --git a/pymapmanager/interface2/__init__.py b/pymapmanager/interface2/__init__.py index af64e98f..c6b687be 100644 --- a/pymapmanager/interface2/__init__.py +++ b/pymapmanager/interface2/__init__.py @@ -5,4 +5,6 @@ from .preferences import Preferences -from .appDisplayOptions import AppDisplayOptions \ No newline at end of file +from .appDisplayOptions import AppDisplayOptions + +from .pyMapManagerApp2 import myApp \ No newline at end of file diff --git a/pymapmanager/interface2/pyMapManagerApp2.py b/pymapmanager/interface2/pyMapManagerApp2.py index 0c842901..7812c0a9 100644 --- a/pymapmanager/interface2/pyMapManagerApp2.py +++ b/pymapmanager/interface2/pyMapManagerApp2.py @@ -113,11 +113,13 @@ def loadPlugins(verbose=False, pluginType='stack') -> dict: return pluginDict -class PyMapManagerApp(QtWidgets.QApplication): +class myApp(QtWidgets.QApplication): def __init__(self, argv=[], deferFirstWindow=False): super().__init__(argv) - return +class PyMapManagerApp(QtWidgets.QApplication): + def __init__(self, argv=[], deferFirstWindow=False): + super().__init__(argv) self._config = pymapmanager.interface2.Preferences(self) # util class to save/load app preferences including recent paths diff --git a/tests/interface/test_app.py b/tests/interface/test_app.py index a5da62c9..7be75338 100644 --- a/tests/interface/test_app.py +++ b/tests/interface/test_app.py @@ -2,7 +2,8 @@ import mapmanagercore.data -from pymapmanager.interface2 import PyMapManagerApp +from pymapmanager.interface2 import myApp +# from pymapmanager.interface2 import PyMapManagerApp from pymapmanager.interface2.stackWidgets import stackWidget2 from pymapmanager._logger import logger @@ -10,7 +11,8 @@ # this makes qapp be our PyMapManagerApp, it is derived from QApplication @pytest.fixture(scope="session") def qapp_cls(): - return PyMapManagerApp + # return PyMapManagerApp + return myApp def test_app(qtbot, qapp): logger.info(f'app:{qapp}') diff --git a/tests/test_stack.py b/tests/test_stack.py index 93544ab6..593d1422 100644 --- a/tests/test_stack.py +++ b/tests/test_stack.py @@ -1,7 +1,7 @@ import pymapmanager as pmm -def test_init_stack(): +def _test_init_stack(): return stackPath = '../PyMapManager-Data/one-timepoint/rr30a_s0_ch2.tif' From 20fcfd6d7e0bb80c28d67db82b5f5341a8df4f86 Mon Sep 17 00:00:00 2001 From: Robert Cudmore Date: Wed, 26 Jun 2024 15:12:33 -0700 Subject: [PATCH 10/26] activating workflows v5 --- .github/workflows/test.yml | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 2f6d6e3c..4d0c3c99 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -26,8 +26,13 @@ jobs: cache-dependency-path: setup.py - name: Install dependencies run: | - pip install . - pip install '.[tests]' + # pip install . + # pip install '.[tests]' + pip install PyQt5 + pip install pytest + pip install pytest-cov + pip install pytest-qt + pip install flake8 - name: Run flake8 run: | From 87528451682af838e567bb90cffef41063b66f21 Mon Sep 17 00:00:00 2001 From: Robert Cudmore Date: Wed, 26 Jun 2024 16:31:27 -0700 Subject: [PATCH 11/26] activating workflows v5 --- .github/workflows/test.yml | 3 ++- pymapmanager/interface2/pyMapManagerApp2.py | 4 ---- tests/interface/test_app.py | 6 ++---- 3 files changed, 4 insertions(+), 9 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 4d0c3c99..b339eaea 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -42,7 +42,8 @@ jobs: run: | # exlude anything that requires large data (it is not included in PyPi) # pytest --cov=./tests --cov-report=xml --ignore=tests/test_gen_example_notebook.py - pytest -s --cov=./tests --cov-report=xml + # pytest -s --cov=./tests --cov-report=xml + pytest -s --cov=./tests --cov-report=xml tests/interface/test_app_tmp.py - name: Upload coverage reports to Codecov uses: codecov/codecov-action@v4.0.1 diff --git a/pymapmanager/interface2/pyMapManagerApp2.py b/pymapmanager/interface2/pyMapManagerApp2.py index 7812c0a9..834af10c 100644 --- a/pymapmanager/interface2/pyMapManagerApp2.py +++ b/pymapmanager/interface2/pyMapManagerApp2.py @@ -113,10 +113,6 @@ def loadPlugins(verbose=False, pluginType='stack') -> dict: return pluginDict -class myApp(QtWidgets.QApplication): - def __init__(self, argv=[], deferFirstWindow=False): - super().__init__(argv) - class PyMapManagerApp(QtWidgets.QApplication): def __init__(self, argv=[], deferFirstWindow=False): super().__init__(argv) diff --git a/tests/interface/test_app.py b/tests/interface/test_app.py index 7be75338..a5da62c9 100644 --- a/tests/interface/test_app.py +++ b/tests/interface/test_app.py @@ -2,8 +2,7 @@ import mapmanagercore.data -from pymapmanager.interface2 import myApp -# from pymapmanager.interface2 import PyMapManagerApp +from pymapmanager.interface2 import PyMapManagerApp from pymapmanager.interface2.stackWidgets import stackWidget2 from pymapmanager._logger import logger @@ -11,8 +10,7 @@ # this makes qapp be our PyMapManagerApp, it is derived from QApplication @pytest.fixture(scope="session") def qapp_cls(): - # return PyMapManagerApp - return myApp + return PyMapManagerApp def test_app(qtbot, qapp): logger.info(f'app:{qapp}') From cdafd224f4be8434b161451899f530c80b480b20 Mon Sep 17 00:00:00 2001 From: Robert Cudmore Date: Wed, 26 Jun 2024 16:34:21 -0700 Subject: [PATCH 12/26] activating workflows v5 --- tests/interface/test_app_tmp.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 tests/interface/test_app_tmp.py diff --git a/tests/interface/test_app_tmp.py b/tests/interface/test_app_tmp.py new file mode 100644 index 00000000..e8f590ca --- /dev/null +++ b/tests/interface/test_app_tmp.py @@ -0,0 +1,14 @@ +import pytest + +from qtpy import QtGui, QtWidgets # QtCore + +class myApp(QtWidgets.QApplication): + def __init__(self, argv=[], deferFirstWindow=False): + super().__init__(argv) + +@pytest.fixture(scope="session") +def qapp_cls(): + return myApp + +def test_app(qtbot, qapp): + print(f'qapp is:{qapp}') From ce2127b73b14a2631883d2ce6200ceab4e918025 Mon Sep 17 00:00:00 2001 From: Robert Cudmore Date: Wed, 26 Jun 2024 16:36:22 -0700 Subject: [PATCH 13/26] activating workflows v5 --- .github/workflows/test.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b339eaea..b50836ef 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -28,6 +28,7 @@ jobs: run: | # pip install . # pip install '.[tests]' + pip install qtpy pip install PyQt5 pip install pytest pip install pytest-cov From 6a55aa576e5dfb859fbdb290207ff4ada22c2977 Mon Sep 17 00:00:00 2001 From: Robert Cudmore Date: Wed, 26 Jun 2024 16:42:16 -0700 Subject: [PATCH 14/26] activating workflows v5 --- tests/interface/test_app_tmp.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/interface/test_app_tmp.py b/tests/interface/test_app_tmp.py index e8f590ca..6c9d46e7 100644 --- a/tests/interface/test_app_tmp.py +++ b/tests/interface/test_app_tmp.py @@ -3,7 +3,7 @@ from qtpy import QtGui, QtWidgets # QtCore class myApp(QtWidgets.QApplication): - def __init__(self, argv=[], deferFirstWindow=False): + def __init__(self, argv, deferFirstWindow=False): super().__init__(argv) @pytest.fixture(scope="session") From 7619ee70c9c1e90159fcc84d238e33e09c250e1b Mon Sep 17 00:00:00 2001 From: Robert Cudmore Date: Wed, 26 Jun 2024 16:44:01 -0700 Subject: [PATCH 15/26] activating workflows v5 --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b50836ef..3e92df7d 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -44,7 +44,7 @@ jobs: # exlude anything that requires large data (it is not included in PyPi) # pytest --cov=./tests --cov-report=xml --ignore=tests/test_gen_example_notebook.py # pytest -s --cov=./tests --cov-report=xml - pytest -s --cov=./tests --cov-report=xml tests/interface/test_app_tmp.py + pytest tests/interface/test_app_tmp.py - name: Upload coverage reports to Codecov uses: codecov/codecov-action@v4.0.1 From 14f4221f93b9f9e516753435f583f13643f99ff9 Mon Sep 17 00:00:00 2001 From: Robert Cudmore Date: Wed, 26 Jun 2024 16:48:54 -0700 Subject: [PATCH 16/26] activating workflows v5 --- tests/interface/test_app_tmp.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/interface/test_app_tmp.py b/tests/interface/test_app_tmp.py index 6c9d46e7..bca29af8 100644 --- a/tests/interface/test_app_tmp.py +++ b/tests/interface/test_app_tmp.py @@ -1,6 +1,6 @@ import pytest -from qtpy import QtGui, QtWidgets # QtCore +from qtpy import QtWidgets # QtCore class myApp(QtWidgets.QApplication): def __init__(self, argv, deferFirstWindow=False): @@ -10,5 +10,5 @@ def __init__(self, argv, deferFirstWindow=False): def qapp_cls(): return myApp -def test_app(qtbot, qapp): +def test_app(qapp): print(f'qapp is:{qapp}') From 289af3f8ddedbc99d6b0bff94c704765d94cb1dc Mon Sep 17 00:00:00 2001 From: Robert Cudmore Date: Wed, 26 Jun 2024 16:55:30 -0700 Subject: [PATCH 17/26] activating workflows v5 --- .github/workflows/test.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 3e92df7d..dd572cf2 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -10,7 +10,8 @@ permissions: jobs: test: - runs-on: ubuntu-latest + # runs-on: ubuntu-latest + runs-on: ${{ matrix.platform }} strategy: matrix: # platform: [ubuntu-latest, windows-latest, macos-latest] From ab2b7d032770b5f3b236cf6414478243e696d3ed Mon Sep 17 00:00:00 2001 From: Robert Cudmore Date: Wed, 26 Jun 2024 17:11:42 -0700 Subject: [PATCH 18/26] maybe fixed pytest workflow --- .github/workflows/test.yml | 14 ++++---------- pymapmanager/interface2/__init__.py | 2 -- tests/interface/test_app.py | 4 ++-- tests/interface/test_app_tmp.py | 14 -------------- 4 files changed, 6 insertions(+), 28 deletions(-) delete mode 100644 tests/interface/test_app_tmp.py diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index dd572cf2..9294a7e7 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -27,14 +27,8 @@ jobs: cache-dependency-path: setup.py - name: Install dependencies run: | - # pip install . - # pip install '.[tests]' - pip install qtpy - pip install PyQt5 - pip install pytest - pip install pytest-cov - pip install pytest-qt - pip install flake8 + pip install . + pip install '.[tests]' - name: Run flake8 run: | @@ -44,8 +38,8 @@ jobs: run: | # exlude anything that requires large data (it is not included in PyPi) # pytest --cov=./tests --cov-report=xml --ignore=tests/test_gen_example_notebook.py - # pytest -s --cov=./tests --cov-report=xml - pytest tests/interface/test_app_tmp.py + pytest -s --cov=./tests --cov-report=xml + # pytest tests/interface/test_app_tmp.py - name: Upload coverage reports to Codecov uses: codecov/codecov-action@v4.0.1 diff --git a/pymapmanager/interface2/__init__.py b/pymapmanager/interface2/__init__.py index c6b687be..9b8c5d4f 100644 --- a/pymapmanager/interface2/__init__.py +++ b/pymapmanager/interface2/__init__.py @@ -6,5 +6,3 @@ from .preferences import Preferences from .appDisplayOptions import AppDisplayOptions - -from .pyMapManagerApp2 import myApp \ No newline at end of file diff --git a/tests/interface/test_app.py b/tests/interface/test_app.py index a5da62c9..876317da 100644 --- a/tests/interface/test_app.py +++ b/tests/interface/test_app.py @@ -16,7 +16,7 @@ def test_app(qtbot, qapp): logger.info(f'app:{qapp}') @pytest.fixture -def _stackWidgetObject(qtbot, qapp): +def stackWidgetObject(qtbot, qapp): # path = '../PyMapManager-Data/maps/rr30a/rr30a_s0_ch2.tif' path = mapmanagercore.data.getSingleTimepointMap() sw = stackWidget2(path=path) @@ -30,7 +30,7 @@ def _stackWidgetObject(qtbot, qapp): # def test_stackWidget(stackWidgetObject): # assert stackWidgetObject is not None -def _test_stackWidget_zoomToPointAnnotation(stackWidgetObject, qapp): +def test_stackWidget_zoomToPointAnnotation(stackWidgetObject, qapp): # figure out how to set log level # caplog.set_level(logger.ERROR) diff --git a/tests/interface/test_app_tmp.py b/tests/interface/test_app_tmp.py deleted file mode 100644 index bca29af8..00000000 --- a/tests/interface/test_app_tmp.py +++ /dev/null @@ -1,14 +0,0 @@ -import pytest - -from qtpy import QtWidgets # QtCore - -class myApp(QtWidgets.QApplication): - def __init__(self, argv, deferFirstWindow=False): - super().__init__(argv) - -@pytest.fixture(scope="session") -def qapp_cls(): - return myApp - -def test_app(qapp): - print(f'qapp is:{qapp}') From 19fc40c4136ce14c88ea13d4ce614088672689d2 Mon Sep 17 00:00:00 2001 From: Robert Cudmore Date: Wed, 26 Jun 2024 17:12:15 -0700 Subject: [PATCH 19/26] maybe fixed pytest workflow --- tests/interface/test_editEvent.py | 54 ++++++++++++++++++++++++ tests/interface/test_stack_widgets.py | 59 +++++++++++++++++++++++++++ 2 files changed, 113 insertions(+) create mode 100644 tests/interface/test_editEvent.py create mode 100644 tests/interface/test_stack_widgets.py diff --git a/tests/interface/test_editEvent.py b/tests/interface/test_editEvent.py new file mode 100644 index 00000000..128c1e2e --- /dev/null +++ b/tests/interface/test_editEvent.py @@ -0,0 +1,54 @@ +import pytest + +import mapmanagercore.data + +from pymapmanager.interface2 import PyMapManagerApp +from pymapmanager.interface2.stackWidgets import stackWidget2 +from pymapmanager.interface2.stackWidgets.event.spineEvent import EditSpinePropertyEvent, DeleteSpineEvent +from pymapmanager._logger import logger + +# this makes qapp be our PyMapManagerApp, it is derived from QApplication +@pytest.fixture(scope="session") +def qapp_cls(): + return PyMapManagerApp + +@pytest.fixture +def stackWidgetObject(qtbot, qapp): + # path = '../PyMapManager-Data/maps/rr30a/rr30a_s0_ch2.tif' + path = mapmanagercore.data.getSingleTimepointMap() + sw = stackWidget2(path=path) + + # sw.showScatterPlot2(show=True) + # sw.showAnalysisParams() + + return sw + +def test_deleteSpine(stackWidgetObject, qapp): + spineID = 2 + dse = DeleteSpineEvent(stackWidgetObject, spineID=spineID) + +def test_editSpineProperty(stackWidgetObject, qapp): + logger.info('') + + assert stackWidgetObject is not None + assert isinstance(stackWidgetObject, stackWidget2) + + # path = '../PyMapManager-Data/maps/rr30a/rr30a_s0_ch2.tif' + # sw = stackWidget2(path=path) + + spineID = 2 + col = 'userType' + value = 12 + + esp = EditSpinePropertyEvent(stackWidgetObject, spineID=spineID, col=col, value=value) + + spineID = 5 + col = 'accept' + value = True # str, int, float + esp.addEdit(spineID, col, value) + + for idx, oneEdit in enumerate(esp): + print(' ', idx, oneEdit) + + # print('getList:', esp.getList()) + \ No newline at end of file diff --git a/tests/interface/test_stack_widgets.py b/tests/interface/test_stack_widgets.py new file mode 100644 index 00000000..3a2e53ed --- /dev/null +++ b/tests/interface/test_stack_widgets.py @@ -0,0 +1,59 @@ +import os +import sys +import pytest + +import mapmanagercore.data + +from pymapmanager.interface2.pyMapManagerApp2 import PyMapManagerApp +from pymapmanager.interface2.stackWidgets import stackWidget2 + +from pymapmanager._logger import logger + +# this makes qapp be our PyMapManagerApp, it is derived from QApplication +@pytest.fixture(scope="session") +def qapp_cls(): + return PyMapManagerApp + +# def test_stack_plugins(): +# # logger.info(f'calling pyMapManagerApp.loadPlugins()') + +# app = PyMapManagerApp() + +# _stack = app.getStackPluginDict() +# _map = app.getMapPluginDict() + +# # for k,v in pluginDict.items(): +# # logger.info(k) +# # # logger.info(v) +# # for k2, v2 in v.items(): +# # logger.info(f' {k2}: {v2}') + +def test_plugins(qtbot, qapp): + """Run all plugins through a number of different tests. + """ + + print('qapp:', qapp) + + mmapPath = mapmanagercore.data.getSingleTimepointMap() + + logger.info(f'opening stack widget path {mmapPath}') + stackWidgetWindow = stackWidget2(path=mmapPath) + + # get list of all stack widgets from app, keys are class of plugin + stackPluginDict = qapp.getStackPluginDict() + + for pluginName, _dict in stackPluginDict.items(): + # if pluginName in ['Point List', 'Line List', 'Histogram', 'Search Widget', 'Selection Widget']: + # stackWidgetWindow.runPlugin(pluginName) + + if pluginName in ['Stack Widget', 'line plot', 'point plot', 'not assigned']: + # stack widget is special + continue + + logger.info(f'running plugin: {pluginName}') + stackWidgetWindow.runPlugin(pluginName) + + stackWidgetWindow.zoomToPointAnnotation(5) + +if __name__ == '__main__': + pass \ No newline at end of file From f7b05f2b3ce661528f9e312c171ebe6a707c8350 Mon Sep 17 00:00:00 2001 From: Robert Cudmore Date: Thu, 27 Jun 2024 11:11:31 -0700 Subject: [PATCH 20/26] fixing badges --- pymapmanager/interface2/stackWidgets/annotationPlotWidget2.py | 1 + readme.md | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/pymapmanager/interface2/stackWidgets/annotationPlotWidget2.py b/pymapmanager/interface2/stackWidgets/annotationPlotWidget2.py index 01a5b503..7ac1b9a3 100644 --- a/pymapmanager/interface2/stackWidgets/annotationPlotWidget2.py +++ b/pymapmanager/interface2/stackWidgets/annotationPlotWidget2.py @@ -7,6 +7,7 @@ from pymapmanager.interface2.stackWidgets import stackWidget2 from pymapmanager.annotations.baseAnnotationsCore import AnnotationsCore, SpineAnnotationsCore, LineAnnotationsCore +import math import time from typing import List, Optional diff --git a/readme.md b/readme.md index 30a2d758..273afd58 100644 --- a/readme.md +++ b/readme.md @@ -1,9 +1,9 @@ [![PyPI version](https://badge.fury.io/py/pymapmanager.svg)](https://badge.fury.io/py/pymapmanager) -[![tests](https://github.com/mapmanager/PyMapManager/workflows/Test/badge.svg)](https://github.com/mapmanager/PyMapManager/actions) +[![tests](https://github.com/mapmanager/PyMapManager/actions/workflows/test.yml/badge.svg)](https://github.com/mapmanager/PyMapManager/actions) [![codecov](https://codecov.io/gh/mapmanager/PyMapManager/branch/main/graph/badge.svg?token=UIRVU7IZG0)](https://codecov.io/gh/mapmanager/PyMapManager)
[![OS](https://img.shields.io/badge/OS-Linux|Windows|macOS-blue.svg)]() -[![Python](https://img.shields.io/badge/python-3.8|3.9|3.10|3.11-blue.svg)](https://www.python.org/downloads/release/python-3111/) +[![Python](https://img.shields.io/badge/python-3.11-blue.svg)](https://www.python.org/downloads/release/python-3111/) [![License](https://img.shields.io/badge/license-GPLv3-blue)](https://github.com/cudmore/mapmanager/blob/master/LICENSE) From beea07147a8ee8314e16aca7b670b45b941c3616 Mon Sep 17 00:00:00 2001 From: Robert Cudmore Date: Thu, 27 Jun 2024 11:20:42 -0700 Subject: [PATCH 21/26] fixing badges --- .github/workflows/test.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 9294a7e7..a48f4165 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -27,9 +27,11 @@ jobs: cache-dependency-path: setup.py - name: Install dependencies run: | - pip install . + # pip install . pip install '.[tests]' - + # so we don't need to depend on pip install MapManagerCore + git clone git@github.com:mapmanager/MapManagerCore.git + pip install MapManagerCore/. - name: Run flake8 run: | flake8 ./pymapmanager --count --select=E9,F63,F7,F82 --show-source --statistics From 7c5e32c5d6f73148430aa734f38180f80388f9cb Mon Sep 17 00:00:00 2001 From: Robert Cudmore Date: Thu, 27 Jun 2024 11:25:26 -0700 Subject: [PATCH 22/26] fixing badges 2 --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index a48f4165..50103bc0 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -30,7 +30,7 @@ jobs: # pip install . pip install '.[tests]' # so we don't need to depend on pip install MapManagerCore - git clone git@github.com:mapmanager/MapManagerCore.git + git clone https://github.com/mapmanager/MapManagerCore.git pip install MapManagerCore/. - name: Run flake8 run: | From 3ea27981eec0dad0949851f28837117ba6607cfd Mon Sep 17 00:00:00 2001 From: Robert Cudmore Date: Thu, 27 Jun 2024 11:28:58 -0700 Subject: [PATCH 23/26] fixing badges 2 --- .github/workflows/test.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 50103bc0..338eb98c 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -40,7 +40,9 @@ jobs: run: | # exlude anything that requires large data (it is not included in PyPi) # pytest --cov=./tests --cov-report=xml --ignore=tests/test_gen_example_notebook.py - pytest -s --cov=./tests --cov-report=xml + # need to specify tests/ folder because now we have a + # MapManagerCore folder which has its own tests/ + pytest -s --cov=./tests --cov-report=xml tests/ # pytest tests/interface/test_app_tmp.py - name: Upload coverage reports to Codecov From 41f9336fad1316a81993e215e995a26591084559 Mon Sep 17 00:00:00 2001 From: Robert Cudmore Date: Thu, 27 Jun 2024 11:34:41 -0700 Subject: [PATCH 24/26] fixing badges 2 --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 338eb98c..42033b2d 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -30,7 +30,7 @@ jobs: # pip install . pip install '.[tests]' # so we don't need to depend on pip install MapManagerCore - git clone https://github.com/mapmanager/MapManagerCore.git + git clone -b cudmore-dev https://github.com/mapmanager/MapManagerCore.git pip install MapManagerCore/. - name: Run flake8 run: | From 7496096656ba5b3a0dd86edf53adcd627a4e787a Mon Sep 17 00:00:00 2001 From: Robert Cudmore Date: Tue, 9 Jul 2024 13:21:12 -0400 Subject: [PATCH 25/26] push for j on tue 7-9 --- .../annotations/baseAnnotationsCore.py | 31 ++- pymapmanager/interface2/appDisplayOptions.py | 2 +- .../interface2/core/scatter_plot_widget.py | 4 +- pymapmanager/interface2/core/search_widget.py | 78 ++++--- pymapmanager/interface2/mainMenus.py | 58 +++-- pymapmanager/interface2/pyMapManagerApp2.py | 8 +- pymapmanager/interface2/runInterfaceBob.py | 4 +- .../stackWidgets/annotationListWidget2.py | 2 +- .../stackWidgets/annotationPlotWidget2.py | 77 +++--- .../stackWidgets/event/spineEvent.py | 52 ++++- .../stackWidgets/histogramWidget2.py | 113 ++++++--- .../stackWidgets/imagePlotWidget2.py | 185 ++++----------- .../interface2/stackWidgets/mmWidget2.py | 13 +- .../interface2/stackWidgets/stackWidget2.py | 219 +++++++----------- pymapmanager/stack.py | 83 +++---- readme-dev.md | 11 +- tests/test_mmMap.py | 2 + 17 files changed, 487 insertions(+), 455 deletions(-) diff --git a/pymapmanager/annotations/baseAnnotationsCore.py b/pymapmanager/annotations/baseAnnotationsCore.py index 32dd6ae2..dbb85850 100644 --- a/pymapmanager/annotations/baseAnnotationsCore.py +++ b/pymapmanager/annotations/baseAnnotationsCore.py @@ -113,7 +113,7 @@ def getValue(self, colName : str, rowIdx : int): def getValues(self, colName : List[str], rowIdx : Union[int, List[int], None] = None, - ) -> Union[np.ndarray, None]: + ) -> Optional[np.ndarray]: """Get value(s) from a column or list of columns. Parameters @@ -131,17 +131,17 @@ def getValues(self, # logger.info(f'{rowIdx} {type(rowIdx)}') df = self.getDataFrame() # geopandas.geodataframe.GeoDataFrame - - # TODO: 042024 implement a list of columns - # if not isinstance(colName, list): - # colName = [colName] - #if not self.columns.columnIsValid(colName): + # print('df:') + # print(df.index) + if colName not in list(df.columns): logger.error(f'did not find column name "{colName}"') return if rowIdx is None: + # TODO: this won't work, need to get actual row labels + # some may be missing after delet rowIdx = range(self.numAnnotations) # get all rows elif not isinstance(rowIdx, list): rowIdx = [rowIdx] @@ -151,7 +151,7 @@ def getValues(self, return ret except (KeyError): - logger.error(f'bad rowIdx(s) {rowIdx}, colName:{colName} range is 0...{len(self)-1}') + logger.error(f'bad rowIdx(s) {rowIdx}, colName:{colName} values are in row labels') return None def moveSpine(self, spineID :int, x, y, z): @@ -359,6 +359,16 @@ def editSpine(self, editSpineProperty : EditSpinePropertyEvent): class LineAnnotationsCore(AnnotationsCore): + @property + def numSegments(self): + return len(self._fullMap.segments[:]) + + def newSegment(self): + return self._fullMap.newSegment() + + def appendSegmentPoint(self, segmentID : int, x : int, y: int, z : int): + self._fullMap.appendSegmentPoint(segmentID, x, y, z) + def getSummaryDf(self): """DataFrame with per segment info (one segment per ro) """ @@ -370,6 +380,11 @@ def _buildSummaryDf(self) -> pd.DataFrame: self._summaryDf = pd.DataFrame() self._summaryDf['segmentID'] = self._df['segmentID'].unique() + lengthList = [] + for row in range(len(self._summaryDf)): + lengthList.append(self._fullMap.segments['segment'].loc[row].length) + self._summaryDf['length'] = lengthList + def _buildDataFrame(self): _startSec = time.time() @@ -411,7 +426,7 @@ def _buildDataFrame(self): # # left/right - logger.warning('left/right is slow, can we get this pre-built and saved into zarr') + # logger.warning('left/right is slow, can we get this pre-built and saved into zarr') # TODO: 6/19 fix xyLeft and xyRight # TODO: put ths back in, the backend changed. We no longer have "segmentLeft" or "segmentRight" diff --git a/pymapmanager/interface2/appDisplayOptions.py b/pymapmanager/interface2/appDisplayOptions.py index aa119164..e176a364 100644 --- a/pymapmanager/interface2/appDisplayOptions.py +++ b/pymapmanager/interface2/appDisplayOptions.py @@ -45,6 +45,7 @@ def _getDefaultDisplayOptions(self): theDict['windowState']['height'] = 500 # position on screen # TODO: pass into imageplotwidget + theDict['windowState']['doSlidingZ'] = False # added 20240706 theDict['windowState']['zPlusMinus'] = 3 # interface.pointPlotWidget @@ -93,5 +94,4 @@ def _getDefaultDisplayOptions(self): # abj: 6/20 theDict['lineDisplay']['radius'] = 3 - return theDict diff --git a/pymapmanager/interface2/core/scatter_plot_widget.py b/pymapmanager/interface2/core/scatter_plot_widget.py index 65e7e265..d70aa441 100644 --- a/pymapmanager/interface2/core/scatter_plot_widget.py +++ b/pymapmanager/interface2/core/scatter_plot_widget.py @@ -524,7 +524,9 @@ def __init__(self, """ super().__init__(parent=parent) - self._blockSlots : bool = False + + # abb not used + # self._blockSlots : bool = False self.dict = {"X Stat" : "", "Y Stat" : "", diff --git a/pymapmanager/interface2/core/search_widget.py b/pymapmanager/interface2/core/search_widget.py index e94299d2..cc53c232 100644 --- a/pymapmanager/interface2/core/search_widget.py +++ b/pymapmanager/interface2/core/search_widget.py @@ -1,5 +1,5 @@ -import sys from typing import List, Optional +from contextlib import contextmanager import pandas as pd @@ -335,6 +335,7 @@ def __init__(self, df : pd.DataFrame = None, name : str = None): self.currentColName = "" self.currentSearchStr = "" + # used by _blockSlotsManager() self._blockSignalSelectionChanged = False # List to hold all column names within DF @@ -500,7 +501,7 @@ def _setDataFrame(self, newDf): def _mySetModel(self): - logger.info(f'{self.getMyName()}') + # logger.info(f'{self.getMyName()}') if self.df is not None: # abb removed @@ -553,12 +554,11 @@ def getSelectedRows(self): # # logger.info(f'indexes: {indexes} ') #data {item.data()} # return indexes - def keyPressEvent(self, event : QtGui.QKeyEvent): - logger.info('THIS keyPressEvent DOES NOTHING') - super().keyPressEvent(event) + # def keyPressEvent(self, event : QtGui.QKeyEvent): + # super().keyPressEvent(event) - # abb on_selectionChanged is not using its params - self.on_selectionChanged(None) + # # abb on_selectionChanged is not using its params + # # self.on_selectionChanged(None) def on_selectionChanged(self, item): """Respond to user selection. @@ -575,7 +575,7 @@ def on_selectionChanged(self, item): logger.info(f'{self.getMyName()}') if self._blockSignalSelectionChanged: - logger.info(f' _blockSignalSelectionChanged -->> return') + # logger.info(f' _blockSignalSelectionChanged -->> return') return # PyQt5.QtCore.Qt.KeyboardModifiers @@ -678,7 +678,19 @@ def _old_update_data(self): # # self.modelReset() # self.model.endResetModel() - + @contextmanager + def _blockSlotsManager(self): + try: + self._blockSignalSelectionChanged = True + yield self._blockSignalSelectionChanged + except Exception as e: + logger.error(e) + logger.error('setting block slot to False') + self._blockSignalSelectionChanged = False + raise e + finally: + self._blockSignalSelectionChanged = False + def _selectRow(self, rowList): """Programatically select rows of model via mySelectionModel @@ -689,38 +701,38 @@ def _selectRow(self, rowList): logger.info(f'{self.getMyName()} rowList:{rowList}') - # abb was here - # self._blockSignalSelectionChanged = True - - # Remove previously selected rows - super().clearSelection() - if rowList is None or len(rowList)==0: return - # abb moved here - self._blockSignalSelectionChanged = True + # here we will use a context manager to block slots + # if we get a runtime error within the 'with' + # the conext manager will set blockSlots to false + with self._blockSlotsManager(): + # self._blockSignalSelectionChanged = True - # logger.info(f'rowList:{rowList}') - - # 2nd argument is column, here we default to zero since we will select the entire row regardless - for _idx, rowIdx in enumerate(rowList): - modelIndex = self.model.index(rowIdx, 0) + # Remove previously selected rows + super().clearSelection() + + # test context manager exception + # logger.info('testing raise ValueError') + # raise ValueError - # QModelIndex - proxyIndex = self.proxyModel.mapFromSource(modelIndex) + # 2nd argument is column + # here we default to zero since we will select the entire row regardless + for _idx, rowIdx in enumerate(rowList): + modelIndex = self.model.index(rowIdx, 0) + + # QModelIndex + proxyIndex = self.proxyModel.mapFromSource(modelIndex) - mode = QtCore.QItemSelectionModel.Select | QtCore.QItemSelectionModel.Rows - # self.selectionMode().select(proxyIndex, mode) - self.mySelectionModel.select(proxyIndex, mode) + mode = QtCore.QItemSelectionModel.Select | QtCore.QItemSelectionModel.Rows + self.mySelectionModel.select(proxyIndex, mode) - if _idx == 0: - # logger.info(f"{self.getMyName()} scrollTo proxyIndex {proxyIndex.row()} modelIndex:{modelIndex.row()}") - # logger.info(f' {type(proxyIndex)} {type(modelIndex)}') - # self.scrollTo(proxyIndex, QtWidgets.QAbstractItemView.PositionAtTop) - self.scrollTo(proxyIndex) + # scroll the list to the first selection + if _idx == 0: + self.scrollTo(proxyIndex) - self._blockSignalSelectionChanged = False + # self._blockSignalSelectionChanged = False # abb 042024 to match expected api in annotationListWidget2 def mySelectRows(self, rowIdx): diff --git a/pymapmanager/interface2/mainMenus.py b/pymapmanager/interface2/mainMenus.py index da5f84be..c47ab447 100644 --- a/pymapmanager/interface2/mainMenus.py +++ b/pymapmanager/interface2/mainMenus.py @@ -63,19 +63,8 @@ def _buildMenus(self, mainMenu, mainWindow): # # edit - editMenu = mainMenu.addMenu("&Edit") - - undoAction = QtWidgets.QAction("Undo", self.getApp()) - undoAction.setCheckable(False) # setChecked is True by default? - undoAction.setShortcut("Ctrl+Z") - undoAction.triggered.connect(self.getApp()._undo_action) - editMenu.addAction(undoAction) - - redoAction = QtWidgets.QAction("Redo", self.getApp()) - redoAction.setCheckable(False) # setChecked is True by default? - redoAction.setShortcut("Shift+Ctrl+Z") - redoAction.triggered.connect(self.getApp()._redo_action) - editMenu.addAction(redoAction) + self.editMenu = mainMenu.addMenu("&Edit") + self.editMenu.aboutToShow.connect(self._refreshEditMenu) # # view, used by stack windows @@ -299,6 +288,49 @@ def _onHelpMenuAction(self, name): def _onAboutMenuAction(self, name): logger.info(name) + def _refreshEditMenu(self): + """Manage undo/redo menus. + """ + + self.editMenu.clear() + + enableUndo = True + enableRedo = True + + from pymapmanager.interface2.stackWidgets import stackWidget2 + frontWindow = self.getApp().getFrontWindow() + if isinstance(frontWindow, stackWidget2): + nextUndo = frontWindow.getUndoRedo().nextUndoStr() + nextRedo = frontWindow.getUndoRedo().nextRedoStr() + enableUndo = frontWindow.getUndoRedo().numUndo() > 0 + enableRedo = frontWindow.getUndoRedo().numRedo() > 0 + else: + nextUndo = '' + nextRedo = '' + + # TODO: we want the undo and redo menu report (as str) what will be undone and redone + # like ('add spine', 'delete spine', 'move spine', etc) + # this requires using 'aboutToShow' and asking the app for the front window + # if front window is stack, then get the str !!!! for undo and redo + undoAction = QtWidgets.QAction("Undo " + nextUndo, self.getApp()) + undoAction.setCheckable(False) # setChecked is True by default? + undoAction.setShortcut("Ctrl+Z") + undoAction.setEnabled(enableUndo) + undoAction.triggered.connect(self.getApp()._undo_action) + self.editMenu.addAction(undoAction) + + # turning off redo because 'redo delete' causes gui errors + # once we go to update the gui, spine does not exist and spine line stays in gui + # e.g. is stale + redoAction = QtWidgets.QAction("Redo " + nextRedo, self.getApp()) + redoAction.setCheckable(False) # setChecked is True by default? + redoAction.setShortcut("Shift+Ctrl+Z") + logger.warning('manually turning off redo') + enableRedo = False + redoAction.setEnabled(enableRedo) + redoAction.triggered.connect(self.getApp()._redo_action) + self.editMenu.addAction(redoAction) + def _refreshSettingsMenu(self): logger.info('') diff --git a/pymapmanager/interface2/pyMapManagerApp2.py b/pymapmanager/interface2/pyMapManagerApp2.py index 834af10c..377b5d1e 100644 --- a/pymapmanager/interface2/pyMapManagerApp2.py +++ b/pymapmanager/interface2/pyMapManagerApp2.py @@ -83,6 +83,10 @@ def loadPlugins(verbose=False, pluginType='stack') -> dict: # logger.info(e) continue + # don't add widgets with no specific name + if _widgetName == 'not assigned': + continue + # _showInMenu = obj.showInMenu # showInMenu is a static bool onePluginDict = { "pluginClass": moduleName, @@ -104,7 +108,7 @@ def loadPlugins(verbose=False, pluginType='stack') -> dict: pluginDict = dict(sorted(pluginDict.items())) # print the loaded plugins - logger.info(f'app loadPlugins loaded {len(pluginDict.keys())} plugins:') + logger.info(f'loaded {len(pluginDict.keys())} stack widget plugins:') if verbose: for k,v in pluginDict.items(): logger.info(f' {k}') @@ -367,7 +371,7 @@ def loadStackWidget(self, path : str): self._stackWidgetDict[path].show() else: # load stack and make widget - logger.info(f'loading stack widget from path: {path}') + # logger.info(f'loading stack widget from path: {path}') # _stackWidget = pmm.interface2.stackWidgets.stackWidget2(path) _stackWidget = stackWidget2(path) diff --git a/pymapmanager/interface2/runInterfaceBob.py b/pymapmanager/interface2/runInterfaceBob.py index 5a15b64a..7286343d 100644 --- a/pymapmanager/interface2/runInterfaceBob.py +++ b/pymapmanager/interface2/runInterfaceBob.py @@ -31,9 +31,11 @@ def run(): # from trySegment, works! # path = '/Users/cudmore/Desktop/trySeg.mmap' + # was working before switch to DirectoryStore path = mapmanagercore.data.getSingleTimepointMap() - logger.info(f'app.loadStackWidgeth: {path}') + # path = '/Users/cudmore/Sites/MapManagerCore/data/rr30a_s0u_v2.mmap' + sw2 = app.loadStackWidget(path) # works diff --git a/pymapmanager/interface2/stackWidgets/annotationListWidget2.py b/pymapmanager/interface2/stackWidgets/annotationListWidget2.py index d4a8194b..7b004222 100644 --- a/pymapmanager/interface2/stackWidgets/annotationListWidget2.py +++ b/pymapmanager/interface2/stackWidgets/annotationListWidget2.py @@ -195,7 +195,7 @@ class pointListWidget(annotationListWidget): def __init__(self, stackWidget : stackWidget2): annotations = stackWidget.getStack().getPointAnnotations() - logger.info(annotations) + # logger.info(annotations) super().__init__(stackWidget, annotations, name='pointListWidget') diff --git a/pymapmanager/interface2/stackWidgets/annotationPlotWidget2.py b/pymapmanager/interface2/stackWidgets/annotationPlotWidget2.py index 7ac1b9a3..129ffa2d 100644 --- a/pymapmanager/interface2/stackWidgets/annotationPlotWidget2.py +++ b/pymapmanager/interface2/stackWidgets/annotationPlotWidget2.py @@ -54,7 +54,7 @@ def updateLabel(self, labelID): # adjust + or - depending on angle spineAngles = self._df.getDataFrame()["spineAngle"] idSpineAngle = spineAngles[labelID] - logger.info(f"idSpineAngle {idSpineAngle}") + # logger.info(f"idSpineAngle {idSpineAngle}") # self._labels[labelID].setPos(QtCore.QPointF(x - 9, y - 9)) adjustConstant = 2 @@ -64,10 +64,10 @@ def updateLabel(self, labelID): adjustY = abs(adjustY) if 0 <= abs(idSpineAngle) and abs(idSpineAngle) <= 90: - logger.info(f"labelID: {labelID}, newX: {x + adjustX}, newY: {y + adjustY}") + # logger.info(f"labelID: {labelID}, newX: {x + adjustX}, newY: {y + adjustY}") self._labels[labelID].setPos(QtCore.QPointF(x + adjustX, y + adjustY)) elif 90 <= abs(idSpineAngle) and abs(idSpineAngle) <= 180: - logger.info(f"labelID: {labelID}, newX: {x + adjustX}, newY: {y + adjustY}") + # logger.info(f"labelID: {labelID}, newX: {x + adjustX}, newY: {y + adjustY}") self._labels[labelID].setPos(QtCore.QPointF(x - adjustX, y + adjustY)) elif 180 <= idSpineAngle and idSpineAngle <= 270: self._labels[labelID].setPos(QtCore.QPointF(x - adjustX, y - adjustY)) @@ -78,9 +78,17 @@ def updateLabel(self, labelID): acceptColumn = self._df.getDataFrame()["accept"] # logger.info(f"acceptColumn {acceptColumn}") # logger.info(f"labelID {labelID} acceptVal {acceptColumn[labelID]}") + _font=QtGui.QFont() + _font.setBold(True) if not acceptColumn[labelID]: - logger.info("Changing label color") + # logger.info("Changing label color -> not accept") self._labels[labelID].setColor(QtGui.QColor(255, 255, 255, 120)) + _font.setItalic(True) + self._labels[labelID].setFont(_font) + else: + # logger.info("Changing label color -> accept") + self._labels[labelID].setColor(QtGui.QColor(200, 200, 200, 255)) + self._labels[labelID].setFont(_font) def addedLabel(self, labelID): """Add a new label. @@ -96,7 +104,10 @@ def addedLabel(self, labelID): # add to dict self._labels[labelID] = newLabel - + + # abb, this will update based on (accept, usertype) + self.updateLabel(labelID) + def deleteLabel(self, labelID): """Delete a label. @@ -140,7 +151,7 @@ def _newLabel(self, labelID, x, y) -> pg.LabelItem: """Make a new label. """ # label = pg.LabelItem("", **{"color": "#FFF", "size": "6pt", "anchor": (1,1)}) - label = pg.TextItem('', anchor=(0.5,0.5)) + label = pg.TextItem('', anchor=(0.5,0.5)) # border=pg.mkPen(width=5) # label = QtWidgets.QLabel('labelID', self._plotItem) # label.setPos(QtCore.QPointF(x - 9, y - 9)) @@ -331,7 +342,9 @@ def _buildUI(self): self._scatterUserSelection.setZValue( zorder ) # put it on top, may need to change '10' - self._view.addItem(self._scatterUserSelection) + + # using 'self._view.plot', item is already added + # self._view.addItem(self._scatterUserSelection) # self._scatterUserSelection.sigPointsClicked.connect(self._on_highlighted_mouse_click) @@ -461,7 +474,7 @@ def _on_mouse_click(self, points, event): self._annotations, SpineAnnotationsCore, ): - logger.error('annotation plot widget emitting selection!!!!!!!!') + logger.info('annotation plot widget emitting selection!!!!!!!!') event.getStackSelection().setPointSelection(dbIdx) event.setAlt(isAlt) self.emitEvent(event, blockSlots=False) @@ -882,21 +895,13 @@ def moveAnnotationEvent(self, event : MoveSpineEvent): logger.info(event) for spine in event: - # self._addAnnotation(spineID) - # self._selectAnnotation([spineID]) - # update label spineID = spine['spineID'] - # x = spine['x'] - # y = spine['y'] - # z = spine['z'] - # self._labels[spineID].setPos(QtCore.QPointF(x - 9, y - 9)) self._pointLabels.updateLabel(spineID) # remake all spine lines self._bMakeSpineLines() - self._refreshSlice() def addedEvent(self, event : AddSpineEvent): @@ -911,9 +916,8 @@ def addedEvent(self, event : AddSpineEvent): def editedEvent(self, event: pmmEvent): - # self._refreshSlice() - - #abj: 6/25 + logger.warning(event) + for spine in event: # update label spineID = spine['spineID'] @@ -930,6 +934,9 @@ def deletedEvent(self, event: pmmEvent): logger.info(event) logger.warning(f'todo: delete label from _pointLabels') + self._selectAnnotation([]) + # self._cancelSpineRoiSelection() + self._refreshSlice() # for spineID in event.getSpines(): @@ -1146,8 +1153,14 @@ def _bMakeSpineLines(self): connect: Values of 1 indicate that the respective point will be connected to the next """ + + # logger.warning('make sure this is not called more than once') + anchorDf = self._annotations.getSpineLines() + # print('anchorDf:') + # print(anchorDf) + # logger.info(f'anchorDf:{type(anchorDf["x"])}') # print(anchorDf) @@ -1285,7 +1298,8 @@ def _buildUI(self): ) # put it on top, may need to change '10' # logger.info(f'adding _rightRadiusLines to view: {self.__class__.__name__}') - self._view.addItem(self._rightRadiusLines) + # 'using "self._view.plot", item is already added + # self._view.addItem(self._rightRadiusLines) def toggleRadiusLines(self): visible = not self._leftRadiusLines.isVisible() @@ -1295,10 +1309,8 @@ def toggleRadiusLines(self): self._rightRadiusLines.setVisible(visible) def _getScatterColor(self): - logger.info(f'"{self.getClassName()}"') - - # TODO: refactor this to not explicitly loop - + # logger.info(f'"{self.getClassName()}"') + dfPlot = self._dfPlot # abb having trouble with pandas setting a slice on a copy @@ -1307,28 +1319,13 @@ def _getScatterColor(self): # dfPlot.loc[:,'color'] = ['b'] * len(dfPlot) dfPlot['color'] = 'b' - # logger.info('') - # print(type(dfPlot)) - # print(dfPlot) - _stackSelection = self.getStackWidget().getStackSelection() _segmentSelection = _stackSelection.getSegmentSelection() - logger.info(f'{self.getClassName()} _segmentSelection:{_segmentSelection}') - - # logger.info('dfPlot Before') - # print(dfPlot) - if _segmentSelection is not None and len(_segmentSelection) > 0: - _tmp = dfPlot.loc[ dfPlot.index.isin(_segmentSelection) ] - - # print(' _tmp:', _tmp) - + _tmp = dfPlot.loc[ dfPlot.index.isin(_segmentSelection) ] dfPlot.loc[_tmp.index, 'color'] = 'y' - # logger.info('dfPlot AFTER') - # print(dfPlot) - return dfPlot['color'].tolist() # abj diff --git a/pymapmanager/interface2/stackWidgets/event/spineEvent.py b/pymapmanager/interface2/stackWidgets/event/spineEvent.py index 5de1c89e..5bc0db3f 100644 --- a/pymapmanager/interface2/stackWidgets/event/spineEvent.py +++ b/pymapmanager/interface2/stackWidgets/event/spineEvent.py @@ -49,6 +49,11 @@ def __init__(self, eventType : pmmEventType, mmWidget : mmWidget2): # list of dict with keys in (spineID, col, value) self._list = [] + def getName(self): + """Derived classes define this and is used in undo/redo menus. + """ + return '' + def getSpines(self) -> List[int]: """Get list of spine id in the event. """ @@ -157,6 +162,9 @@ def __init__(self, # self.addAddSpine(segmentID, x, y, z) self.addAddSpine(x, y, z) + def getName(self): + return 'Add Spine' + def addAddSpine(self, x, y, z): self.addEdit(x=x, y=y, z=z) @@ -173,13 +181,15 @@ def _getItem(self, item : SpineEdit): ) return item -class UndoSpineEvent(_EditSpine): - def __init__(self, mmWidget): +class _old_UndoSpineEvent(_EditSpine): + def __init__(self, mmWidget, event : pmmEvent): super().__init__(pmmEventType.undoSpineEvent, mmWidget) - -class RedoSpineEvent(_EditSpine): - def __init__(self, mmWidget): + self._undoEvent = event + +class _old_RedoSpineEvent(_EditSpine): + def __init__(self, mmWidget, event): super().__init__(pmmEventType.redoSpineEvent, mmWidget) + self._redoEvent = event class ManualConnectSpineEvent(_EditSpine): """Manual connect spine event. @@ -200,6 +210,9 @@ def __init__(self, self.addEdit(spineID=spineID, x=x, y=y, z=z) + def getName(self): + return 'Manual Connect Spine' + def _getItem(self, item : SpineEdit) -> SpineEdit: """Get the meaningful keys for this edit type. """ @@ -212,7 +225,7 @@ def _getItem(self, item : SpineEdit) -> SpineEdit: z=item['z'] ) return item - + class MoveSpineEvent(_EditSpine): """Add spine event. @@ -240,6 +253,9 @@ def __init__(self, self.addEdit(spineID=spineID, x=x, y=y, z=z) + def getName(self): + return 'Move Spine' + def _getItem(self, item : SpineEdit) -> SpineEdit: """Get the meaningful keys for this edit type. """ @@ -270,6 +286,9 @@ def __init__(self, self.addDeleteSpine(spineID) + def getName(self): + return 'Delete Spine' + def addDeleteSpine(self, spineID : int): self.addEdit(spineID=spineID) @@ -278,6 +297,24 @@ def _getItem(self, item : SpineEdit): """ item = SpineEdit(spineID=item['spineID']) return item + +class EditedSpineEvent(_EditSpine): + """Used to undo and redo a spine change including: + - move + - move anchor + - edit property + """ + def __init__(self, + mmWidget : mmWidget2, + spineID : int = None): + super().__init__(pmmEventType.refreshSpineEvent, mmWidget) + self.addEdit(spineID=spineID) + + def _getItem(self, item : SpineEdit): + """Get the meaningful keys for this edit type. + """ + item = SpineEdit(spineID=item['spineID']) + return item class EditSpinePropertyEvent(_EditSpine): """A list of spine edits to set values. @@ -296,6 +333,9 @@ def __init__(self, if spineID is not None: self.addEditProperty(spineID, col, value) + def getName(self): + return 'Edit Spine' + def addEditProperty(self, spineID, col, diff --git a/pymapmanager/interface2/stackWidgets/histogramWidget2.py b/pymapmanager/interface2/stackWidgets/histogramWidget2.py index 31f9198e..195a1c17 100644 --- a/pymapmanager/interface2/stackWidgets/histogramWidget2.py +++ b/pymapmanager/interface2/stackWidgets/histogramWidget2.py @@ -5,13 +5,9 @@ from qtpy import QtGui, QtCore, QtWidgets import pyqtgraph as pg -from .mmWidget2 import mmWidget2, pmmEventType, pmmEvent, pmmStates -# from mmWidget2 import mmWidget2 -# from mmWidget2 import pmmEventType, pmmEvent, pmmStates - +from .mmWidget2 import mmWidget2, pmmEvent from pymapmanager._logger import logger -#class bHistogramWidget(QtWidgets.QToolBar): class HistogramWidget(mmWidget2): signalContrastChange = QtCore.Signal(object) # (contrast dict) @@ -26,9 +22,13 @@ def __init__(self, stackWidget): self._myStack = stackWidget._stack self._contrastDict = stackWidget._contrastDict + # logger.info('') + # print(self._contrastDict) + self._sliceNumber = 0 self._channel = 2 - self._maxValue = 2**self._myStack.header['bitDepth'] # will default to 8 if not found + # self._maxValue = 2**self._myStack.header['bitDepth'] # will default to 8 if not found + self._maxValue = 2**self._contrastDict[1]['displayBitDepth'] self._sliceImage = None # set by self.plotLogHist = True @@ -58,16 +58,23 @@ def setSliceEvent(self, event: pmmEvent): sliceNumber = event.getSliceNumber() self._setSlice(sliceNumber) + def setColorChannelEvent(self, event): + colorChannel = event.getColorChannel() + # logger.info(f'{self.getClassName()} colorChannel:{colorChannel}') + self.slot_setChannel(colorChannel) + def slot_setChannel(self, channel : int): """Show/hide channel buttons. """ # logger.info(f'bHistogramWidget channel:{channel}') + self._channel = channel if channel in [1,2,3]: for histWidget in self.histWidgetList: if histWidget._channel == channel: histWidget.show() + histWidget._refreshSlice() else: histWidget.hide() elif channel == 'rgb': @@ -88,6 +95,8 @@ def slot_contrastChanged(self, contrastDict): """ self.signalContrastChange.emit(contrastDict) + self.getStackWidget().slot_contrastChanged(contrastDict) + def _checkbox_callback(self, isChecked): sender = self.sender() title = sender.text() @@ -117,7 +126,7 @@ def _checkbox_callback(self, isChecked): def bitDepth_Callback(self, idx): newMaxValue = self._myBitDepths[idx] - logger.info(f' newMaxValue: {newMaxValue}') + # logger.info(f' newMaxValue: {newMaxValue}') self._maxValue = newMaxValue for histWidget in self.histWidgetList: @@ -133,22 +142,18 @@ def bitDepth_Callback(self, idx): ''' def _buildUI(self): - minVal = 0 - maxVal = self._maxValue - # as a toolbar #_tmpWidget = QtWidgets.QWidget() vBoxLayout = QtWidgets.QVBoxLayout() # main layout - #self.myGridLayout = QtWidgets.QGridLayout(self) self._makeCentralWidget(vBoxLayout) - spinBoxWidth = 64 + # spinBoxWidth = 64 # starts off as min/max intensity in stack - _minContrast = self._contrastDict[self._channel]['minContrast'] - _maxContrast = self._contrastDict[self._channel]['maxContrast'] + # _minContrast = self._contrastDict[self._channel]['minContrast'] + # _maxContrast = self._contrastDict[self._channel]['maxContrast'] # log checkbox self.logCheckbox = QtWidgets.QCheckBox('Log') @@ -158,7 +163,12 @@ def _buildUI(self): # bit depth # don't include 32, it causes an over-run self._myBitDepths = [2**x for x in range(1,17)] + # find in list bitDepthIdx = self._myBitDepths.index(self._maxValue) # will sometimes fail + # logger.info(f'_myBitDepths:{self._myBitDepths}') + # logger.info(f'_maxValue:{self._maxValue}') + # logger.info(f'bitDepthIdx:{bitDepthIdx}') + bitDepthLabel = QtWidgets.QLabel('Bit Depth') bitDepthComboBox = QtWidgets.QComboBox() #bitDepthComboBox.setMaximumWidth(spinBoxWidth) @@ -167,6 +177,9 @@ def _buildUI(self): bitDepthComboBox.setCurrentIndex(bitDepthIdx) bitDepthComboBox.currentIndexChanged.connect(self.bitDepth_Callback) + autoContrastButton = QtWidgets.QPushButton('Auto') + autoContrastButton.clicked.connect(self._onAutoContrast) + _alignLeft = QtCore.Qt.AlignLeft # TODO: add 'histogram' checkbox to toggle histograms @@ -174,20 +187,11 @@ def _buildUI(self): hBoxLayout.addWidget(self.logCheckbox, alignment=_alignLeft) hBoxLayout.addWidget(bitDepthLabel, alignment=_alignLeft) hBoxLayout.addWidget(bitDepthComboBox, alignment=_alignLeft) + hBoxLayout.addWidget(autoContrastButton, alignment=_alignLeft) hBoxLayout.addStretch() vBoxLayout.addLayout(hBoxLayout) - ''' - row = 0 - col = 0 - self.myGridLayout.addWidget(self.logCheckbox, row, col) - col += 1 - self.myGridLayout.addWidget(bitDepthLabel, row, col) - col += 1 - self.myGridLayout.addWidget(bitDepthComboBox, row, col) - ''' - hBoxLayout2 = QtWidgets.QHBoxLayout() # main layout # for channel in numChannel @@ -227,6 +231,25 @@ def _buildUI(self): #colorComboBox.setEnabled(False) ''' + def _onAutoContrast(self): + # theMin, theMax = self.getStack().getAutoContrast(self._channel) # channel is 1 based, e.g. (1,2,3,...) + + minAutoContrast = self._contrastDict[self._channel]['minAutoContrast'] + maxAutoContrast = self._contrastDict[self._channel]['maxAutoContrast'] + + # logger.info(f'channe:{self._channel} min:{minAutoContrast} max:{maxAutoContrast}') + + self._contrastDict[self._channel]['minContrast'] = minAutoContrast + self._contrastDict[self._channel]['maxContrast'] = maxAutoContrast + + _oneChannelContrast = self._contrastDict[self._channel] + + self.getStackWidget().slot_contrastChanged(_oneChannelContrast) + + # cludge, need to properly connect signal/slot + channelIdx = self._channel - 1 + self.histWidgetList[channelIdx]._refreshContrast() + #class _histogram(QtWidgets.QToolBar): class _histogram(QtWidgets.QWidget): """A histogram for one channel. @@ -237,12 +260,20 @@ class _histogram(QtWidgets.QWidget): def __init__(self, myStack, contrastDict, channel) -> None: super().__init__() + + # logger.info(f'contrastDict:{contrastDict}') + self._myStack = myStack self._contrastDict = contrastDict self._sliceNumber = 0 self._channel = channel - self._maxValue = 2**self._myStack.header['bitDepth'] # will default to 8 if not found + + # assuming multichannel images have same bit depth for all channels + # # TODO: pull this from ch 1 of contrast dict + # self._maxValue = 2**self._myStack.header['bitDepth'] # will default to 8 if not found + self._maxValue = 2 ** contrastDict[1]['displayBitDepth'] + self._sliceImage = None # set by self._plotLogHist = True @@ -307,7 +338,7 @@ def _setSlice(self, sliceNumber, doInit=False): if self._plotLogHist: y = np.log10(y, where=y>0) - logger.info(f'self._sliceImage:{self._sliceImage.shape} x:{len(x)} y:{len(y)}') + # logger.info(f'self._sliceImage:{self._sliceImage.shape} x:{len(x)} y:{len(y)}') # abb windows # Exception: X and Y arrays must be the same shape--got (256,) and (255,). @@ -335,12 +366,12 @@ def _setSlice(self, sliceNumber, doInit=False): _imageMin = np.min(self._sliceImage) _imageMax = np.max(self._sliceImage) - _imageMedian = np.median(self._sliceImage) + # _imageMedian = np.median(self._sliceImage) self.pgPlotWidget.setXRange(_imageMin, self._maxValue, padding=0) self.minIntensityLabel.setText(f'min:{_imageMin}') self.maxIntensityLabel.setText(f'max:{_imageMax}') - self.medianIntensityLabel.setText(f'med:{_imageMedian}') + # self.medianIntensityLabel.setText(f'med:{_imageMedian}') self.update() @@ -378,6 +409,15 @@ def setBitDepth(self, maxValue): if self.maxContrastSlider.value() > maxValue: self.maxContrastSlider.setValue(maxValue) + # update spin box + self.minSpinBox.setMaximum(maxValue) + self.maxSpinBox.setMaximum(maxValue) + + if self.minSpinBox.value() > maxValue: + self.minSpinBox.setValue(maxValue) + if self.maxSpinBox.value() > maxValue: + self.maxSpinBox.setValue(maxValue) + # _imageMin = np.min(self._sliceImage) # self.pgPlotWidget.setXRange(_imageMin, self._maxValue, padding=0) @@ -386,6 +426,19 @@ def setBitDepth(self, maxValue): # update histogram self._refreshSlice() + def _refreshContrast(self): + minContrast = self._contrastDict[self._channel]['minContrast'] + maxContrast = self._contrastDict[self._channel]['maxContrast'] + + self.minSpinBox.setValue(minContrast) + self.maxSpinBox.setValue(maxContrast) + + self.minContrastSlider.setValue(minContrast) + self.maxContrastSlider.setValue(maxContrast) + + self.minContrastLine.setValue(minContrast) + self.maxContrastLine.setValue(maxContrast) + def _buildUI(self): minVal = 0 maxVal = self._maxValue @@ -488,13 +541,13 @@ def _buildUI(self): _specialCol = 0 self.minIntensityLabel = QtWidgets.QLabel('min:') self.maxIntensityLabel = QtWidgets.QLabel('max:') - self.medianIntensityLabel = QtWidgets.QLabel('median:') + # self.medianIntensityLabel = QtWidgets.QLabel('median:') _specialRow = row + 1 self.myGridLayout.addWidget(self.minIntensityLabel, _specialRow, _specialCol) _specialRow += 1 self.myGridLayout.addWidget(self.maxIntensityLabel, _specialRow, _specialCol) _specialRow += 1 - self.myGridLayout.addWidget(self.medianIntensityLabel, _specialRow, _specialCol) + # self.myGridLayout.addWidget(self.medianIntensityLabel, _specialRow, _specialCol) row += 1 specialCol = 1 # to skip column with spin boxes diff --git a/pymapmanager/interface2/stackWidgets/imagePlotWidget2.py b/pymapmanager/interface2/stackWidgets/imagePlotWidget2.py index d0803c93..8627377e 100644 --- a/pymapmanager/interface2/stackWidgets/imagePlotWidget2.py +++ b/pymapmanager/interface2/stackWidgets/imagePlotWidget2.py @@ -1,6 +1,4 @@ import enum -# import math -# from functools import partial import numpy as np import pyqtgraph as pg @@ -21,22 +19,22 @@ from pymapmanager._logger import logger -class stackWidgetState(enum.Enum): - """ - Enum to encapsulate one widget state +# class stackWidgetState(enum.Enum): +# """ +# Enum to encapsulate one widget state - baseState: - The base/default state. - movePointState: - The user is moving a point (spine for now) - Next mouse click will specify new position (z,y,x) - connectSpineState: - The user is selecting a manual connection point (on the line) - Next mouse click (on the line) will specify the connectionIdx - """ - baseState = "stateBase" - movePointState = "movePointState" - connectSpineState = "connectSpineState" +# baseState: +# The base/default state. +# movePointState: +# The user is moving a point (spine for now) +# Next mouse click will specify new position (z,y,x) +# connectSpineState: +# The user is selecting a manual connection point (on the line) +# Next mouse click (on the line) will specify the connectionIdx +# """ +# baseState = "stateBase" +# movePointState = "movePointState" +# connectSpineState = "connectSpineState" class ImagePlotWidget(mmWidget2): """A plot widget (pg.PlotWidget) to plot @@ -82,13 +80,14 @@ def __init__(self, stackWidget): self._displayThisChannel = _channel # 1->0, 2->1, 3->2, etc - self._doSlidingZ = False + # self._doSlidingZ = False + # a dictionary of contrast, one key per channel #self._setDefaultContrastDict() # assigns _contrastDict self._sliceImage = None - self._state = stackWidgetState.baseState + # self._state = stackWidgetState.baseState # not used # self._mouseMovedState = False @@ -104,7 +103,7 @@ def __init__(self, stackWidget): #self._setChannel(1) # removed feb 25 2023 # 20220824, playing with this ... does not work. - self.autoContrast() + # self.autoContrast() # self.refreshSlice() @@ -516,48 +515,17 @@ def slot_slider_setSlice(self, sliceNumber): return self.slot_setSlice(sliceNumber=sliceNumber) + def setSliceEvent(self, event): + sliceNumber = event.getSliceNumber() + logger.info(f'sliceNumber:{sliceNumber}') + self._setSlice(sliceNumber, doEmit=False) + def slot_setSlice(self, sliceNumber, doEmit=True): - # logger.warning(f'sliceNumber:{sliceNumber} doEmit:{doEmit}') + logger.warning(f'sliceNumber:{sliceNumber} doEmit:{doEmit}') if self.slotsBlocked(): return self._setSlice(sliceNumber, doEmit=doEmit) - def _old_slot_selectAnnotation2(self, selectionEvent : "pymapmanager.annotations.SelectionEvent"): - if self._blockSlots: - return - self._selectAnnotation(selectionEvent) - - def _old__selectAnnotation(self, selectionEvent : "pymapmanager.annotations.SelectionEvent"): - self._blockSlots = True - - self.signalAnnotationSelection2.emit(selectionEvent) - - if selectionEvent.isAlt: - #if selectionEvent.type == pymapmanager.annotations.pointAnnotations: - if selectionEvent.isPointSelection(): - #print('!!! SET SLICE AND ZOOM') - rowIdx = selectionEvent.getRows() - if len(rowIdx) > 0: - rowIdx = rowIdx[0] - x = selectionEvent.annotationObject.getValue('x', rowIdx) - y = selectionEvent.annotationObject.getValue('y', rowIdx) - z = selectionEvent.annotationObject.getValue('z', rowIdx) - #logger.info(f' calling _zoomToPoint with x:{x} and y:{y}') - self._zoomToPoint(x, y) - #logger.info(f' calling _setSlice with z:{z}') - self._setSlice(z) - - self._blockSlots = False - - def _old_slot_deletedAnnotation(self, delDict : dict): - """On delete, cancel spine selection. - """ - _pointSelectionEvent = pymapmanager.annotations.SelectionEvent(self._aPointPlot._annotations, - rowIdx=[], - isAlt=False, - stack=self._myStack) - self.signalAnnotationSelection2.emit(_pointSelectionEvent) - def slot_setContrast(self, contrastDict): #logger.info(f'contrastDict:') #pprint(contrastDict) @@ -570,7 +538,7 @@ def slot_setChannel(self, channel): logger.info(f'channel:{channel}') self._setChannel(channel, doEmit=False) - def slot_setSlidingZ(self, d): + def _old_slot_setSlidingZ(self, d): """ Args: d: dictionary of (checked, upDownSlices) @@ -583,12 +551,12 @@ def slot_setSlidingZ(self, d): self._doSlidingZ = checked # self._upDownSlices = upDownSlices - self._displayOptionsDict['windowState']['zPlusMinus'] = upDownSlices - self._displayOptionsDict['pointDisplay']['zPlusMinus'] = upDownSlices - self._displayOptionsDict['lineDisplay']['zPlusMinus'] = upDownSlices + # self._displayOptionsDict['windowState']['zPlusMinus'] = upDownSlices + # self._displayOptionsDict['pointDisplay']['zPlusMinus'] = upDownSlices + # self._displayOptionsDict['lineDisplay']['zPlusMinus'] = upDownSlices # print("self._displayOptionsDict['windowState']['zPlusMinus']", self._displayOptionsDict['windowState']['zPlusMinus']) - self.refreshSlice() + # self.refreshSlice() def _setChannel(self, channel, doEmit=True): """ @@ -654,33 +622,6 @@ def _setContrast(self): levelList = levelList[0] self._myImage.setLevels(levelList, update=True) - def autoContrast(self): - """20220824, playing with this ... does not work. - """ - logger.error('TURNED OFF ON SWITCH TO CORE') - return - - _percent_low = 30.0 #0.5 # .30 - _percent_high = 99.95 #100 - 0.5 - - # logger.warning(f'THIS IS EXPERIMENTAL _percent_low:{_percent_low} _percent_high:{_percent_high}') - - data = self._myStack.getImageChannel(channel=self._displayThisChannel) - percentiles = np.percentile(data, (_percent_low, _percent_high)) - - # logger.info(f' percentiles:{percentiles}') - - theMin = percentiles[0] - theMax = percentiles[1] - - theMin = int(theMin) - theMax = int(theMax) - - self._contrastDict[self._displayThisChannel]['minContrast'] = theMin - self._contrastDict[self._displayThisChannel]['maxContrast'] = theMax - - return - def refreshSlice(self): self._setSlice(self._currentSlice, doEmit=False) @@ -692,9 +633,7 @@ def _setSlice(self, sliceNumber : int, doEmit = True): TODO: get rid of doEmit, use _blockSlots """ - - # logger.info(f'sliceNumber:{sliceNumber} doEmit:{doEmit} ===================================================') - + if isinstance(sliceNumber, float): sliceNumber = int(sliceNumber) @@ -702,17 +641,18 @@ def _setSlice(self, sliceNumber : int, doEmit = True): # order matters # channel = self._displayThisChannel - channelIdx = self._displayThisChannel - 1 + # channelIdx = self._displayThisChannel - 1 + + _doSlidingZ = self._displayOptionsDict['windowState']['doSlidingZ'] + # logger.info(f'_doSlidingZ:{_doSlidingZ}') if self._channelIsRGB(): ch1_image = self._myStack.getImageSlice(imageSlice=sliceNumber, channel=1) ch2_image = self._myStack.getImageSlice(imageSlice=sliceNumber, channel=2) - # print('1) ch1_image:', ch1_image.shape, ch1_image.dtype) - # rgb requires 8-bit images ch1_image = ch1_image/ch1_image.max() * 2**8 - ch2_image = ch2_image/ch1_image.max() * 2**8 + ch2_image = ch2_image/ch2_image.max() * 2**8 ch1_image = ch1_image.astype(np.uint8) ch2_image = ch2_image.astype(np.uint8) @@ -723,54 +663,33 @@ def _setSlice(self, sliceNumber : int, doEmit = True): sliceImage[:,:,1] = ch1_image # green sliceImage[:,:,0] = ch2_image # red sliceImage[:,:,2] = ch2_image # blue - elif self._doSlidingZ: - # upDownSlices = self._upDownSlices + + elif _doSlidingZ: upDownSlices = self._displayOptionsDict['windowState']['zPlusMinus'] - logger.warning('re-implement with core') - # sliceImage = self._myStack.getMaxProjectSlice(sliceNumber, - # channel, - # upDownSlices, upDownSlices, - # func=np.max) + # logger.warning(f're-implement with core upDownSlices:{upDownSlices}') + sliceImage = self._myStack.getMaxProjectSlice(sliceNumber, + self._displayThisChannel, + upDownSlices, upDownSlices, + func=np.max) else: # one channel + channelIdx = self._displayThisChannel - 1 sliceImage = self._myStack.getImageSlice(imageSlice=sliceNumber, channel=channelIdx) - # myStack.createBrightestIndexes(sliceImage, channel) - autoLevels = True levels = None - # Setting current slice to be used in _buildUI - # self._sliceImage = sliceImage - # print("sliceimage is:", sliceImage) self._myImage.setImage(sliceImage, levels=levels, autoLevels=autoLevels) self._sliceImage = sliceImage - # myStack.createBrightestIndexes(sliceImage) - - # print("test sliceimage is:", self._sliceImage) - # set color self._setColorLut() - # update contrast self._setContrast() - # self.update() # update pyqtgraph interface - - # emit - #logger.info(f' -->> emit signalUpdateSlice() _currentSlice:{self._currentSlice}') - - # self._stackSlider.setValue(self._currentSlice) self._sliderBlocked = True self._stackSlider._updateSlice(self._currentSlice, doEmit=False) self._sliderBlocked = False - # return - # removed aug 31 if doEmit: - # # self._blockSlots = True - # # self.signalUpdateSlice.emit(self._currentSlice) - # # self._blockSlots = False - # without this, point and line plots do not update??? _pmmEvent = pmmEvent(pmmEventType.setSlice, self) _pmmEvent.setSliceNumber(self._currentSlice) @@ -962,7 +881,7 @@ def _buildUI(self): # self.setCentralWidget(centralWidget) def selectedEvent(self, event : pmmEvent): - """Snap and optionally zoom to point and line annotations. + """Snap set slice and optionally zoom to point and line annotations. Notes ----- @@ -985,8 +904,9 @@ def selectedEvent(self, event : pmmEvent): y = _pointAnnotations.getValue('y', oneItem) z = _pointAnnotations.getValue('z', oneItem) - logger.info(f"spine: zoom to coordinates x:{x} y:{y} z:{z}") - self._zoomToPoint(x, y) + if event.isAlt(): + logger.info(f"spine: zoom to coordinates x:{x} y:{y}") + self._zoomToPoint(x, y) self._currentSlice = z doEmit = True @@ -997,17 +917,14 @@ def selectedEvent(self, event : pmmEvent): _lineAnnotations = self.getStackWidget().getStack().getLineAnnotations() x, y, z = _lineAnnotations.getMedianZ(oneSegmentID) - logger.info(f"segment: zoom to coordinates x:{x} y:{y} z:{z}") - self._zoomToPoint(x, y) + if event.isAlt(): + logger.info(f"segment: zoom to coordinates x:{x} y:{y}") + self._zoomToPoint(x, y) self._currentSlice = z doEmit = True self._setSlice(z, doEmit=doEmit) - def setSliceEvent(self, event): - # logger.info(event) - pass - def setColorChannelEvent(self, event : pmmEvent): # logger.info(event) colorChannel = event.getColorChannel() diff --git a/pymapmanager/interface2/stackWidgets/mmWidget2.py b/pymapmanager/interface2/stackWidgets/mmWidget2.py index f696ec09..72d41ab0 100644 --- a/pymapmanager/interface2/stackWidgets/mmWidget2.py +++ b/pymapmanager/interface2/stackWidgets/mmWidget2.py @@ -42,6 +42,9 @@ class pmmEventType(Enum): # acceptPoint = auto() # abj, used for setting isBad boolean # changeUserType = auto() + # added to refresh gui after modifying the core with undo and redo + refreshSpineEvent = auto() + undoSpineEvent = auto() redoSpineEvent = auto() @@ -374,7 +377,7 @@ def getSender(self) -> "mmWidget2": """ return self._dict['senderName'] - def getSenderObject(self): + def getSenderObject(self) -> mmWidget2: return self._sender @property @@ -729,6 +732,14 @@ def slot_pmmEvent(self, event : pmmEvent): elif event.type == pmmEventType.edit: acceptEvent = self.editedEvent(event) + elif event.type == pmmEventType.refreshSpineEvent: + # no backend (stackWidget) action + # event to update gui after core change (undo and redo) + if not self._iAmStackWidget: + acceptEvent = self.editedEvent(event) + else: + acceptEvent = True + elif event.type == pmmEventType.stateChange: acceptEvent = self.stateChangedEvent(event) elif event.type == pmmEventType.moveAnnotation: diff --git a/pymapmanager/interface2/stackWidgets/stackWidget2.py b/pymapmanager/interface2/stackWidgets/stackWidget2.py index 83225a1a..4c3c4ce1 100644 --- a/pymapmanager/interface2/stackWidgets/stackWidget2.py +++ b/pymapmanager/interface2/stackWidgets/stackWidget2.py @@ -3,6 +3,10 @@ # see: https://stackoverflow.com/questions/39740632/python-type-hinting-without-cyclic-imports from __future__ import annotations from typing import TYPE_CHECKING + +import pymapmanager.interface2 +import pymapmanager.interface2.stackWidgets +import pymapmanager.interface2.stackWidgets.histogramWidget2 if TYPE_CHECKING: from pymapmanager.interface2 import PyMapManagerApp, AppDisplayOptions @@ -24,7 +28,7 @@ # from .tracingWidget import tracingWidget # from .histogramWidget2 import HistogramWidget # from .searchWidget2 import SearchWidget2 -from pymapmanager.interface2.stackWidgets.event.spineEvent import AddSpineEvent, DeleteSpineEvent, UndoSpineEvent +from pymapmanager.interface2.stackWidgets.event.spineEvent import AddSpineEvent, DeleteSpineEvent #, UndoSpineEvent from pymapmanager._logger import logger @@ -60,7 +64,8 @@ def __init__(self, if stack is not None: self._stack = stack else: - logger.info(f'loading stack from path: {path}') + logger.info('loading stack from path:') + logger.info(f'{path}') self._stack = pymapmanager.stack(path) # add 2/24 when implementing map/timeseries GUI @@ -88,11 +93,14 @@ def __init__(self, # self._currentSelection.setImageChannel(_channel) """Keep track of the current selection""" + from pymapmanager.interface2.stackWidgets.event.undoRedo import UndoRedoEvent + self._undoRedo = UndoRedoEvent(self) + self.setWindowTitle(path) self._buildUI() self._buildMenus() - + def getDisplayOptions(self) -> AppDisplayOptions: return self._displayOptionsDict @@ -337,32 +345,42 @@ def _buildUI(self): self.addToolBar(QtCore.Qt.TopToolBarArea, self._topToolbar) self._widgetDict[topToobarName] = self._topToolbar + # adding bottom contrast widget + # vBoxMainLayout = QtWidgets.QVBoxLayout() + # self._makeCentralWidget(vBoxMainLayout) + # main h box to hold left control panel and image plot hBoxLayout_main = QtWidgets.QHBoxLayout() self._makeCentralWidget(hBoxLayout_main) + # vBoxMainLayout.addLayout(hBoxLayout_main) + # left v-layout for point and line lists vLayout = QtWidgets.QVBoxLayout() hBoxLayout_main.addLayout(vLayout) # - pointListName = pointListWidget._widgetName + # pointListName = pointListWidget._widgetName plw = pointListWidget(self) + pointListName = plw._widgetName pointListDock = self._addDockWidget(plw, 'left', 'Points') self._widgetDict[pointListName] = pointListDock # the dock, not the widget ??? # - lineListName = lineListWidget._widgetName + # lineListName = lineListWidget._widgetName llw = lineListWidget(self) + lineListName = llw._widgetName lineListDock = self._addDockWidget(llw, 'left', 'Lines') self._widgetDict[lineListName] = lineListDock # the dock, not the widget ??? # - imagePlotName = ImagePlotWidget._widgetName + # imagePlotName = ImagePlotWidget._widgetName _imagePlotWidget = ImagePlotWidget(self) + imagePlotName = _imagePlotWidget._widgetName hBoxLayout_main.addWidget(_imagePlotWidget) self._widgetDict[imagePlotName] = _imagePlotWidget # the dock, not the widget ??? + self._imagePlotName = imagePlotName # # status toolbar (bottom) numSlices = self._stack.numSlices @@ -376,6 +394,12 @@ def _buildUI(self): self._topToolbar.signalChannelChange.connect(self.slot_setChannel) + _histWidget = pymapmanager.interface2.stackWidgets.histogramWidget2.HistogramWidget(self) + _histWidgetName = _histWidget._widgetName + _histDock = self._addDockWidget(_histWidget, 'bottom', 'Histogram') + self._widgetDict[_histWidgetName] = _histWidget # the dock, not the widget ??? + self._widgetDict[_histWidgetName].hide() # hide histogram by default + # # plugin panel with tabs self.pluginDock1 = stackPluginDock(self) @@ -387,20 +411,22 @@ def updateDisplayOptionsZ(self, d): top tool bar + Arguments: d = { 'checked': checked, 'upDownSlices': upDownSlices, } """ + self._displayOptionsDict['windowState']['doSlidingZ'] = d['checked'] self._displayOptionsDict['windowState']['zPlusMinus'] = d["upDownSlices"] - temp = self._displayOptionsDict["windowState"]['zPlusMinus'] - logger.info(f"self._displayOptionsDict['windowState']['zPlusMinus'] {temp}") + self._displayOptionsDict['pointDisplay']['zPlusMinus'] = d["upDownSlices"] self._displayOptionsDict['lineDisplay']['zPlusMinus'] = d["upDownSlices"] # Call to refresh other widgets # Simply use slice change + # self._widgetDict[self._imagePlotName].slot_setSlidingZ(d) _pmmEvent = pmmEvent(pmmEventType.setSlice, self) _pmmEvent.setSliceNumber(self._currentSliceNumber) @@ -440,7 +466,7 @@ def selectedEvent(self, event : "pmmEvent"): _stackSelection = self.getStackSelection() _eventSelection = event.getStackSelection() - _state = self.getStackSelection().getState() + _state = _stackSelection.getState() # logger.info(f'state is: {_state}') @@ -478,6 +504,7 @@ def selectedEvent(self, event : "pmmEvent"): _onePoint = _pointSelection[0] segmentIndex = self.getStack().getPointAnnotations().getValue("segmentID", _onePoint) segmentIndex= [int(segmentIndex)] + self.getStackSelection().setSegmentSelection(segmentIndex) # _eventSelection.setSegmentSelection(segmentIndex) @@ -486,8 +513,6 @@ def selectedEvent(self, event : "pmmEvent"): else: self.getStackSelection().setPointSelection([]) - - if not _eventSelection.hasPointSelection(): if _eventSelection.hasSegmentSelection(): _segmentSelection = _eventSelection.getSegmentSelection() self.getStackSelection().setSegmentSelection(_segmentSelection) @@ -506,8 +531,6 @@ def selectedEvent(self, event : "pmmEvent"): def addedEvent(self, event : AddSpineEvent) -> bool: """Add to backend. - Currently only allows adding a spine annotation. - Returns ------- True if added, False otherwise @@ -522,7 +545,6 @@ def addedEvent(self, event : AddSpineEvent) -> bool: return False segmentID = _stackSelection.firstSegmentSelection() - # segmentID = _stackSelection.getSegmentSelection() for _rowIdx, item in enumerate(event): logger.info(item) @@ -538,17 +560,7 @@ def addedEvent(self, event : AddSpineEvent) -> bool: event._list[_rowIdx]['spineID'] = newSpineID event._list[_rowIdx]['segmentID'] = segmentID - return True - - x, y, z = event.getAddMovePosition() - newRow = self.getStack().getPointAnnotations().addSpine(x, y, z, segmentID, self._stack) - logger.info(f' newRow:{newRow}') - - # make our new item selected - _stackSelection.setPointSelection([newRow]) - - # so children know whichi index to update - event.getStackSelection().setPointSelection([newRow]) + self.getUndoRedo().addUndo(event) return True @@ -570,6 +582,8 @@ def editedEvent(self, event : pmmEvent) -> bool: logger.info(event) self.getStack().getPointAnnotations().editSpine(event) + self.getUndoRedo().addUndo(event) + def deletedEvent(self, event : DeleteSpineEvent) -> bool: """Delete items from backend. @@ -584,6 +598,8 @@ def deletedEvent(self, event : DeleteSpineEvent) -> bool: for spineID in spineIDs: self.getStack().getPointAnnotations().deleteAnnotation(spineID) + self.getUndoRedo().addUndo(event) + # # delete segment # _segmentSelection = _selection.getSegmentSelection() # if _segmentSelection is not None: @@ -696,30 +712,14 @@ def _afterEdit(self, event): self.slot_setStatus('Ready') def moveAnnotationEvent(self, event : "pmmEvent"): - - # items = event.getSpines() # [int] - # if len(items) == 0: - # return logger.info('=== === STACK WIDGET PERFORMING Move === ===') for item in event: logger.info(f'item:{item}') - # _eventSelection = event.getStackSelection() - - # logger.info(f'event:{event} _eventSelection:{_eventSelection}') - - # if not _eventSelection.hasPointSelection(): - # logger.warning('only works for single item selection') - # return - - # itemList = _eventSelection.getPointSelection() - # item = itemList[0] - # x, y, z = event.getAddMovePosition() spineID = item['spineID'] - # if len(spineID) > 0: - # spineID = spineID[0] + x = item['x'] y = item['y'] z = item['z'] @@ -727,30 +727,9 @@ def moveAnnotationEvent(self, event : "pmmEvent"): logger.info(f' spineID:{spineID} x:{x} y:{y} z:{z}') _pointAnnotation = self.getStack().getPointAnnotations() _pointAnnotation.moveSpine(spineID=spineID, x=x, y=y, z=z) - # _pointAnnotation.setValue('x', spineID, x) - # _pointAnnotation.setValue('y', spineID, y) - # _pointAnnotation.setValue('z', spineID, z) - # force recalculation of brightest index - #_pointAnnotation.setValue('brightestIndex', item, np.nan) + self.getUndoRedo().addUndo(event) - # la = self.getStack().getLineAnnotations() - # channelNumber = 1 - # _imageSlice = z - # imgSliceData = self._stack.getImageSlice(_imageSlice, channelNumber) - - # abb 202404, done by core - # _pointAnnotation.updateSpineInt2( - # item, - # self._stack) - - # - - # sliceNum = event.getSliceNumber() - # logger.info(f"moveAnnotationEvent sliceNum {sliceNum}") - - # logger.error('put call to _afterEdit back in') - self._afterEdit2(event) def manualConnectSpineEvent(self, event : pmmEvent): @@ -770,50 +749,9 @@ def manualConnectSpineEvent(self, event : pmmEvent): _pointAnnotation = self.getStack().getPointAnnotations() _pointAnnotation.manualConnectSpine(spineID=spineID, x=x, y=y, z=z) - self._afterEdit2(event) - - # OLD - # # _stackSelection = self.getStackSelection() - # _stackSelection = event.getStackSelection() - - # manuallyConnectSpine = _stackSelection.getManualConnectSpine() - # if manuallyConnectSpine is None or manuallyConnectSpine == []: - # errStr = 'Did not get spine selection - can not make manual connection' - # logger.error(f'{errStr} manuallyConnectSpine:{manuallyConnectSpine}') - # self.slot_setStatus(errStr) - # logger.error(f'_stackSelection: {_stackSelection}') - # return - - # # user selected brightest index - # if not _stackSelection.hasSegmentPointSelection(): - # logger.error('got bad brightestIndex') - # return - - # brightestIndex = _stackSelection.getSegmentPointSelection() - - # logger.info('=== === STACK WIDGET PERFORMING MANUAL CONNECT === ===') - # logger.info(f' manuallyConnectSpine:{manuallyConnectSpine} to brightestIndex:{brightestIndex}') - - # # set backend - # _pointAnnotation = self.getStack().getPointAnnotations() - # _pointAnnotation.setValue('brightestIndex', manuallyConnectSpine, brightestIndex) - # _pointAnnotation.updateSpineInt2(manuallyConnectSpine, self.getStack()) - - # # - # # need to transform event into a spine selection (it is currently a line selection) - # eventType = pmmEventType.selection - # newEvent = pmmEvent(eventType, self) - # newEvent.getStackSelection().setPointSelection(manuallyConnectSpine) - # sliceNum = event.getSliceNumber() - # newEvent.setSliceNumber(sliceNum) - - # logger.info(f'manualConnectSpineEvent Slice number emit {sliceNum} ') - # # self.emitEvent(event, blockSlots=False) - - # # Removed 3/6 since it causes looping of calls - # # self.slot_pmmEvent(newEvent) + self.getUndoRedo().addUndo(event) - # self._afterEdit(newEvent) + self._afterEdit2(event) def autoConnectSpineEvent(self, event): """Auto connect the currently selected spine. @@ -919,35 +857,45 @@ def _buildColorLut(self): self._colorLutDict['blue'] = lut self._colorLutDict['b'] = lut + def slot_contrastChanged(self, contrastDict): + self._widgetDict[self._imagePlotName].slot_setContrast(contrastDict) + def _setDefaultContrastDict(self): """Remember contrast setting and color LUT for each channel. """ - logger.info(f'num channels is: {self._stack.numChannels}') + # logger.info(f'num channels is: {self._stack.numChannels}') self._contrastDict = {} for channelIdx in range(self._stack.numChannels): channelNumber = channelIdx + 1 - logger.warning('removed on merge core 20240513') + _defaultDisplayBitDepth = 11 + + # logger.warning('removed on merge core 20240513') # _stackData = self._stack.getImageChannel(channel=channelNumber) - minStackIntensity = 0 # np.min(_stackData) - maxStackIntensity = 2048 # np.max(_stackData) - if minStackIntensity is None: - minStackIntensity = 0 - if maxStackIntensity is None: - maxStackIntensity = 255 - - logger.warning('need to fix this when there is no image data') - logger.info(f' channel {channelIdx} minStackIntensity:{minStackIntensity} maxStackIntensity:{maxStackIntensity}') + # minStackIntensity = 0 # np.min(_stackData) + # maxStackIntensity = 2**_defaultDisplayBitDepth # 2048 # np.max(_stackData) + + # if minStackIntensity is None: + # minStackIntensity = 0 + # if maxStackIntensity is None: + # maxStackIntensity = 255 + + # logger.warning('need to fix this when there is no image data') + # logger.info(f' channel {channelIdx} minStackIntensity:{minStackIntensity} maxStackIntensity:{maxStackIntensity}') + + # expensive, get once + minAutoContrast, maxAutoContrast = self._stack.getAutoContrast(channel=channelIdx) self._contrastDict[channelNumber] = { 'channel': channelNumber, 'colorLUT': self._channelColor[channelIdx], - 'minContrast': minStackIntensity, # set by user - 'maxContrast': maxStackIntensity, # set by user - #'minStackIntensity': minStackIntensity, # to set histogram/contrast slider guess - #'maxStackIntensity': maxStackIntensity, - 'bitDepth': self._stack.header['bitDepth'] + 'minContrast': minAutoContrast, # set by user + 'maxContrast': maxAutoContrast, # set by user + 'minAutoContrast': minAutoContrast, + 'maxAutoContrast': maxAutoContrast, + # 'bitDepth': self._stack.header['bitDepth'] + 'displayBitDepth': _defaultDisplayBitDepth } def zoomToPointAnnotation(self, @@ -985,7 +933,7 @@ def zoomToPointAnnotation(self, sliceNum = self.getStack().getPointAnnotations().getValue("z", idx) event.setSliceNumber(sliceNum) - event.setAlt(True) + event.setAlt(isAlt) self.slot_pmmEvent(event) #self.emitEvent(event, blockSlots=False) @@ -1041,6 +989,15 @@ def _old_acceptPoint(self, event): self._afterEdit(newEvent) + def getUndoRedo(self): + return self._undoRedo + + # abb 20240701, logic here is flawed. We might not need a class for undo/redo + # e.g. UndoSpineEvent and RedoSpineEvent + # might be sufficient to write code that emits event to properly update the GUI + # the core has taken care of actual undo + # for example, to undo an add spine + def _undo_action(self): # logger.info('') #self.getStack().undo() @@ -1048,8 +1005,10 @@ def _undo_action(self): self.getStack().undo() - undoSpineEvent = UndoSpineEvent(self) - self.emitEvent(undoSpineEvent) + self.getUndoRedo().doUndo() + + # undoSpineEvent = UndoSpineEvent(self, undoEvent) + # self.emitEvent(undoSpineEvent) def _redo_action(self): # logger.info('') @@ -1058,6 +1017,8 @@ def _redo_action(self): self.getStack().redo() - from pymapmanager.interface2.stackWidgets.event.spineEvent import RedoSpineEvent - redoSpineEvent = RedoSpineEvent(self) - self.emitEvent(redoSpineEvent) + redoEvent = self.getUndoRedo().doRedo() + + # from pymapmanager.interface2.stackWidgets.event.spineEvent import RedoSpineEvent + # redoSpineEvent = RedoSpineEvent(self, redoEvent) + # self.emitEvent(redoSpineEvent) diff --git a/pymapmanager/stack.py b/pymapmanager/stack.py index 68abe084..c54978d7 100644 --- a/pymapmanager/stack.py +++ b/pymapmanager/stack.py @@ -38,7 +38,7 @@ def __init__(self, path : str, # load the map _startSec = time.time() - logger.info(f'loading core map from zar:{self._zarrPath}') + # logger.info(f'loading core map from zar:{self._zarrPath}') self._fullMap : MapAnnotations = MapAnnotations.load(self._zarrPath) ## .cached()) # self._fullMap : MapAnnotations = MapAnnotations(MMapLoader(self._zarrPath).cached()) # self._fullMap : MapAnnotations = MapAnnotations(MMapLoader(self._zarrPath)) @@ -74,11 +74,16 @@ def _buildSessionMap(self): return self._sessionMap def __str__(self): - x = self.header['x'] - y = self.header['y'] + x = self.header['xPixels'] + y = self.header['yPixels'] dtype = self.header['dtype'] - str = f'{self.getFileName()} channels:{self.numChannels} slices:{self.numSlices} x:{x} y:{y} dtype:{dtype}' + numAnnotations = self.getPointAnnotations().numAnnotations + numSegments = self.getLineAnnotations().numSegments + + str = f'{self.getFileName()}\n' + str += f' channels:{self.numChannels} slices:{self.numSlices} x:{x} y:{y} dtype:{dtype}' + str += f' annotations:{numAnnotations} segments:{numSegments}' return str def _buildHeader(self): @@ -115,7 +120,8 @@ def _buildHeader(self): 'yPixels' : y, } - self.printHeader() + # self.printHeader() + logger.warning('TODO: hard coded some parts of the header') def printHeader(self): for k, v in self.header.items(): @@ -198,6 +204,11 @@ def _old_getImageChannel(self, logger.warning(f'Max channel is {self.numChannels}, got channelIdx:{channelIdx}') return None + def getAutoContrast(self, channel): + channelIdx = channel - 1 + _min, _max = self.sessionMap.getAutoContrast_qt(channel=channelIdx) + return _min, _max + def loadImages(self, channel : int = None): """Load all images for one channel. """ @@ -256,58 +267,23 @@ def getImageSlice(self, """Get a single image slice from a channel. Args: - slice (int): Image slice. Zero based - channel (int): Channel number, one based + imageSlice (int): Image slice. Zero based + channel (int): Channel number. One based Returns: np.ndarray of image data, None if image is not loaded. """ - + channelIdx = channel - 1 if not isinstance(imageSlice, int): imageSlice = int(imageSlice) - # TODO: implement a global stack option to either - # - dynamically load from core - # - load all image data once - - # core - # zRange = (imageSlice, imageSlice+1) - # slices = self.sessionMap.slices(time=0, channel=channelIdx, zRange=zRange) - # _imgData = slices._image - - # logger.info(f'channel:{channel} imageSlice:{imageSlice} {type(imageSlice)}') - - #abj: changed channel to channelIdx _imgData = self.sessionMap.getPixels(channel=channelIdx, z=imageSlice) - # _imgData = self.sessionMap.getPixels(channel=channel, z=imageSlice) _imgData = _imgData._image - - # logger.info(f'_imgData: {type(_imgData)} {_imgData.shape}') return _imgData - # - # before properly using core - # _doInMemory = True - - # if _doInMemory: - # channelIdx = channel - 1 - # if self._images[channelIdx] is None: - # # image data not loaded - # logger.error(f'channel index {channelIdx} is None') - # return - # _imgData = self._images[channelIdx][imageSlice][:][:] - # else: - # # core - # _images = self.sessionMap.images - # _imgData = _images.fetchSlices2(self.sessionID, channelIdx, (imageSlice, imageSlice+1)) - # _imgData = _imgData[0,:,:] - # logger.info(f'_imgData: {_imgData.shape}') - - # return _imgData - def getMaxProjectSlice(self, imageSlice : int, channel : int = 1, @@ -341,7 +317,7 @@ def getMaxProjectSlice(self, zRange = (firstSlice, lastSlice) - slices = self.sessionMap.slices(time=0, channel=channelIdx, zRange=zRange) + slices = self.sessionMap.getPixels(channel=channelIdx, zRange=zRange) # logger.info(type(slices)) # logger.info(f'{slices._image.shape}') @@ -372,21 +348,24 @@ def undo(self): _beforeDf = self.getPointAnnotations().getDataFrame() - print('_beforeDf[115]') - print(_beforeDf.loc[115, ['x', 'y', 'z']]) + # print('_beforeDf[115]') + # print(_beforeDf.loc[115, ['x', 'y', 'z']]) _ret = self._fullMap.undo() - print(f'_ret:{_ret}') - self.getPointAnnotations()._buildDataFrame() + # print(f'_ret:{_ret}') - _afterDf = self.getPointAnnotations().getDataFrame() - print('_afterDf[115]') - print(_afterDf.loc[115, ['x', 'y', 'z']]) + self.getPointAnnotations()._buildDataFrame() + # _afterDf = self.getPointAnnotations().getDataFrame() + # print('_afterDf[115]') + # print(_afterDf.loc[115, ['x', 'y', 'z']]) def redo(self): logger.info('') _ret = self._fullMap.redo() - print(f'_ret:{_ret}') + # print(f'_ret:{_ret}') + + self.getPointAnnotations()._buildDataFrame() + \ No newline at end of file diff --git a/readme-dev.md b/readme-dev.md index 51e02d60..ee013269 100644 --- a/readme-dev.md +++ b/readme-dev.md @@ -1,6 +1,11 @@ -TODO -annotationListWidget should be given `stack`, not `stackWidget` +## Start working on multi timepoint + +1) Write function to make best guess of connected spines + - Need to incorporate a point along the line repesenting the same position on a line (between timepoints) + - Need to improve algorithm because it allows spine connections to criss cross + +2) Write function to force a spine ROI to have a given lnegth. The length of all connected spines need to be the same so they have the same number of pixels in the ROI. Such that the sum intensity will be normalized + -our base pmmWidget need to have a copy of current selection. That is why we passed the annotationListWidget the stackWidget (above) and will be removed diff --git a/tests/test_mmMap.py b/tests/test_mmMap.py index 305d9be3..4d3c6430 100644 --- a/tests/test_mmMap.py +++ b/tests/test_mmMap.py @@ -3,6 +3,8 @@ import pymapmanager as pmm def _test_mmMap_init(): + return + path = '/Users/cudmore/Sites/PyMapManager-Data/public/rr30a/rr30a.txt' _map = pmm.mmMap(path) From 7d765b037f984b41b1059718a72dbcbf24582b87 Mon Sep 17 00:00:00 2001 From: Robert Cudmore Date: Tue, 9 Jul 2024 13:21:56 -0400 Subject: [PATCH 26/26] push for j on tue 7-9 --- .../interface2/stackWidgets/event/undoRedo.py | 166 ++++++++++++++++++ tests/test_core_segments.py | 96 ++++++++++ 2 files changed, 262 insertions(+) create mode 100644 pymapmanager/interface2/stackWidgets/event/undoRedo.py create mode 100644 tests/test_core_segments.py diff --git a/pymapmanager/interface2/stackWidgets/event/undoRedo.py b/pymapmanager/interface2/stackWidgets/event/undoRedo.py new file mode 100644 index 00000000..80b625aa --- /dev/null +++ b/pymapmanager/interface2/stackWidgets/event/undoRedo.py @@ -0,0 +1,166 @@ +from typing import List, Union, Optional + +from pymapmanager.interface2.stackWidgets import stackWidget2 +from pymapmanager.interface2.stackWidgets.mmWidget2 import pmmEvent, pmmEventType +from pymapmanager.interface2.stackWidgets.event.spineEvent import (AddSpineEvent, + DeleteSpineEvent, + MoveSpineEvent, + ManualConnectSpineEvent, + EditSpinePropertyEvent, + EditedSpineEvent) +from pymapmanager._logger import logger + +class UndoRedoEvent: + """Undo and Redo spine events for a stack widget. + + Undo delete is not working, could be because core does not refresh spine lines on undo/redo? + Same might be true for apsine property 'Accept' + """ + def __init__(self, parentStackWidget : stackWidget2): + self._parentStackWidget = parentStackWidget + self._undoList = [] + self._redoList = [] + + def _getStackWidgetSlice(self) -> int: + """Used to emit set slice. + """ + return self._parentStackWidget.getCurrentSliceNumber() + + def addUndo(self, event : pmmEvent) -> None: + self._undoList.append(event) + + def _addRedo(self, event : pmmEvent) -> None: + self._redoList.append(event) + + def doUndo(self) -> Optional[pmmEvent]: + """Undo the last edit event. + """ + + if self.numUndo() == 0: + logger.info('nothing to undo') + return + + # the last undo event + undoEvent = self._undoList.pop(len(self._undoList)-1) + + # add to redo + self._addRedo(undoEvent) + + # do the undo of event + if isinstance(undoEvent, AddSpineEvent): + logger.info('TODO: undo add spine') + self._cancelSelection(undoEvent) + self._refreshSlice(undoEvent) + + elif isinstance(undoEvent, DeleteSpineEvent): + logger.info('TODO: undo delete spine') + self._reselectSpine(undoEvent) + + elif isinstance(undoEvent, (MoveSpineEvent, + ManualConnectSpineEvent, + EditSpinePropertyEvent)): + logger.info('TODO: undo modify spine') + self._emitEditedSpineEvent(undoEvent) + self._reselectSpine(undoEvent) + + else: + logger.warning('did not understand undo event') + logger.warning(undoEvent) + + return undoEvent + + def doRedo(self) -> Optional[pmmEvent]: + if self.numRedo() == 0: + logger.info('nothing to redo') + return + + # the last undo event + redoEvent = self._redoList.pop(len(self._redoList)-1) + + # add to undo + self.addUndo(redoEvent) + + if isinstance(redoEvent, AddSpineEvent): + logger.info('TODO: redo add spine') + self._reselectSpine(redoEvent) + + elif isinstance(redoEvent, DeleteSpineEvent): + # TODO: NOT WORKING + logger.info('TODO: redo delete spine') + self._cancelSelection(redoEvent) + # self._emitEditedSpineEvent(redoEvent) # -->> error + self._refreshSlice(redoEvent) + + elif isinstance(redoEvent, (MoveSpineEvent, + ManualConnectSpineEvent, + EditSpinePropertyEvent)): + logger.info('TODO: redo modify spine') + self._emitEditedSpineEvent(redoEvent) + self._reselectSpine(redoEvent) + + else: + logger.warning('did not understand redo event') + logger.warning(redoEvent) + + return redoEvent + + def nextUndoStr(self) -> str: + """Get a str rep for the next undo action. + """ + if self.numUndo() == 0: + return '' + else: + return self._undoList[self.numUndo()-1].getName() + + def nextRedoStr(self) -> str: + """Get a str rep for the next undo action. + """ + if self.numRedo() == 0: + return '' + else: + return self._redoList[self.numRedo()-1].getName() + + def numUndo(self) -> int: + return len(self._undoList) + + def numRedo(self) -> int: + return len(self._redoList) + + def _emitEditedSpineEvent(self, event): + theWidget = event.getSenderObject() + + # TODO: make a deep copy of event + spineID = event.getSpines() + + logger.info(f'*** spineID: {spineID}') + + # cludge + spineID = spineID[0] + + logger.info(f'*** spineID: {spineID}') + + ese = EditedSpineEvent(theWidget, spineID) + theWidget.emitEvent(ese) + + def _refreshSlice(self, event): + theWidget = event.getSenderObject() + setSliceEvent = pmmEvent(pmmEventType.setSlice, theWidget) + _sliceNumber = self._getStackWidgetSlice() + setSliceEvent.setSliceNumber(_sliceNumber) + theWidget.emitEvent(setSliceEvent) + + def _cancelSelection(self, event): + items = [] + theWidget = event.getSenderObject() + event = pmmEvent(pmmEventType.selection, theWidget) + event.getStackSelection().setPointSelection(items) + theWidget.emitEvent(event) + + def _reselectSpine(self, event): + items = event.getSpines() + + theWidget = event.getSenderObject() + event = pmmEvent(pmmEventType.selection, theWidget) + event.getStackSelection().setPointSelection(items) + theWidget.emitEvent(event) + diff --git a/tests/test_core_segments.py b/tests/test_core_segments.py new file mode 100644 index 00000000..b3fa7750 --- /dev/null +++ b/tests/test_core_segments.py @@ -0,0 +1,96 @@ +import pytest + +import pandas as pd +import geopandas as gpd + +from mapmanagercore import MapAnnotations, MultiImageLoader +from mapmanagercore.data import getTiffChannel_1, getLinesFile, getSingleTimepointMap + +from pymapmanager.annotations.baseAnnotationsCore import LineAnnotationsCore + +def _getEmptyMap(): + # add image channels to the loader + loader = MultiImageLoader() + loader.read(getTiffChannel_1(), channel=0) + + map = MapAnnotations(loader.build()) + tp = map.getTimePoint(0) + + return tp + +def test_segment(): + + # 1) load segments from and grab points + linesFile = getLinesFile() + df = pd.read_csv(linesFile) + + s = gpd.GeoSeries.from_wkt(df['segment']) + # print(type(s.loc[0])) # shapely.geometry.linestring.LineString + for row in range(len(s)): + print(f'length of loaded segment {row} is: {s.loc[row].length}') + + dfXyz = s.get_coordinates(include_z=True) + dfSegment = dfXyz[dfXyz.index==0] + + # 2) make an empty map + tp = _getEmptyMap() + + segmentID = tp.newSegment() + + n = len(dfSegment) + print('adding points n:', n) + for row in range(n): + x = int(dfSegment['x'].iloc[row]) + y = int(dfSegment['y'].iloc[row]) + z = int(dfSegment['z'].iloc[row]) + + _len0 = tp.appendSegmentPoint(segmentID, x, y, z) + + print('_len0:', _len0) + + tp.segments[:] + + print('tp.segments[:]') + print(tp.segments[:]) + + # s = gpd.GeoSeries.from_wkt(tp.segments[:]['segment']) + print('segment length:', tp.segments[:]['segment'].loc[0].length) + print('rough tracing length:', tp.segments[:]['roughTracing'].loc[0].length) + +def test_qt_segments(): + """Load zarr, test core segment + - add a segment + - add a point + - rebuild main df and summary df + """ + from pymapmanager import stack + + zarrPath = getSingleTimepointMap() + _stack = stack(zarrPath) + print(_stack) + + segments = _stack.getLineAnnotations() + + segmentID = segments.newSegment() + + x = 100 + y = 100 + z = 20 + _len0 = segments.appendSegmentPoint(segmentID, x, y, z) + + x += 20 + y += 20 + z += 5 + _len0 = segments.appendSegmentPoint(segmentID, x, y, z) + + # see if 1 point segment works + # new segment does not show up until it has 2x points + segments._buildDataFrame() + segments._buildSummaryDf() + + print(segments.getDataFrame()) + print(segments.getSummaryDf()) + +if __name__ == '__main__': + # test_segment() + test_qt_segments() \ No newline at end of file