diff --git a/magmap/atlas/brain_globe.py b/magmap/atlas/brain_globe.py index cdac0bbeb..b7cbcc570 100644 --- a/magmap/atlas/brain_globe.py +++ b/magmap/atlas/brain_globe.py @@ -1,8 +1,8 @@ -"""BrainGlobe integration in MageallanMapper""" +"""BrainGlobe model for integration in MagellanMapper""" from requests.exceptions import ConnectionError import shutil -from typing import Dict +from typing import Dict, Optional from bg_atlasapi import list_atlases, bg_atlas @@ -12,40 +12,81 @@ class BrainGlobeMM: + """Model for BrainGlobe-MagellanMapper interactions. + + Attributes: + atlases_avail: Dictionary of the names of available atlases from + BrainGlobe to their latest version string. + atlases_local: Dictionary of the names of locally downloaded BrainGlobe + atlases to their version string. + + """ def __init__(self): self.atlases_avail: Dict[str, str] = {} self.atlases_local: Dict[str, str] = {} - def get_avail_atlases(self): + def get_avail_atlases(self) -> Dict[str, str]: + """Fetch the available atlases from BrainGlobe. + + Returns: + Dictionary of the names of available atlases from BrainGlobe to + their latest version string. + + """ try: self.atlases_avail = list_atlases.get_all_atlases_lastversions() except ConnectionError: _logger.warn("Unable to get BrainGlobe available atlases") return self.atlases_avail - def get_local_atlases(self): + def get_local_atlases(self) -> Dict[str, str]: + """Get local, downloaded BrainGlobe atlases. + + Returns: + Dictionary of the names of locally downloaded BrainGlobe atlases + to their version string. + + """ self.atlases_local = { a: list_atlases.get_local_atlas_version(a) for a in list_atlases.get_downloaded_atlases() } return self.atlases_local - def get_atlas(self, name, download=True): + def get_atlas( + self, name: str, download: bool = True + ) -> Optional[bg_atlas.BrainGlobeAtlas]: + """Get a BrainGlobe atlas. + + Args: + name: Name of atlas to retrieve. + download: True to download the atlas if not available locally; + False to return None if the atlas is not present. + + Returns: + The BrainGlobe atlas instance. + + """ if not download and name not in self.atlases_local: return None atlas = bg_atlas.BrainGlobeAtlas(name) return atlas - def remove_local_atlas(self, name): + def remove_local_atlas(self, name: str): + """Remove local copy of downloaded BrainGlobe atlas. + + Args: + name: Name of atlas to remove + + """ atlas = self.get_atlas(name, False) - print("atlas to remove", name, atlas) if not atlas: _logger.warn("'%s' atlas not found", name) return try: - print("path", atlas.root_dir) if atlas.root_dir.is_dir(): - print("removing") shutil.rmtree(atlas.root_dir) + _logger.debug( + "Removed '%s' atlas from '%s'", name, atlas.root_dir) except FileExistsError as e: _logger.warn(e) diff --git a/magmap/gui/bg_panel.py b/magmap/gui/bg_panel.py index 71397fd9c..8138df449 100644 --- a/magmap/gui/bg_panel.py +++ b/magmap/gui/bg_panel.py @@ -1,78 +1,97 @@ -"""Panel for BrainGlobe access""" +"""Panel controller for BrainGlobe access""" + +from typing import Callable, Optional, Sequence, TYPE_CHECKING from PyQt5 import QtCore from magmap.atlas import brain_globe +if TYPE_CHECKING: + from bg_atlasapi import BrainGlobeAtlas + class SetupAtlasesThread(QtCore.QThread): - """Thread for setting up file import by extracting image metadata. + """Thread for setting atlases by fetching the BrainGlobe atlas listing. Attributes: - fn_success (func): Signal taking - no arguments, to be emitted upon successfull import; defaults - to None. + brain_globe_mm: BrainGlobe-MagellanMapper model. + fn_success: Signal function taking no arguments, to be emitted upon + successfull import. + fn_progress: Signal function taking a string argument to emit feedback. """ signal = QtCore.pyqtSignal() + progress = QtCore.pyqtSignal(str) - def __init__(self, brain_globe_mm, fn_success): - """Initialize the import thread.""" + def __init__( + self, brain_globe_mm: brain_globe.BrainGlobeMM, + fn_success: Callable[[], None], fn_progress: Callable[[str], None]): + """Initialize the setup thread.""" super().__init__() self.bg_mm: brain_globe.BrainGlobeMM = brain_globe_mm self.signal.connect(fn_success) + self.progress.connect(fn_progress) def run(self): - """Set up image import metadata.""" - print("running") - self.bg_mm.get_avail_atlases() + """Fetch the atlas listing.""" + atlases = self.bg_mm.get_avail_atlases() + msg = ("Fetched atlases available from BrainGlobe" if atlases + else "Unable to access atlas listing from BrainGlobe. " + "Showing atlases dowloaded from BrainGlobe.") + self.progress.emit(msg) self.signal.emit() class AccessAtlasThread(QtCore.QThread): - """Thread for setting up file import by extracting image metadata. + """Thread for setting up a specific access. Attributes: - fn_success (func): Signal taking - no arguments, to be emitted upon successfull import; defaults - to None. + fn_success: Signal function taking no arguments, to be emitted upon + successfull import. + fn_progress: Signal function taking a string argument to emit feedback. """ - signal = QtCore.pyqtSignal() + signal = QtCore.pyqtSignal(object) progress = QtCore.pyqtSignal(str) - def __init__(self, brain_globe_mm, name, fn_success, fn_progress): - """Initialize the import thread.""" + def __init__( + self, brain_globe_mm: brain_globe.BrainGlobeMM, name: str, + fn_success: Callable[[], None], fn_progress: Callable[[str], None]): + """Initialize the atlas access thread.""" super().__init__() self.bg_mm: brain_globe.BrainGlobeMM = brain_globe_mm self.name = name - print("gonna get atlases") self.signal.connect(fn_success) self.progress.connect(fn_progress) - print("connected") def run(self): - """Set up image import metadata.""" - print("running") + """Access the atlas, including download if necessary.""" self.progress.emit( f"Accessing atlas '{self.name}', downloading if necessary...") atlas = self.bg_mm.get_atlas(self.name) self.progress.emit(f"Atlas '{self.name}' accessed:\n{atlas}") - self.signal.emit() + self.signal.emit(atlas) class BrainGlobePanel: - def __init__(self, fn_set_atlases_table, fn_set_feedback): + def __init__( + self, fn_set_atlases_table: Callable[[Sequence], None], + fn_set_feedback: Callable[[str], None], + fn_opened_atlas: Optional[Callable[ + ["BrainGlobeAtlas"], None]] = None): + # set up attributes self.fn_set_atlases_table = fn_set_atlases_table self.fn_set_feedback = fn_set_feedback + self.fn_opened_atlas = fn_opened_atlas # set up BrainGlobe-MagellanMapper interface self.bg_mm = brain_globe.BrainGlobeMM() # fetch listing of available atlases - self._thread = SetupAtlasesThread(self.bg_mm, self.update_atlas_panel) + self._thread = SetupAtlasesThread( + self.bg_mm, self.update_atlas_panel, self.fn_set_feedback) self._thread.start() def update_atlas_panel(self): @@ -89,13 +108,18 @@ def update_atlas_panel(self): installed = "No" data.append([name, ver, installed]) for name, ver in atlases_local.items(): - if atlases and name not in atlases: + if not atlases or name not in atlases: data.append([name, ver, "Yes"]) self.fn_set_atlases_table(data) + def _open_atlas_handler(self, atlas): + self.update_atlas_panel() + if self.fn_opened_atlas: + self.fn_opened_atlas(atlas) + def open_atlas(self, name): self._thread = AccessAtlasThread( - self.bg_mm, name, self.update_atlas_panel, self.fn_set_feedback) + self.bg_mm, name, self._open_atlas_handler, self.fn_set_feedback) self._thread.start() def remove_atlas(self, name): diff --git a/magmap/gui/visualizer.py b/magmap/gui/visualizer.py index 3d25a2044..5e698d3e4 100644 --- a/magmap/gui/visualizer.py +++ b/magmap/gui/visualizer.py @@ -1769,17 +1769,20 @@ def _image_path_updated(self): from the Numpy image filename. Processed files (eg ROIs, blobs) will not be loaded for now. """ - if self._ignore_filename or not self._filename: + ignore_filename = self._ignore_filename or not self._filename + reset_filename = self._reset_filename + if ignore_filename: # avoid triggering file load, eg if only updating widget value; # reset flags self._ignore_filename = False self._reset_filename = True - return - - if self._reset_filename: + + if reset_filename: # reset registered suffixes config.reg_suffixes = dict.fromkeys(config.RegSuffixes, None) self._reset_filename = True + + if ignore_filename or reset_filename: return # load image if possible without allowing import, deconstructing # filename from the selected imported image @@ -3247,18 +3250,28 @@ def _set_bg_feedback(self, val, append=True): else: self._bg_feedback = val + def _bg_open_handler(self, bg_atlas): + config.filename = str(bg_atlas.root_dir) + self.update_filename(config.filename, True) + np_io.setup_images( + config.filename, allow_import=False, bg_atlas=bg_atlas) + self._setup_for_image() + self.redraw_selected_viewer() + self.update_imgadj_for_img() + def _setup_brain_globe(self): - panel = bg_panel.BrainGlobePanel(self._set_bg_atlases, self._set_bg_feedback) + panel = bg_panel.BrainGlobePanel( + self._set_bg_atlases, self._set_bg_feedback, self._bg_open_handler) return panel @on_trait_change("_bg_access_btn") def _open_brain_globe_atlas(self): - if self._bg_atlases and self._bg_atlases_sel: + if self._bg_atlases_sel: self._brain_globe_panel.open_atlas(self._bg_atlases_sel[0]) @on_trait_change("_bg_remove_btn") def _remove_brain_globe_atlas(self): - if self._bg_atlases and self._bg_atlases_sel: + if self._bg_atlases_sel: self._brain_globe_panel.remove_atlas(self._bg_atlases_sel[0])