Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Manage atlas versions in a separate widget #100

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion brainrender_napari/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
__version__ = "0.0.1"


__all__ = "BrainrenderWidget"
__all__ = "BrainrenderWidget", "AtlasVersionManagerWidget"
28 changes: 28 additions & 0 deletions brainrender_napari/atlas_version_manager_widget.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
from qtpy.QtWidgets import QVBoxLayout, QWidget

from brainrender_napari.utils.brainglobe_logo import header_widget
from brainrender_napari.widgets.atlas_manager_view import AtlasManagerView


class AtlasVersionManagerWidget(QWidget):
def __init__(self):
"""Instantiates the version manager widget
and sets up coordinating connections"""
super().__init__()

self.setLayout(QVBoxLayout())
self.layout().addWidget(
header_widget(tutorial_file_name="update-atlas-napari.html")
)

# create widgets
self.atlas_manager_view = AtlasManagerView(parent=self)
self.layout().addWidget(self.atlas_manager_view)

self.atlas_manager_view.download_atlas_confirmed.connect(self._refresh)

self.atlas_manager_view.update_atlas_confirmed.connect(self._refresh)

def _refresh(self) -> None:
# refresh view once an atlas has been downloaded
self.atlas_manager_view = AtlasManagerView(parent=self)
16 changes: 5 additions & 11 deletions brainrender_napari/brainrender_widget.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
NapariAtlasRepresentation,
)
from brainrender_napari.utils.brainglobe_logo import header_widget
from brainrender_napari.widgets.atlas_table_view import AtlasTableView
from brainrender_napari.widgets.atlas_viewer_view import AtlasViewerView
from brainrender_napari.widgets.structure_view import StructureView


Expand All @@ -40,10 +40,12 @@ def __init__(self, napari_viewer: Viewer):

self._viewer = napari_viewer
self.setLayout(QVBoxLayout())
self.layout().addWidget(header_widget())
self.layout().addWidget(
header_widget(tutorial_file_name="visualise-atlas-napari.html")
)

# create widgets
self.atlas_table_view = AtlasTableView(parent=self)
self.atlas_table_view = AtlasViewerView(parent=self)

self.show_structure_names = QCheckBox()
self.show_structure_names.setChecked(False)
Expand Down Expand Up @@ -79,9 +81,6 @@ def __init__(self, napari_viewer: Viewer):
self.layout().addWidget(self.structure_tree_group)

# connect atlas view widget signals
self.atlas_table_view.download_atlas_confirmed.connect(
self._on_download_atlas_confirmed
)
self.atlas_table_view.add_atlas_requested.connect(
self._on_add_atlas_requested
)
Expand All @@ -102,11 +101,6 @@ def __init__(self, napari_viewer: Viewer):
self._on_add_structure_requested
)

def _on_download_atlas_confirmed(self, atlas_name):
"""Ensure structure view is displayed if new atlas downloaded."""
show_structure_names = self.show_structure_names.isChecked()
self.structure_view.refresh(atlas_name, show_structure_names)

