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

A lightweight UI for medical visualizations #4: 2D Line Slider #1205

Merged
merged 15 commits into from Apr 9, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Binary file added dipy/data/files/test_ui_line_slider_2d.log.gz
Binary file not shown.
Binary file added dipy/data/files/test_ui_line_slider_2d.pkl
Binary file not shown.
35 changes: 35 additions & 0 deletions dipy/viz/tests/test_ui.py
Expand Up @@ -250,9 +250,44 @@ def test_text_actor_2d():
# /TextActor2D


@npt.dec.skipif(not have_vtk or skip_it)
@xvfb_it
def test_ui_line_slider_2d(recording=False):
filename = "test_ui_line_slider_2d"
recording_filename = pjoin(DATA_DIR, filename + ".log.gz")
expected_events_counts_filename = pjoin(DATA_DIR, filename + ".pkl")

line_slider_2d_test = ui.LineSlider2D(initial_value=-2,
min_value=-5, max_value=5)
line_slider_2d_test.set_center((300, 300))

# Assign the counter callback to every possible event.
event_counter = EventCounter()
event_counter.monitor(line_slider_2d_test)

current_size = (600, 600)
show_manager = window.ShowManager(size=current_size,
title="DIPY Line Slider")

show_manager.ren.add(line_slider_2d_test)

if recording:
show_manager.record_events_to_file(recording_filename)
print(list(event_counter.events_counts.items()))
event_counter.save(expected_events_counts_filename)

else:
show_manager.play_events_from_file(recording_filename)
expected = EventCounter.load(expected_events_counts_filename)
event_counter.check_counts(expected)


if __name__ == "__main__":
if len(sys.argv) <= 1 or sys.argv[1] == "test_ui_button_panel":
test_ui_button_panel(recording=True)

if len(sys.argv) <= 1 or sys.argv[1] == "test_ui_textbox":
test_ui_textbox(recording=True)

if len(sys.argv) <= 1 or sys.argv[1] == "test_ui_line_slider_2d":
test_ui_line_slider_2d(recording=True)
287 changes: 286 additions & 1 deletion dipy/viz/ui.py
@@ -1,3 +1,4 @@
from __future__ import division
from _warnings import warn

import numpy as np
Expand Down Expand Up @@ -918,7 +919,7 @@ def position(self, position):
The new position. (x, y) in pixels.

"""
self.actor.SetDisplayPosition(*position)
self.actor.SetPosition(*position)


class TextBox2D(UI):
Expand Down Expand Up @@ -1292,3 +1293,287 @@ def key_press(i_ren, obj, textbox_object):
i_ren.remove_active_prop(textbox_object.actor.get_actor())

i_ren.force_render()


class LineSlider2D(UI):
""" A 2D Line Slider.

A sliding ring on a line with a percentage indicator.

Currently supports:
- A disk on a line (a thin rectangle).
- Setting disk position.

Attributes
----------
line_width : int
Width of the line on which the disk will slide.
inner_radius : int
Inner radius of the disk (ring).
outer_radius : int
Outer radius of the disk.
center : (float, float)
Center of the slider.
length : int
Length of the slider.
slider_line : :class:`vtkActor`
The line on which the slider disk moves.
slider_disk : :class:`vtkActor`
The moving slider disk.
text : :class:`TextActor2D`
The text that shows percentage.

"""
def __init__(self, line_width=5, inner_radius=0, outer_radius=10,
center=(450, 300), length=200, initial_value=50,
min_value=0, max_value=100, text_size=16,
text_template="{value:.1f} ({ratio:.0%})"):
"""
Parameters
----------
line_width : int
Width of the line on which the disk will slide.
inner_radius : int
Inner radius of the disk (ring).
outer_radius : int
Outer radius of the disk.
center : (float, float)
Center of the slider.
length : int
Length of the slider.
initial_value : float
Initial value of the slider.
min_value : float
Minimum value of the slider.
max_value : float
Maximum value of the slider.
text_size : int
Size of the text to display alongside the slider (pt).
text_template : str, callable
If str, text template can contain one or multiple of the
replacement fields: `{value:}`, `{ratio:}`.
If callable, this instance of `:class:LineSlider2D` will be
passed as argument to the text template function.

"""
super(LineSlider2D, self).__init__()

self.length = length
self.min_value = min_value
self.max_value = max_value

self.text_template = text_template

self.line_width = line_width
self.center = center
self.current_state = center[0]
self.left_x_position = center[0] - length / 2
self.right_x_position = center[0] + length / 2
self._ratio = (self.current_state - self.left_x_position) / length

self.slider_line = None
self.slider_disk = None
self.text = None

self.build_actors(inner_radius=inner_radius,
outer_radius=outer_radius, text_size=text_size)

# Setting the disk position will also update everything.
self.value = initial_value
# self.update()

self.handle_events(None)

def build_actors(self, inner_radius, outer_radius, text_size):
""" Builds required actors.

Parameters
----------
inner_radius: int
The inner radius of the sliding disk.
outer_radius: int
The outer radius of the sliding disk.
text_size: int
Size of the text that displays percentage.

