Skip to content

Commit

Permalink
Fix #759. Fix several cases where index sliders were incorrect.
Browse files Browse the repository at this point in the history
  • Loading branch information
cmeyer committed Nov 2, 2023
1 parent 3717153 commit 91ad402
Show file tree
Hide file tree
Showing 3 changed files with 187 additions and 9 deletions.
87 changes: 78 additions & 9 deletions nion/swift/DisplayPanel.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

import numpy.typing

from nion.data import DataAndMetadata
from nion.data import Image
from nion.swift import DataItemThumbnailWidget
from nion.swift import DataPanel
Expand All @@ -24,7 +25,6 @@
from nion.swift import ImageCanvasItem
from nion.swift import LinePlotCanvasItem
from nion.swift import MimeTypes
from nion.swift import NotificationDialog
from nion.swift import Panel
from nion.swift import Thumbnails
from nion.swift import Undo
Expand All @@ -47,6 +47,7 @@
from nion.utils import ListModel
from nion.utils import Model
from nion.utils import Process
from nion.utils import ReferenceCounting
from nion.utils import Selection
from nion.utils import Stream

Expand Down Expand Up @@ -470,7 +471,10 @@ def __init__(self, display_item_value_stream: Stream.ValueStream[DisplayItem.Dis
self.__display_item_item_inserted_listener: typing.Optional[Event.EventListener] = None
self.__display_item_item_removed_listener: typing.Optional[Event.EventListener] = None
self.__stream_listener = self.__stream.value_stream.listen(self.__update_display_item)
self.__display_item: typing.Optional[DisplayItem.DisplayItem] = self.__stream.value
self.__display_item: typing.Optional[DisplayItem.DisplayItem] = None
# display item is initialized to none and updated with update display item, which does a check
# to see if the display changed. if it did, then the display item listeners are updated.
self.__update_display_item(self.__stream.value)

def about_to_delete(self) -> None:
if self.__display_item_item_inserted_listener:
Expand Down Expand Up @@ -507,6 +511,27 @@ def __handle_item_changed(self, key: str, value: typing.Any, index: int) -> None
self.value = self.__display_item.display_data_channel


class DisplayDataChannelDataDescriptorStream(Stream.ValueStream[DataAndMetadata.DataDescriptor]):
def __init__(self, display_data_channel_stream: Stream.AbstractStream[DisplayItem.DisplayDataChannel]) -> None:
super().__init__()
self.__display_data_channel_stream = display_data_channel_stream
self.__display_data_channel_stream_listener = self.__display_data_channel_stream.value_stream.listen(ReferenceCounting.weak_partial(DisplayDataChannelDataDescriptorStream.__update_display_data_channel, self))
self.__data_item_listener: typing.Optional[Event.EventListener] = None
self.__update_display_data_channel(self.__display_data_channel_stream.value)

def __update_display_data_channel(self, display_data_channel: typing.Optional[DisplayItem.DisplayDataChannel]) -> None:
if display_data_channel and (data_item := display_data_channel.data_item):
self.__data_item_listener = data_item.property_changed_event.listen(ReferenceCounting.weak_partial(DisplayDataChannelDataDescriptorStream.__handle_data_item_property_changed, self, data_item))
self.value = data_item.data_metadata.data_descriptor if data_item.data_metadata else None
else:
self.__data_item_listener = None
self.value = None

def __handle_data_item_property_changed(self, data_item: DataItem.DataItem, property_name: str) -> None:
if property_name in ("collection_dimension_count", "is_sequence", "data_modified"):
self.value = data_item.data_metadata.data_descriptor if data_item.data_metadata else None


class IndexValueAdapter(typing.Protocol):
def get_index_value_stream(self, display_data_channel_value_stream: Stream.AbstractStream[DisplayItem.DisplayDataChannel]) -> Stream.AbstractStream[float]: ...
def get_index_value(self, display_data_channel: DisplayItem.DisplayDataChannel) -> float: ...
Expand All @@ -521,9 +546,32 @@ def __init__(self, document_controller: DocumentController.DocumentController, c
self.__collection_index = collection_index

def get_index_value_stream(self, display_data_channel_value_stream: Stream.AbstractStream[DisplayItem.DisplayDataChannel]) -> Stream.AbstractStream[float]:
display_data_channel_value_stream = Stream.OptionalStream(display_data_channel_value_stream, lambda x: x is not None and self.__collection_index < x.collection_rank and x.datum_rank == 2)
# mypy bug: typing on next line doesn't recognize OptionalStream[DisplayDataChannel] as a AbstractStream[Observable]
return Stream.MapStream(Stream.PropertyChangedEventStream(display_data_channel_value_stream, "collection_index"), lambda x: x[self.__collection_index] if x is not None else None) # type: ignore
# given a display data channel stream, return a stream of the collection index values
# an additional complexity is that if the underlying data changes, output stream needs to be updated
# to accomplish this, we combine the display data channel stream with a stream of the data descriptor
# to give a stream of tuples of (data_descriptor, display_data_channel) which we use to filter the stream
# and then map the tuple back down to the display_data_channel from which we extract the sequence index.

data_descriptor_value_stream = DisplayDataChannelDataDescriptorStream(display_data_channel_value_stream)
combined_value_stream = Stream.CombineLatestStream[typing.Any, typing.Any]([data_descriptor_value_stream, display_data_channel_value_stream])

def filter_combined_stream(tuple_value: typing.Optional[typing.Tuple[typing.Optional[DataAndMetadata.DataDescriptor], typing.Optional[DisplayItem.DisplayDataChannel]]]) -> bool:
if tuple_value is not None:
data_descriptor, display_data_channel = tuple_value
return data_descriptor is not None and display_data_channel is not None and self.__collection_index < display_data_channel.collection_rank and display_data_channel.datum_rank == 2
return False

optional_display_data_channel_value_stream = Stream.OptionalStream(combined_value_stream, filter_combined_stream)

def select_display_data_channel_from_tuple(tuple_value: typing.Optional[typing.Tuple[typing.Optional[DataAndMetadata.DataDescriptor], typing.Optional[DisplayItem.DisplayDataChannel]]]) -> typing.Optional[DisplayItem.DisplayDataChannel]:
return tuple_value[1] if tuple_value is not None else None

filtered_display_data_channel_value_stream = Stream.MapStream(optional_display_data_channel_value_stream, select_display_data_channel_from_tuple)

def select_collection_index(collection_index: typing.Optional[typing.Tuple[int, ...]]) -> typing.Optional[int]:
return collection_index[self.__collection_index] if collection_index is not None else None

return Stream.MapStream(Stream.PropertyChangedEventStream(filtered_display_data_channel_value_stream, "collection_index"), select_collection_index)

def get_index_value(self, display_data_channel: DisplayItem.DisplayDataChannel) -> float:
index = self.__collection_index + (1 if display_data_channel.is_sequence else 0)
Expand Down Expand Up @@ -568,12 +616,32 @@ def __init__(self, document_controller: DocumentController.DocumentController) -
self.__document_controller = document_controller

def get_index_value_stream(self, display_data_channel_value_stream: Stream.AbstractStream[DisplayItem.DisplayDataChannel]) -> Stream.AbstractStream[float]:
display_data_channel_value_stream = Stream.OptionalStream(display_data_channel_value_stream, lambda x: x is not None and x.is_sequence)
return Stream.PropertyChangedEventStream(display_data_channel_value_stream, "sequence_index")
# given a display data channel stream, return a stream of the sequence index value
# an additional complexity is that if the underlying data changes, output stream needs to be updated
# to accomplish this, we combine the display data channel stream with a stream of the data descriptor
# to give a stream of tuples of (data_descriptor, display_data_channel) which we use to filter the stream
# and then map the tuple back down to the display_data_channel from which we extract the sequence index.

data_descriptor_value_stream = DisplayDataChannelDataDescriptorStream(display_data_channel_value_stream)
combined_value_stream = Stream.CombineLatestStream[typing.Any, typing.Any]([data_descriptor_value_stream, display_data_channel_value_stream])

def filter_combined_stream(tuple_value: typing.Optional[typing.Tuple[typing.Optional[DataAndMetadata.DataDescriptor], typing.Optional[DisplayItem.DisplayDataChannel]]]) -> bool:
if tuple_value is not None:
data_descriptor, display_data_channel = tuple_value
return data_descriptor is not None and display_data_channel is not None and display_data_channel.is_sequence
return False

optional_display_data_channel_value_stream = Stream.OptionalStream(combined_value_stream, filter_combined_stream)

def select_display_data_channel_from_tuple(tuple_value: typing.Optional[typing.Tuple[typing.Optional[DataAndMetadata.DataDescriptor], typing.Optional[DisplayItem.DisplayDataChannel]]]) -> typing.Optional[DisplayItem.DisplayDataChannel]:
return tuple_value[1] if tuple_value is not None else None

filtered_display_data_channel_value_stream = Stream.MapStream(optional_display_data_channel_value_stream, select_display_data_channel_from_tuple)
return Stream.PropertyChangedEventStream(filtered_display_data_channel_value_stream, "sequence_index")

def get_index_value(self, display_data_channel: DisplayItem.DisplayDataChannel) -> float:
sequence_length = display_data_channel.dimensional_shape[0] if display_data_channel.dimensional_shape is not None else 0
return display_data_channel.sequence_index / (sequence_length - 1)
return display_data_channel.sequence_index / (sequence_length - 1) if sequence_length > 1 else 0.0

def get_index_str(self, display_data_channel: DisplayItem.DisplayDataChannel) -> str:
return str(display_data_channel.sequence_index)
Expand Down Expand Up @@ -636,6 +704,7 @@ def __init__(self, title: str, display_item_value_stream: Stream.ValueStream[Dis
self.__stream_action = Stream.ValueStreamAction[typing.Tuple[DisplayItem.DisplayItem, DisplayItem.DisplayDataChannel, int]](combined_stream, self.__index_changed)
self.__play_button_handler = play_button_handler
self.__play_button_model = play_button_model
self.__index_changed(combined_stream.value)

def close(self) -> None:
self.__stream_action.close()
Expand All @@ -647,7 +716,7 @@ def close(self) -> None:
self.__display_item_value_stream = typing.cast(typing.Any, None)
super().close()

def __index_changed(self, args: typing.Optional[typing.Tuple[DisplayItem.DisplayItem, DisplayItem.DisplayDataChannel, int]]) -> None:
def __index_changed(self, args: typing.Optional[typing.Tuple[DisplayItem.DisplayItem, DisplayItem.DisplayDataChannel, typing.Optional[int]]]) -> None:
display_item, display_data_channel, index_value = args if args else (None, None, 0)
if display_data_channel and index_value is not None:
if not self.__slider_row.canvas_items:
Expand Down
107 changes: 107 additions & 0 deletions nion/swift/test/DisplayPanel_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -2881,6 +2881,113 @@ def handle_title_changed(title: str) -> None:
data_item.title = "green"
self.assertTrue(title_changed)

def test_index_sliders_update_when_data_created_later(self):
with TestContext.create_memory_context() as test_context:
document_controller = test_context.create_document_controller()
document_model = document_controller.document_model
data_item = DataItem.DataItem()
document_model.append_data_item(data_item)
display_item = document_model.get_display_item_for_data_item(data_item)
display_panel = document_controller.selected_display_panel
display_panel.set_display_panel_display_item(display_item)
display_panel.display_canvas_item.layout_immediate(Geometry.IntSize(height=480, width=640))
data_item.set_data_and_metadata(DataAndMetadata.new_data_and_metadata(numpy.zeros((8, 8)), data_descriptor=DataAndMetadata.DataDescriptor(False, 0, 2)))
document_controller.periodic()
sequence_slider_row = display_panel.display_canvas_item.canvas_items[2].canvas_items[-3].canvas_items[1]
c0_slider_row = display_panel.display_canvas_item.canvas_items[2].canvas_items[-2].canvas_items[1]
c1_slider_row = display_panel.display_canvas_item.canvas_items[2].canvas_items[-1].canvas_items[1]
self.assertEqual(0, len(sequence_slider_row.canvas_items))
self.assertEqual(0, len(c0_slider_row.canvas_items))
self.assertEqual(0, len(c1_slider_row.canvas_items))
data_item.set_data_and_metadata(DataAndMetadata.new_data_and_metadata(numpy.zeros((8, 8, 8)), data_descriptor=DataAndMetadata.DataDescriptor(True, 0, 2)))
document_controller.periodic()
self.assertNotEqual(0, len(sequence_slider_row.canvas_items))
self.assertEqual(0, len(c0_slider_row.canvas_items))
self.assertEqual(0, len(c1_slider_row.canvas_items))

def test_index_sliders_update_when_data_changed(self):
with TestContext.create_memory_context() as test_context:
document_controller = test_context.create_document_controller()
document_model = document_controller.document_model
data_item = DataItem.DataItem(numpy.zeros((8, 8)))
document_model.append_data_item(data_item)
display_item = document_model.get_display_item_for_data_item(data_item)
display_panel = document_controller.selected_display_panel
display_panel.set_display_panel_display_item(display_item)
display_panel.display_canvas_item.layout_immediate(Geometry.IntSize(height=480, width=640))
sequence_slider_row = display_panel.display_canvas_item.canvas_items[2].canvas_items[-3].canvas_items[1]
c0_slider_row = display_panel.display_canvas_item.canvas_items[2].canvas_items[-2].canvas_items[1]
c1_slider_row = display_panel.display_canvas_item.canvas_items[2].canvas_items[-1].canvas_items[1]
self.assertEqual(0, len(sequence_slider_row.canvas_items))
self.assertEqual(0, len(c0_slider_row.canvas_items))
self.assertEqual(0, len(c1_slider_row.canvas_items))
data_item.set_data_and_metadata(DataAndMetadata.new_data_and_metadata(numpy.zeros((8, 8, 8)), data_descriptor=DataAndMetadata.DataDescriptor(True, 0, 2)))
document_controller.periodic()
self.assertNotEqual(0, len(sequence_slider_row.canvas_items))
self.assertEqual(0, len(c0_slider_row.canvas_items))
self.assertEqual(0, len(c1_slider_row.canvas_items))
data_item.set_data_and_metadata(DataAndMetadata.new_data_and_metadata(numpy.zeros((4, 4, 4, 4)), data_descriptor=DataAndMetadata.DataDescriptor(False, 2, 2)))
document_controller.periodic()
self.assertEqual(0, len(sequence_slider_row.canvas_items))
self.assertNotEqual(0, len(c0_slider_row.canvas_items))
self.assertNotEqual(0, len(c1_slider_row.canvas_items))
data_item.set_data_and_metadata(DataAndMetadata.new_data_and_metadata(numpy.zeros((2, 4, 4, 4, 4)), data_descriptor=DataAndMetadata.DataDescriptor(True, 2, 2)))
document_controller.periodic()
self.assertNotEqual(0, len(sequence_slider_row.canvas_items))
self.assertNotEqual(0, len(c0_slider_row.canvas_items))
self.assertNotEqual(0, len(c1_slider_row.canvas_items))
data_item.set_data_and_metadata(DataAndMetadata.new_data_and_metadata(numpy.zeros((8, 8)), data_descriptor=DataAndMetadata.DataDescriptor(False, 0, 2)))
document_controller.periodic()
self.assertEqual(0, len(sequence_slider_row.canvas_items))
self.assertEqual(0, len(c0_slider_row.canvas_items))
self.assertEqual(0, len(c1_slider_row.canvas_items))

def test_index_sliders_update_when_indexes_changed(self):
with TestContext.create_memory_context() as test_context:
document_controller = test_context.create_document_controller()
document_model = document_controller.document_model
data_item = DataItem.DataItem(numpy.zeros((8, 8)))
document_model.append_data_item(data_item)
display_item = document_model.get_display_item_for_data_item(data_item)
display_panel = document_controller.selected_display_panel
display_panel.set_display_panel_display_item(display_item)
display_panel.display_canvas_item.layout_immediate(Geometry.IntSize(height=480, width=640))
sequence_slider_row = display_panel.display_canvas_item.canvas_items[2].canvas_items[-3].canvas_items[1]
c0_slider_row = display_panel.display_canvas_item.canvas_items[2].canvas_items[-2].canvas_items[1]
c1_slider_row = display_panel.display_canvas_item.canvas_items[2].canvas_items[-1].canvas_items[1]
data_item.set_data_and_metadata(DataAndMetadata.new_data_and_metadata(numpy.zeros((2, 4, 4, 4, 4)), data_descriptor=DataAndMetadata.DataDescriptor(True, 2, 2)))
document_controller.periodic()
self.assertEqual(0.0, sequence_slider_row.canvas_items[3].value)
self.assertEqual(0.0, c0_slider_row.canvas_items[1].value)
self.assertEqual(0.0, c1_slider_row.canvas_items[1].value)
display_item.display_data_channel.sequence_index = 1
self.assertEqual(1.0, sequence_slider_row.canvas_items[3].value)
self.assertEqual(0.0, c0_slider_row.canvas_items[1].value)
self.assertEqual(0.0, c1_slider_row.canvas_items[1].value)
display_item.display_data_channel.collection_index = (2, 3)
self.assertEqual(1.0, sequence_slider_row.canvas_items[3].value)
self.assertAlmostEqual(2.0/3.0, c0_slider_row.canvas_items[1].value)
self.assertEqual(1.0, c1_slider_row.canvas_items[1].value)

def test_index_sliders_update_when_data_created_with_display(self):
with TestContext.create_memory_context() as test_context:
document_controller = test_context.create_document_controller()
document_model = document_controller.document_model
data_item = DataItem.DataItem()
document_model.append_data_item(data_item)
display_item = document_model.get_display_item_for_data_item(data_item)
display_panel = document_controller.selected_display_panel
display_panel.set_display_panel_display_item(display_item)
display_panel.display_canvas_item.layout_immediate(Geometry.IntSize(height=480, width=640))
data_item.set_data_and_metadata(DataAndMetadata.new_data_and_metadata(numpy.zeros((1, 8, 8)), data_descriptor=DataAndMetadata.DataDescriptor(True, 0, 2)))
document_controller.periodic()
sequence_slider_row = display_panel.display_canvas_item.canvas_items[2].canvas_items[-3].canvas_items[1]
c0_slider_row = display_panel.display_canvas_item.canvas_items[2].canvas_items[-2].canvas_items[1]
c1_slider_row = display_panel.display_canvas_item.canvas_items[2].canvas_items[-1].canvas_items[1]
self.assertNotEqual(0, len(sequence_slider_row.canvas_items))
self.assertEqual(0, len(c0_slider_row.canvas_items))
self.assertEqual(0, len(c1_slider_row.canvas_items))


if __name__ == '__main__':
logging.getLogger().setLevel(logging.DEBUG)
Expand Down
2 changes: 2 additions & 0 deletions setup.cfg
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
[metadata]
name = nionswift
version = 16.9.1
# nionutils 0.4.10 (optional stream value fix)
# niondata 15.6.1 (timestamp propagation)
author = Nion Software
author_email = [email protected]
description = Nion Swift: Scientific Image Processing.
Expand Down

0 comments on commit 91ad402

Please sign in to comment.