def _on_add_structure_requested(self, structure_name: str):
"""Add given structure as napari atlas representation"""
selected_atlas = BrainGlobeAtlas(
Expand Down
96 changes: 96 additions & 0 deletions brainrender_napari/data_models/atlas_table_model.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
from bg_atlasapi.list_atlases import (
get_all_atlases_lastversions,
get_atlases_lastversions,
get_downloaded_atlases,
get_local_atlas_version,
)
from qtpy.QtCore import QAbstractTableModel, QModelIndex, Qt

from brainrender_napari.utils.load_user_data import (
read_atlas_metadata_from_file,
)


class AtlasTableModel(QAbstractTableModel):
"""A table data model for atlases."""

def __init__(self):
super().__init__()
self.refresh_data()

def refresh_data(self) -> None:
"""Refresh model data by calling atlas API"""
all_atlases = get_all_atlases_lastversions()
data = []
for name, latest_version in all_atlases.items():
if name in get_atlases_lastversions().keys():
data.append(
[
name,
self._format_name(name),
get_local_atlas_version(name),
latest_version,
]
)
else:
data.append(
[name, self._format_name(name), "n/a", latest_version]
)

self._data = data

def _format_name(self, name: str) -> str:
formatted_name = name.split("_")
formatted_name[0] = formatted_name[0].capitalize()
formatted_name[-1] = f"({formatted_name[-1].split('um')[0]} \u03BCm)"
return " ".join([formatted for formatted in formatted_name])

def data(self, index: QModelIndex, role=Qt.DisplayRole):
if role == Qt.DisplayRole:
return self._data[index.row()][index.column()]
if role == Qt.ToolTipRole:
hovered_atlas_name = self._data[index.row()][0]
return AtlasTableModel._get_tooltip_text(hovered_atlas_name)

def rowCount(self, index: QModelIndex = QModelIndex()):
return len(self._data)

def columnCount(self, index: QModelIndex = QModelIndex()):
return len(self._data[0])

def headerData(
self, section: int, orientation: Qt.Orientation, role: Qt.ItemDataRole
):
"""Customises the horizontal header data of model,
and raises an error if an unexpected column is found."""
if role == Qt.DisplayRole and orientation == Qt.Orientation.Horizontal:
if section == 0:
return "Raw name"
elif section == 1:
return "Atlas"
elif section == 2:
return "Local version"
elif section == 3:
return "Latest version"
else:
raise ValueError("Unexpected horizontal header value.")
else:
return super().headerData(section, orientation, role)

@classmethod
def _get_tooltip_text(cls, atlas_name: str):
"""Returns the atlas metadata as a formatted string,
as well as instructions on how to interact with the atlas."""
if atlas_name in get_downloaded_atlases():
metadata = read_atlas_metadata_from_file(atlas_name)
metadata_as_string = ""
for key, value in metadata.items():
metadata_as_string += f"{key}:\t{value}\n"

tooltip_text = f"{atlas_name} (double-click to add to viewer)\
\n{metadata_as_string}"
elif atlas_name in get_all_atlases_lastversions().keys():
tooltip_text = f"{atlas_name} (double-click to download)"
else:
raise ValueError("Tooltip text called with invalid atlas name.")
return tooltip_text
144 changes: 144 additions & 0 deletions brainrender_napari/data_models/structure_tree_model.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
from typing import Dict, List

from bg_atlasapi.structure_tree_util import get_structures_tree
from qtpy.QtCore import QAbstractItemModel, QModelIndex, Qt
from qtpy.QtGui import QStandardItem


class StructureTreeItem(QStandardItem):
"""A class to hold items in a tree model."""

def __init__(self, data, parent=None):
self.parent_item = parent
self.item_data = data
self.child_items = []

def appendChild(self, item):
self.child_items.append(item)

def child(self, row):
return self.child_items[row]

def childCount(self):
return len(self.child_items)

def columnCount(self):
return len(self.item_data)

def data(self, column):
try:
return self.item_data[column]
except IndexError:
return None

Check warning on line 32 in brainrender_napari/data_models/structure_tree_model.py

View check run for this annotation

Codecov / codecov/patch

brainrender_napari/data_models/structure_tree_model.py#L31-L32

Added lines #L31 - L32 were not covered by tests

def parent(self):
return self.parent_item

def row(self):
if self.parent_item:
return self.parent_item.child_items.index(self)
return 0

Check warning on line 40 in brainrender_napari/data_models/structure_tree_model.py

View check run for this annotation

Codecov / codecov/patch

brainrender_napari/data_models/structure_tree_model.py#L40

Added line #L40 was not covered by tests


class StructureTreeModel(QAbstractItemModel):
"""Implementation of a read-only QAbstractItemModel to hold
the structure tree information provided by the Atlas API in a Qt Model"""

def __init__(self, data: List, parent=None):
super().__init__()
self.root_item = StructureTreeItem(data=("acronym", "name", "id"))
self.build_structure_tree(data, self.root_item)

def build_structure_tree(self, structures: List, root: StructureTreeItem):
"""Build the structure tree given a list of structures."""
tree = get_structures_tree(structures)
structure_id_dict = {}
for structure in structures:
structure_id_dict[structure["id"]] = structure

inserted_items: Dict[int, StructureTreeItem] = {}
for n_id in tree.expand_tree(): # sorts nodes by default,
# so parents will always be already in the QAbstractItemModel
# before their children
node = tree.get_node(n_id)
acronym = structure_id_dict[node.identifier]["acronym"]
name = structure_id_dict[node.identifier]["name"]
if (
len(structure_id_dict[node.identifier]["structure_id_path"])
== 1
):
parent_item = root
else:
parent_id = tree.parent(node.identifier).identifier
parent_item = inserted_items[parent_id]

item = StructureTreeItem(
data=(acronym, name, node.identifier), parent=parent_item
)
parent_item.appendChild(item)
inserted_items[node.identifier] = item

def data(self, index: QModelIndex, role=Qt.DisplayRole):
"""Provides read-only data for a given index if
intended for display, otherwise None."""
if not index.isValid():
return None

Check warning on line 85 in brainrender_napari/data_models/structure_tree_model.py

View check run for this annotation

Codecov / codecov/patch

brainrender_napari/data_models/structure_tree_model.py#L85

Added line #L85 was not covered by tests

if role != Qt.DisplayRole:
return None

item = index.internalPointer()

return item.data(index.column())

def rowCount(self, parent: StructureTreeItem):
"""Returns the number of rows(i.e. children) of an item"""
if parent.column() > 0:
return 0

Check warning on line 97 in brainrender_napari/data_models/structure_tree_model.py

View check run for this annotation

Codecov / codecov/patch

brainrender_napari/data_models/structure_tree_model.py#L97

Added line #L97 was not covered by tests

if not parent.isValid():
parent_item = self.root_item
else:
parent_item = parent.internalPointer()

return parent_item.childCount()

def columnCount(self, parent: StructureTreeItem):
"""The number of columns of an item."""
if parent.isValid():
return parent.internalPointer().columnCount()
else:
return self.root_item.columnCount()

def parent(self, index: QModelIndex):
"""The first-column index of parent of the item
at a given index. Returns an empty index if the root,
or an invalid index, is passed.
"""
if not index.isValid():
return QModelIndex()

Check warning on line 119 in brainrender_napari/data_models/structure_tree_model.py

View check run for this annotation

Codecov / codecov/patch

brainrender_napari/data_models/structure_tree_model.py#L119

Added line #L119 was not covered by tests

child_item = index.internalPointer()
parent_item = child_item.parent()

if parent_item == self.root_item:
return QModelIndex()

return self.createIndex(parent_item.row(), 0, parent_item)

def index(self, row, column, parent=QModelIndex()):
"""The index of the item at (row, column) with a given parent.
By default, the given parent is assumed to be the root."""
if not self.hasIndex(row, column, parent):
return QModelIndex()

Check warning on line 133 in brainrender_napari/data_models/structure_tree_model.py

View check run for this annotation

Codecov / codecov/patch

brainrender_napari/data_models/structure_tree_model.py#L133

Added line #L133 was not covered by tests

if not parent.isValid():
parent_item = self.root_item
else:
parent_item = parent.internalPointer()

child_item = parent_item.child(row)
if child_item:
return self.createIndex(row, column, child_item)
else:
return QModelIndex()

Check warning on line 144 in brainrender_napari/data_models/structure_tree_model.py

View check run for this annotation

Codecov / codecov/patch

brainrender_napari/data_models/structure_tree_model.py#L144

Added line #L144 was not covered by tests
5 changes: 5 additions & 0 deletions brainrender_napari/napari.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@ contributions:
- id: brainrender-napari.make_brainrender_widget
python_name: brainrender_napari.brainrender_widget:BrainrenderWidget
title: Make Brainrender Napari
- id: brainrender-napari.make_atlas_version_manager_widget
python_name: brainrender_napari.atlas_version_manager_widget:AtlasVersionManagerWidget
title: Make Brainrender Napari
widgets:
- command: brainrender-napari.make_brainrender_widget
display_name: Brainrender
- command: brainrender-napari.make_atlas_version_manager_widget
display_name: Atlas version manager
37 changes: 18 additions & 19 deletions brainrender_napari/utils/brainglobe_logo.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,32 +13,31 @@
<\h1>
"""

_docs_links_html = """
<h3>
<p>Atlas visualisation</p>
<p><a href="https://brainglobe.info" style="color:gray;">Website</a></p>
<p><a href="https://brainglobe.info/tutorials/visualise-atlas-napari.html" style="color:gray;">Tutorial</a></p>
<p><a href="https://github.com/brainglobe/brainrender-napari" style="color:gray;">Source</a></p>
<p><a href="https://doi.org/10.7554/eLife.65751" style="color:gray;">Citation</a></p>
<p><small>For help, hover the cursor over the atlases/regions.</small>
</h3>
""" # noqa: E501


def _docs_links_widget():
docs_links_widget = QLabel(_docs_links_html)

def _docs_links_widget(tutorial_file_name: str, parent: QWidget = None):
_docs_links_html = f"""
<h3>
<p>Atlas visualisation</p>
<p><a href="https://brainglobe.info" style="color:gray;">Website</a></p>
<p><a href="https://brainglobe.info/tutorials/{tutorial_file_name}" style="color:gray;">Tutorial</a></p>
<p><a href="https://github.com/brainglobe/brainrender-napari" style="color:gray;">Source</a></p>
<p><a href="https://doi.org/10.7554/eLife.65751" style="color:gray;">Citation</a></p>
<p><small>For help, hover the cursor over the atlases/regions.</small>
</h3>
""" # noqa: E501
docs_links_widget = QLabel(_docs_links_html, parent=parent)
docs_links_widget.setOpenExternalLinks(True)
return docs_links_widget


def _logo_widget():
return QLabel(_logo_html)
def _logo_widget(parent: QWidget = None):
return QLabel(_logo_html, parent=None)


def header_widget(parent: QWidget = None):
def header_widget(tutorial_file_name: str, parent: QWidget = None):
box = QGroupBox(parent)
box.setFlat(True)
box.setLayout(QHBoxLayout())
box.layout().addWidget(_logo_widget())
box.layout().addWidget(_docs_links_widget())
box.layout().addWidget(_logo_widget(parent=box))
box.layout().addWidget(_docs_links_widget(tutorial_file_name, parent=box))
return box
Loading