"""
# Slider Line
self.slider_line = Rectangle2D(size=(self.length, self.line_width),
center=self.center).actor
self.slider_line.GetProperty().SetColor(1, 0, 0)
# /Slider Line

# Slider Disk
# Create source
disk = vtk.vtkDiskSource()
disk.SetInnerRadius(inner_radius)
disk.SetOuterRadius(outer_radius)
disk.SetRadialResolution(10)
disk.SetCircumferentialResolution(50)
disk.Update()

# Mapper
mapper = vtk.vtkPolyDataMapper2D()
mapper.SetInputConnection(disk.GetOutputPort())

# Actor
self.slider_disk = vtk.vtkActor2D()
self.slider_disk.SetMapper(mapper)
# /Slider Disk

# Slider Text
self.text = TextActor2D()
self.text.position = (self.left_x_position - 50, self.center[1] - 10)
self.text.font_size = text_size
# /Slider Text

def get_actors(self):
""" Returns the actors that compose this UI component.

"""
return [self.slider_line, self.slider_disk, self.text.get_actor()]

def set_position(self, position):
""" Sets the disk's position.

Parameters
----------
position : (float, float)
The absolute position of the disk (x, y).

"""
x_position = position[0]

if x_position < self.center[0] - self.length/2:
x_position = self.center[0] - self.length/2

if x_position > self.center[0] + self.length/2:
x_position = self.center[0] + self.length/2

self.current_state = x_position
self.update()

@property
def value(self):
return self._value

@value.setter
def value(self, value):
value_range = self.max_value - self.min_value
self.ratio = (value - self.min_value) / value_range

@property
def ratio(self):
return self._ratio

@ratio.setter
def ratio(self, ratio):
position_x = self.left_x_position + ratio*self.length
self.set_position((position_x, None))

def format_text(self):
""" Returns formatted text to display along the slider. """
if callable(self.text_template):
return self.text_template(self)

return self.text_template.format(ratio=self.ratio, value=self.value)

def update(self):
""" Updates the slider. """

# Compute the ratio determined by the position of the slider disk.
length = float(self.right_x_position - self.left_x_position)
assert length == self.length
self._ratio = (self.current_state - self.left_x_position) / length

# Compute the selected value considering min_value and max_value.
value_range = self.max_value - self.min_value
self._value = self.min_value + self.ratio*value_range

# Update text disk actor.
self.slider_disk.SetPosition(self.current_state, self.center[1])

# Update text.
text = self.format_text()
self.text.message = text
offset_x = 8 * len(text) / 2.
offset_y = 30
self.text.position = (self.current_state - offset_x,
self.center[1] - offset_y)

def set_center(self, position):
""" Sets the center of the slider to position.

Parameters
----------
position : (float, float)
The new center of the whole slider (x, y).

"""
self.slider_line.SetPosition(position[0] - self.length / 2,
position[1] - self.line_width / 2)

x_change = position[0] - self.center[0]
self.current_state += x_change
self.center = position
self.left_x_position = position[0] - self.length / 2
self.right_x_position = position[0] + self.length / 2
self.set_position((self.current_state, self.center[1]))

@staticmethod
def line_click_callback(i_ren, obj, slider):
""" Update disk position and grab the focus.

Parameters
----------
i_ren : :class:`CustomInteractorStyle`
obj : :class:`vtkActor`
The picked actor
slider : :class:`LineSlider2D`

"""
position = i_ren.event.position
slider.set_position(position)
i_ren.force_render()
i_ren.event.abort() # Stop propagating the event.

@staticmethod
def disk_press_callback(i_ren, obj, slider):
""" Only need to grab the focus.

Parameters
----------
i_ren : :class:`CustomInteractorStyle`
obj : :class:`vtkActor`
The picked actor
slider : :class:`LineSlider2D`

"""
i_ren.event.abort() # Stop propagating the event.

@staticmethod
def disk_move_callback(i_ren, obj, slider):
""" Actual disk movement.

Parameters
----------
i_ren : :class:`CustomInteractorStyle`
obj : :class:`vtkActor`
The picked actor
slider : :class:`LineSlider2D`

"""
position = i_ren.event.position
slider.set_position(position)
i_ren.force_render()
i_ren.event.abort() # Stop propagating the event.

def handle_events(self, actor):
""" Handle all events for the LineSlider.
Base method needs to be overridden due to multiple actors.

"""
self.add_callback(self.slider_line, "LeftButtonPressEvent", self.line_click_callback)
self.add_callback(self.slider_disk, "LeftButtonPressEvent", self.disk_press_callback)
self.add_callback(self.slider_disk, "MouseMoveEvent", self.disk_move_callback)
self.add_callback(self.slider_line, "MouseMoveEvent", self.disk_move_callback)
1 change: 1 addition & 0 deletions doc/examples/valid_examples.txt
Expand Up @@ -51,3 +51,4 @@
workflow_creation.py
combined_workflow_creation.py
viz_surfaces.py
viz_ui.py