From 5984dd09e9e109250978cfb779b4f695dfaa792b Mon Sep 17 00:00:00 2001 From: Johnson Le Date: Fri, 16 Aug 2024 17:44:13 -0700 Subject: [PATCH] Improved Save and Analysis parameters Analysis parameters is now stored in user/documents file on initial use. - This file is used when user loads from tif file - can be accessed from application to update/ save under File -> App Analysis Parameters --- pymapmanager/interface2/mainMenus.py | 15 + pymapmanager/interface2/openFirstWindow.py | 4 +- pymapmanager/interface2/pyMapManagerApp2.py | 68 +++- .../interface2/runInterfaceJohnson.py | 10 +- .../stackWidgets/analysisParamWidget2.py | 360 ++++++++++++++++++ .../interface2/stackWidgets/stackWidget2.py | 46 ++- pymapmanager/pmmUtils.py | 115 ++++++ pymapmanager/stack.py | 17 + pymapmanager/utils.py | 2 +- 9 files changed, 615 insertions(+), 22 deletions(-) create mode 100644 pymapmanager/interface2/stackWidgets/analysisParamWidget2.py diff --git a/pymapmanager/interface2/mainMenus.py b/pymapmanager/interface2/mainMenus.py index 64ed6f8f..4206b952 100644 --- a/pymapmanager/interface2/mainMenus.py +++ b/pymapmanager/interface2/mainMenus.py @@ -427,6 +427,10 @@ def _refreshFileMenu(self): self.fileMenu.addSeparator() # abj + enableUndo = False + enableRedo = False + isDirty = False + frontWindow = self.getApp().getFrontWindow() if isinstance(frontWindow, stackWidget2): enableUndo = frontWindow.getUndoRedo().numUndo() > 0 @@ -457,6 +461,12 @@ def _refreshFileMenu(self): self.settingsMenu = self.fileMenu.addMenu('User Options...') self.settingsMenu.aboutToShow.connect(self._refreshSettingsMenu) + self.fileMenu.addSeparator() + + #abj + analysisParametersAction = QtWidgets.QAction('App Analysis Parameters', self.getApp()) + analysisParametersAction.triggered.connect(self.getApp()._showAnalysisParameters) + self.fileMenu.addAction(analysisParametersAction) def _refreshOpenRecent(self): """Dynamically generate the open recent stack/map menu. @@ -487,3 +497,8 @@ def _refreshOpenRecent(self): ) self.openRecentMenu.addAction(loadFolderAction) + + def _refreshAnalysisParameters(self): + """ + """ + logger.info(f"refreshing analysis Parameters") \ No newline at end of file diff --git a/pymapmanager/interface2/openFirstWindow.py b/pymapmanager/interface2/openFirstWindow.py index 365aeec8..fc26924e 100644 --- a/pymapmanager/interface2/openFirstWindow.py +++ b/pymapmanager/interface2/openFirstWindow.py @@ -102,7 +102,9 @@ def _on_recent_stack_click(self, rowIdx : int): path = self.recentStackList[rowIdx] logger.info(f'rowId:{rowIdx} path:{path}') - if os.path.isfile(path): + # abj: Changed this to check if it is a directory instead of a file + # if os.path.isfile(path): + if os.path.isdir(path): self.getApp().loadStackWidget(path) else: logger.error(f'did not find path: {path}') diff --git a/pymapmanager/interface2/pyMapManagerApp2.py b/pymapmanager/interface2/pyMapManagerApp2.py index ef35d086..fbc181d1 100644 --- a/pymapmanager/interface2/pyMapManagerApp2.py +++ b/pymapmanager/interface2/pyMapManagerApp2.py @@ -1,3 +1,4 @@ +import json import os import sys import math @@ -11,6 +12,8 @@ import qdarktheme +from pymapmanager.interface2.stackWidgets.analysisParamWidget2 import AnalysisParamWidget + # Enable HiDPI. qdarktheme.enable_hi_dpi() @@ -28,7 +31,9 @@ from pymapmanager.interface2.mainMenus import PyMapManagerMenus from pymapmanager._logger import logger, setLogLevel +# from pymapmanager.pmmUtils import addUserPath, getBundledDir, getUserAnalysisParamJsonData, saveAnalysisParamJsonFile from pymapmanager.pmmUtils import getBundledDir +import pymapmanager.pmmUtils def loadPlugins(verbose=False, pluginType='stack') -> dict: """Load stack plugins from both: @@ -117,10 +122,18 @@ def loadPlugins(verbose=False, pluginType='stack') -> dict: return pluginDict + class PyMapManagerApp(QtWidgets.QApplication): def __init__(self, argv=[], deferFirstWindow=False): super().__init__(argv) - + + self._analysisParams = mapmanagercore.analysis_params.AnalysisParams() + + firstTimeRunning = self._initUserDocuments() + + if firstTimeRunning: + logger.info(" We created /Documents/Pymapmanager-User-Files and need to restart") + self._config = pymapmanager.interface2.Preferences(self) # util class to save/load app preferences including recent paths @@ -167,6 +180,35 @@ def __init__(self, argv=[], deferFirstWindow=False): self._openFirstWindow = None self.openFirstWindow() + def _initUserDocuments(self): + """ + """ + # platformdirs + jsonDump = self._analysisParams.getJson() + + # Create user's pmm directory in user/documents if necessary and save json to it + return pymapmanager.pmmUtils.addUserPath(jsonDump) + + def getAnalysisParams(self): + """ get analysis params from json file within user documents + """ + return self._analysisParams + + def getUserJsonData(self): + return pymapmanager.pmmUtils.getUserAnalysisParamJsonData() + + def saveAnalysisParams(self, dict): + """ + dict: analysis Parameters dictionary + """ + # aP = self.getAnalysisParams() + + # convert dictionary to json + analysisParamJson = json.dumps(dict) + + # save to json file in user documents + pymapmanager.pmmUtils.saveAnalysisParamJsonFile(analysisParamJson) + def getNewUntitledNumber(self) -> int: """Get a unique number for each new map (From tiff file). """ @@ -320,26 +362,28 @@ def saveFile(self): stackWidget.save() def saveAsFile(self): - """ Save change to a new file - - 2 Scenarios: - 1.) mmap file has not been created yet and we need to save a new one - 2.) There is already a mmap file and user wants to create a new one - - in this scenario, after creating a new one, save would still save on the old one, - until user loads in that new file + """ Save as a new file """ logger.info(f'Saving as file { self._stackWidgetDict.keys()}') - # scenario 1) Should be called when something is dragged and dropped, new file is loaded in - # this is handled in loadTifFile - # scenario 2) if len(self._stackWidgetDict) > 0: for key in self._stackWidgetDict.keys(): # looping through every path in stackWidgetDict # key = path of current stack stackWidget = self._stackWidgetDict[key] stackWidget.fileSaveAs() - # stackWidget.saveAs(key) + + #abj + def _showAnalysisParameters(self): + + if len(self._stackWidgetDict) > 0: + for key in self._stackWidgetDict.keys(): + # looping through every path in stackWidgetDict + # key = path of current stack + currentStackWidget = self._stackWidgetDict[key] + + self.apWidget = AnalysisParamWidget(stackWidget=currentStackWidget, pmmApp=self) + self.apWidget.show() def _undo_action(self): self.getFrontWindow().emitUndoEvent() diff --git a/pymapmanager/interface2/runInterfaceJohnson.py b/pymapmanager/interface2/runInterfaceJohnson.py index ecda4e91..79cf8a25 100644 --- a/pymapmanager/interface2/runInterfaceJohnson.py +++ b/pymapmanager/interface2/runInterfaceJohnson.py @@ -62,12 +62,12 @@ def run(): def run2(): app = PyMapManagerApp() - # path = '/Users/johns/Documents/GitHub/PyMapManager-Data/one-timepoint/rr30a_s0_ch1.tif' - path = '/Users/johns/Documents/GitHub/PyMapManager-Data/one-timepoint/rr30a_s0_ch1.mmap' - # sw2 = app.loadTifFile(path) + path = '/Users/johns/Documents/GitHub/PyMapManager-Data/one-timepoint/rr30a_s0_ch1.tif' + # path = '/Users/johns/Documents/GitHub/PyMapManager-Data/one-timepoint/rr30a_s0_ch1.mmap' + # # sw2 = app.loadTifFile(path) sw2 = app.loadStackWidget(path) sys.exit(app.exec_()) if __name__ == '__main__': - # run() - run2() \ No newline at end of file + run() + # run2() \ No newline at end of file diff --git a/pymapmanager/interface2/stackWidgets/analysisParamWidget2.py b/pymapmanager/interface2/stackWidgets/analysisParamWidget2.py new file mode 100644 index 00000000..4b24d67a --- /dev/null +++ b/pymapmanager/interface2/stackWidgets/analysisParamWidget2.py @@ -0,0 +1,360 @@ +import copy +from functools import partial +import json +import math +from pymapmanager._logger import logger +from qtpy import QtGui, QtCore, QtWidgets +# from pymapmanager import AnalysisParams +import pymapmanager +from pymapmanager.interface2.stackWidgets.mmWidget2 import mmWidget2, pmmEventType, pmmEvent, pmmStates +# from pymapmanager.interface2.stackWidgets.event.spineEvent import DeleteSpineEvent, EditSpinePropertyEvent +from pymapmanager.interface2.stackWidgets import stackWidget2 + +class AnalysisParamWidget(mmWidget2): + + _widgetName = 'Analysis Parameters' + + signalSaveParameters = QtCore.Signal(dict) # dict + signalReplotWidgetsWithNewParams = QtCore.Signal(dict) # dict + + def __init__(self, stackWidget: stackWidget2, pmmApp = None): + """ + paramDict: detectionParamDict class + + """ + super().__init__(None) + + self.pmmApp = pmmApp + self.widgetDict = {} + self.stackWidget = stackWidget + self.canApply = False + self.canSave = False + + if pmmApp is not None: + # if stackWidget.isTifPath(): + # load and save from app + + # this essentially uses same analysis Params as stackWidget? + self._analysisParameters = pmmApp.getAnalysisParams() + # self._analysisParameters = stackWidget.getAnalysisParams() + self.setWindowTitle('Analysis Parameters (Application)') + # Get analysis Parameters from app that is save within user/documents + _dict = pmmApp.getUserJsonData() + logger.info(f"app _dict: {_dict}") + + else: + # TODO: Disable if there is no current stack? + + self._analysisParameters = stackWidget.getAnalysisParams() + self.setWindowTitle('Analysis Parameters (Stack)') + # Get analysis params from MapManagerCore Backend + _dict = self._analysisParameters.getDict() + logger.info(f"mmc _dict: {_dict}") + + # logger.info(f"self._dict {self._dict}") + + # deep copy to not alter original dict until we apply + self._dict = copy.deepcopy(_dict) + # self._dict = _dict + + + # self.changedDict = {} + self._buildGUI() + + # self.show() + + def on_spin_box(self, paramName, value): + """ + When QDoubldeSpinBox accepts None, value is -1e9 + """ + # Create a key value pair in changed value dictionary + # self.changedDict[paramName] = value + + if paramName == "channel": + # channel is offseted by 1 in calculation + self._dict[paramName]["currentValue"] = value - 1 + else: + # update dictionary directly + self._dict[paramName]["currentValue"] = value + + self.canApply = True + self.canSave = True + self.applyButton.setEnabled(self.canApply) + self.saveButton.setEnabled(self.canSave) + # logger.info(f"updated Dict {self._dict}") + + def _buildGUI(self): + + self.layout = QtWidgets.QVBoxLayout() + # self.setLayout(self.layout) + windowLayout = self.buildAnalysisParamUI() + self.layout.addLayout(windowLayout) + self._makeCentralWidget(self.layout) + + def buildAnalysisParamUI(self): + # key = name of parameter + # val = columns within dictionary. Used column name to get value + + vLayout = QtWidgets.QVBoxLayout() + vLayoutParams = QtWidgets.QGridLayout() + + hControlLayout = self.controlUI() + + vLayout.addLayout(hControlLayout) + vLayout.addLayout(vLayoutParams) + # self.stacked.addWidget(vLayout) + + col = 0 + row = 0 + rowSpan = 1 + colSpan = 1 + # logger.info(f"type of dict {type(self._dict)}") + for key, val in self._dict.items(): + + if key == "__version__": + continue + else: + col = 0 + # print("key", key) + # print("val", val) + # print("v", val["type"]) + paramName = key + currentValue = val["currentValue"] + defaultValue = val["defaultValue"] + valueType = "int" # Currently all values are int. TODO: have a 'types' key in the backend + # valueType = val[ + # "type" + # ] # from ('int', 'float', 'boolean', 'string', detectionTypes_) + # units = val["units"] + # humanName = val["humanName"] + description = val["description"] + + aLabel = QtWidgets.QLabel(paramName) + vLayoutParams.addWidget(aLabel, row, col, rowSpan, colSpan) + col += 1 + + # humanNameLabel = QtWidgets.QLabel(humanName) + # vLayoutParams.addWidget(humanNameLabel, row, col, rowSpan, colSpan) + # col += 1 + + # unitsLabel = QtWidgets.QLabel(units) + # vLayoutParams.addWidget(unitsLabel, row, col, rowSpan, colSpan) + # col += 1 + + # Different conditions for key type + aWidget = None + if valueType == "int": + aWidget = QtWidgets.QSpinBox() + + # TODO: limit by how many channels there actually are + if paramName == "channel": + logger.info(f"channel current value {currentValue}") + aWidget.setRange(1, 2) + + # need to offset value for channel indexing in backend + currentValue = val["currentValue"] + 1 + else: + aWidget.setRange(0, 2**16) + + # minimum is used for setSpecialValueText() + # aWidget.setSpecialValueText( + # "None" + # ) # displayed when widget is set to minimum + if currentValue is None or math.isnan(currentValue): + aWidget.setValue(0) + else: + aWidget.setValue(currentValue) + aWidget.setKeyboardTracking(False) # don't trigger signal as user edits + aWidget.valueChanged.connect(partial(self.on_spin_box, paramName)) + + elif valueType == "float": + aWidget = QtWidgets.QDoubleSpinBox() + aWidget.setRange( + -1e9, +1e9 + ) # minimum is used for setSpecialValueText() + # aWidget.setSpecialValueText( + # "None" + # ) # displayed when widget is set to minimum + + if currentValue is None or math.isnan(currentValue): + aWidget.setValue(-1e9) + else: + aWidget.setValue(currentValue) + aWidget.setKeyboardTracking(False) # don't trigger signal as user edits + aWidget.valueChanged.connect(partial(self.on_spin_box, paramName)) + elif valueType == "list": + # text edit a list + pass + elif valueType in ["bool", "boolean"]: + # popup of True/False + aWidget = QtWidgets.QComboBox() + aWidget.addItem("True") + aWidget.addItem("False") + aWidget.setCurrentText(str(currentValue)) + aWidget.currentTextChanged.connect( + partial(self.on_bool_combo_box, paramName) + ) + elif valueType == "string": + # text edit + aWidget = QtWidgets.QLineEdit(currentValue) + aWidget.setReadOnly(True) # for now our 1 edit widget is not editable + aWidget.setAlignment(QtCore.Qt.AlignLeft) + aWidget.editingFinished.connect( + partial(self.on_text_edit, aWidget, paramName) + ) + else: + logger.error( + f'Did not understand valueType:"{valueType}" for parameter:"{paramName}"' + ) + + if aWidget is not None: + # keep track of what we are displaying + # So that we can set to default + self.widgetDict[paramName] = { + "widget": aWidget, + # "nameLabelWidget": humanNameLabel, + } + + vLayoutParams.addWidget(aWidget, row, col, rowSpan, colSpan) + col += 1 + + descriptionLabel = QtWidgets.QLabel(description) + vLayoutParams.addWidget(descriptionLabel, row, col, rowSpan, colSpan) + row += 1 + return vLayout + + def controlUI(self): + # top controls + hControlLayout = QtWidgets.QHBoxLayout() + + aName = "Set Defaults" + aButton = QtWidgets.QPushButton(aName) + aButton.clicked.connect(partial(self.on_button_click, aName)) + hControlLayout.addWidget(aButton, alignment=QtCore.Qt.AlignLeft) + + applyButtonName = "Apply" + self.applyButton = QtWidgets.QPushButton(applyButtonName) + self.applyButton.clicked.connect(partial(self.on_button_click, applyButtonName)) + self.applyButton.setEnabled(self.canApply) + hControlLayout.addWidget(self.applyButton, alignment=QtCore.Qt.AlignLeft) + + saveButtonName = "Save" + self.saveButton = QtWidgets.QPushButton(saveButtonName) + self.saveButton.clicked.connect(partial(self.on_button_click, saveButtonName)) + self.saveButton.setEnabled(self.canSave) + hControlLayout.addWidget( self.saveButton, alignment=QtCore.Qt.AlignLeft) + + # Moves the buttons closer togethers + hControlLayout.addStretch() + return hControlLayout + + + def refreshWidget(self): + """ refresh Analysis Params Widget interface + """ + + # logger.info(f"replot self._dict {self._dict}") + for key, val in self._dict.items(): + # key = paramName, ex: width + # val = all values of that key, (currentValue, defaultValue, etc...) + if key == "__version__": + continue + else: + paramName = key + aWidget = self.widgetDict[paramName]["widget"] + # print("key", key) + # print("val", val) + currentValue = self._dict[key]["defaultValue"] + if isinstance(aWidget, QtWidgets.QSpinBox): + try: + if currentValue is None or math.isnan(currentValue): + aWidget.setValue(0) + else: + aWidget.setValue(currentValue) + except TypeError as e: + logger.error(f"QSpinBox analysisParam:{paramName} ... {e}") + elif isinstance(aWidget, QtWidgets.QDoubleSpinBox): + try: + if currentValue is None or math.isnan(currentValue): + aWidget.setValue(-1e9) + else: + aWidget.setValue(currentValue) + except TypeError as e: + logger.error( + f"QDoubleSpinBox analysisParam:{paramName} ... {e}" + ) + elif isinstance(aWidget, QtWidgets.QComboBox): + aWidget.setCurrentText(str(currentValue)) + elif isinstance(aWidget, QtWidgets.QLineEdit): + aWidget.setText(str(currentValue)) + else: + logger.warning( + f'key "{paramName}" has value "{currentValue}" but widget type "{type(aWidget)}" not understood.') + + def on_button_click(self, buttonName): + """ + Buttons: + Set Default - Reset all current values to the original Default Values + Apply - Confirms changes and applies the changed values in the backend + - Only applys to that one particular spine + Save - permanently saves dict changes to the backend (zarr directory file) + + + """ + if buttonName == "Set Defaults": + # call a function in mapmanagercore to set default + # temp = self._analysisParameters.resetDefaults() + # logger.info(f"temp {temp}") + # self._dict = self._analysisParameters._getDefaults() + _resetDict = self._analysisParameters.resetDefaults() + self._dict = copy.deepcopy(_resetDict) + # logger.info(f"self._dict {self._dict}") + self.refreshWidget() + + elif buttonName == "Apply": + + #TODO: disable apply when all current changes have already been applied + + # Set dict in mapmanagercore backend + self._analysisParameters.setDict(self._dict) + appliedDict = self._analysisParameters.getDict() + # Ensure that local dict is a deep copy of backend dict + # Otherwise non applied changes are reflected immediately + self._dict = copy.deepcopy(appliedDict) + + # refresh point and line annotations within stack/stackwidget + # self.stackWidget.updateDFwithNewParams() + + self.canApply = False + self.applyButton.setEnabled(self.canApply) + + #TODO: roi extend is is used from csv instead of analysis parameters for mapmanagercore + #TODO: restrict certain keys values such as channel () + #should we only apply the changes to the current selected spine immediately. + # apply - apply changes to only current spine + # --- if we do this then each time we open up the widget we have to get each spines unique value + + elif buttonName == "Save": + if self.pmmApp is None: + # save to mmap directory file + self._analysisParameters.save(externalDict = self._dict) + else: + # save to json file in user directory + self.pmmApp.saveAnalysisParams(self._dict) + + self.canSave= False + self.saveButton.setEnabled(self.canSave) + + else: + logger.warning(f'Button "{buttonName}" not understood.') + +if __name__ == '__main__': + dp = AnalysisParams() + + import pymapmanager.interface + app = pymapmanager.interface.PyMapManagerApp() + dpWidget = AnalysisParamWidget(dp) + + import sys + sys.exit(app.exec_()) + # print(dp._getDocs()) \ No newline at end of file diff --git a/pymapmanager/interface2/stackWidgets/stackWidget2.py b/pymapmanager/interface2/stackWidgets/stackWidget2.py index 16f720a2..e8415efb 100644 --- a/pymapmanager/interface2/stackWidgets/stackWidget2.py +++ b/pymapmanager/interface2/stackWidgets/stackWidget2.py @@ -153,6 +153,22 @@ def getStack(self) -> pymapmanager.stack: """ return self._stack + def isTifPath(self) -> bool: + """ Check if stack has been saved by checking extension + + ".mmap" = has been saved before -> we can get json from .zattributes + ".tif" = has not been saved -> use default json in users/documents + """ + path = self.getStack().getPath() + ext = os.path.splitext(path)[1] + # logger.info(f"ext {ext}") + if ext == ".tif": + return True + elif ext == ".mmap": + return False + else: + logger.info(f"Unsupported extension: {ext}") + def _cancelSelection(self): """Cancel the current stack selection. @@ -447,8 +463,6 @@ def updateRadius(self, newRadius): _pmmEvent.setSliceNumber(self._currentSliceNumber) self.emitEvent(_pmmEvent, blockSlots=True) - - def updatePlotBoxes(self, plotName): """ update check boxes that displays individual plots in ImagePlotWidget """ @@ -1126,4 +1140,30 @@ def getDirty(self): if isPaDirty or isLaDirty: return True else: - return False \ No newline at end of file + return False + + #abj + def getAnalysisParams(self): + """ Get analysis Params from MapManagerCore + """ + # pass + + return self.getStack().getAnalysisParameters() + + # def saveAnalysisParamsDict(self): + # """ Save analysis Params changes to zarr directory using MapManagerCore + # """ + # pass + + def updateDFwithNewParams(self): + """ Rebult line and point dataframes after analysis params changes are applied + """ + self.getStack().getLineAnnotations()._buildDataFrame() + self.getStack().getPointAnnotations()._buildDataFrame() + + # call set slice to refresh widgets + # TODO: create a custom event for this? + _pmmEvent = pmmEvent(pmmEventType.setSlice, self) + _pmmEvent.setSliceNumber(self._currentSliceNumber) + self.emitEvent(_pmmEvent, blockSlots=True) + diff --git a/pymapmanager/pmmUtils.py b/pymapmanager/pmmUtils.py index cdd6718a..28e736d4 100644 --- a/pymapmanager/pmmUtils.py +++ b/pymapmanager/pmmUtils.py @@ -1,6 +1,7 @@ """ Includes utilities that uses classes within pymapmanager """ +import json import sys, os import math from typing import List @@ -16,6 +17,9 @@ import pymapmanager as pmm from pymapmanager.utils import _findBrightestIndex +from pymapmanager._logger import logger +import pathlib +import shutil def getBundledDir(): """Get the working directory where user preferences are save. @@ -31,6 +35,117 @@ def getBundledDir(): bundle_dir = os.path.dirname(os.path.abspath(__file__)) return bundle_dir +#abj +# In order to save anaylis parameters json file need to include json file in .spec pyinstaller file +# This will typically import it from the main directory (with datas) but maybe we can get it from the +# mapmanagercore directory +# TODO: use this when app is first made +def addUserPath(jsonDump): + """Make /Documents/Pymapmanager-User-Files folder and add it to the Python sys.path + + Returns: + True: If we made the folder (first time SanPy is running) + """ + + madeUserFolder = _makePmmFolders(jsonDump) # make /Documents/Pmm if necc + + userPmmFolder = _getUserPmmFolder() + + if not userPmmFolder in sys.path: + logger.info(f"Adding to sys.path: {userPmmFolder}") + sys.path.append(userPmmFolder) + + logger.info("sys.path is now:") + for path in sys.path: + logger.info(f" {path}") + + return madeUserFolder + +def _makePmmFolders(analysisParamJson): + """Make /Documents/Pymapmanager-User-Files folder . + + If no Documents folder then make Pmm folder directly in path. + + Args: + Json File to hold analysis parameters + + """ + # userDocumentsFolder = _getUserDocumentsFolder() + + madeUserFolder = False + + # main /Documents/SanPy folder + pmmFolder = _getUserPmmFolder() + if not os.path.isdir(pmmFolder): + # first time run + logger.info(f'Making /Pymapmanager-User-Files folder "{pmmFolder}"') + os.makedirs(pmmFolder) + madeUserFolder = True + + # _bundDir = getBundledDir() + + # Save json file to create pmm folder + # _dstPath = pathlib.Path(pmmFolder) + _dstPath = os.path.join(pmmFolder, "userAnalysisParameters.Json") + logger.info(f" _dstPath:{_dstPath}") + + with open(_dstPath, 'w') as file: + json.dump(analysisParamJson, file, indent = 4) + + else: + # already exists, make sure we have all sub-folders that are expected + pass + + return madeUserFolder + +def saveAnalysisParamJsonFile(jsonData): + """ Save/ overwrite new data to user analysis parameters json file + """ + pmmFolder = _getUserPmmFolder() + _dstPath = os.path.join(pmmFolder, "userAnalysisParameters.Json") + + with open(_dstPath, 'w') as file: + json.dump(jsonData, file) + +def getUserAnalysisParamJsonData(): + """ + get User's Json data for Analysis Parameters + + """ + pmmFolder = _getUserPmmFolder() + _dstPath = os.path.join(pmmFolder, "userAnalysisParameters.Json") + readFile = open(_dstPath) + + if not os.path.exists(_dstPath): + logger.info(f"Could not find path {_dstPath}") + else: + try: + jsonString = json.load(readFile) + jsonDict = json.loads(jsonString) + except: + logger.info("error loading in user json") + + # logger.info(f"jsonDict {jsonDict}") + return jsonDict + +def _getUserPmmFolder(): + """Get /Documents/Pymapmanager-User-Files folder.""" + userDocumentsFolder = _getUserDocumentsFolder() + pmmFolder = os.path.join(userDocumentsFolder, "Pymapmanager-User-Files") + return pmmFolder + +def _getUserDocumentsFolder(): + """Get /Documents folder.""" + userPath = pathlib.Path.home() + userDocumentsFolder = os.path.join(userPath, "Documents") + if not os.path.isdir(userDocumentsFolder): + logger.error(f'Did not find path "{userDocumentsFolder}"') + logger.error(f' Using "{userPath}"') + return userPath + else: + return userDocumentsFolder + + def calculateRectangleROIcoords(xPlotLines, yPlotLines, xPlotSpines, yPlotSpines): """ Args: diff --git a/pymapmanager/stack.py b/pymapmanager/stack.py index ad628b0d..e3e37209 100644 --- a/pymapmanager/stack.py +++ b/pymapmanager/stack.py @@ -65,6 +65,7 @@ def __init__(self, path : str = None, # TODO (cudmore) we should add an option to defer loading until explicitly called self.loadAnnotations() self.loadLines() + self.loadAnalysisParams() #abj self._buildHeader() @@ -228,6 +229,10 @@ def sessionID(self): """ return self._mmMapSession + #abj + def getAnalysisParameters(self): + return self._analysisParams + def getPointAnnotations(self) -> SpineAnnotationsCore: return self._annotations @@ -248,6 +253,12 @@ def loadLines(self) -> None: defaultColums = self._fullMap.segments[:].columns self._lines = LineAnnotationsCore(self.sessionMap, defaultColums=defaultColums) + def loadAnalysisParams(self) -> None: + """ load analysis parameters + """ + self._analysisParams = self._fullMap.analysisParams + logger.info(f"analysis params {self._analysisParams}") + def getAutoContrast(self, channel): channelIdx = channel - 1 _min, _max = self.sessionMap.getAutoContrast_qt(channel=channelIdx) @@ -430,5 +441,11 @@ def saveAs(self, path): """ self._fullMap.save(path) + def isEmpty(self): + + if len(self._annotations) > 0 or len(self._lines) > 0: + return True + else: + return False \ No newline at end of file diff --git a/pymapmanager/utils.py b/pymapmanager/utils.py index 95c9bd95..5b0801f4 100644 --- a/pymapmanager/utils.py +++ b/pymapmanager/utils.py @@ -1,4 +1,4 @@ -""" +""" DEFUNCT - NO LONGER USED SINCE MapManagerCoreIncorporation General purpose utilities. These should not include any imports beyond things like