datanavigator 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- datanavigator/__init__.py +133 -0
- datanavigator/__version__.py +1 -0
- datanavigator/_config.py +53 -0
- datanavigator/assets.py +362 -0
- datanavigator/components.py +512 -0
- datanavigator/core.py +397 -0
- datanavigator/events.py +833 -0
- datanavigator/examples.py +291 -0
- datanavigator/opticalflow.py +173 -0
- datanavigator/plots.py +123 -0
- datanavigator/pointtracking.py +1667 -0
- datanavigator/signals.py +86 -0
- datanavigator/utils.py +409 -0
- datanavigator/videos.py +351 -0
- datanavigator-1.0.0.dist-info/LICENSE +9 -0
- datanavigator-1.0.0.dist-info/METADATA +99 -0
- datanavigator-1.0.0.dist-info/RECORD +18 -0
- datanavigator-1.0.0.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
r"""
|
|
2
|
+
Interactive data visualization for signals, videos, and complex data objects.
|
|
3
|
+
|
|
4
|
+
Browsers
|
|
5
|
+
|
|
6
|
+
- :py:class:`GenericBrowser`: Generic class to browse data. Meant to be extended.
|
|
7
|
+
- :py:class:`SignalBrowser`: Browse an array of pysampled.Data elements, or 2D arrays.
|
|
8
|
+
- :py:class:`PlotBrowser`: Scroll through an array of complex data where a plotting function is defined for each element.
|
|
9
|
+
- :py:class:`VideoBrowser`: Scroll through the frames of a video.
|
|
10
|
+
- :py:class:`VideoPlotBrowser`: Browse through video and 1D signals synced to the video side by side.
|
|
11
|
+
- :py:class:`ComponentBrowser`: Browse signals (e.g., from periodic motion) as scatterplots of components (e.g., from UMAP, PCA).
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
Point tracking
|
|
15
|
+
|
|
16
|
+
- :py:class:`Video`: Extended VideoReader class with additional functionalities (helper for VideoPointAnnotator).
|
|
17
|
+
- :py:class:`VideoAnnotation`: Manage one point annotation layer in a video.
|
|
18
|
+
- :py:class:`VideoAnnotations`: Manager for multiple video annotation layers.
|
|
19
|
+
- :py:class:`VideoPointAnnotator`: Annotate points in a video.
|
|
20
|
+
|
|
21
|
+
Optical flow
|
|
22
|
+
|
|
23
|
+
- :py:func:`lucas_kanade`: Track points in a video using the Lucas-Kanade algorithm.
|
|
24
|
+
- :py:func:`lucas_kanade_rstc`: Track points in a video using Lucas-Kanade with reverse sigmoid tracking correction.
|
|
25
|
+
- :py:func:`test_lucas_kanade_rstc`: Test function for Lucas-Kanade with reverse sigmoid tracking correction.
|
|
26
|
+
|
|
27
|
+
Assets
|
|
28
|
+
|
|
29
|
+
- :py:class:`Button`: Custom button widget with a 'name' state.
|
|
30
|
+
- :py:class:`StateButton`: Button widget that stores a number/coordinate state.
|
|
31
|
+
- :py:class:`ToggleButton`: Button widget with a toggle state.
|
|
32
|
+
- :py:class:`Selector`: Select points in a plot using the lasso selection widget.
|
|
33
|
+
- :py:class:`StateVariable`: Manage state variables with multiple states.
|
|
34
|
+
- :py:class:`EventData`: Manage the data from one event type in one trial.
|
|
35
|
+
- :py:class:`Event`: Manage selection of a sequence of events.
|
|
36
|
+
|
|
37
|
+
Assetcontainers
|
|
38
|
+
|
|
39
|
+
- :py:class:`AssetContainer`: Container for managing assets such as buttons, memory slots, etc.
|
|
40
|
+
- :py:class:`Buttons`: Manager for buttons in a matplotlib figure or GUI.
|
|
41
|
+
- :py:class:`Selectors`: Manager for selector objects for picking points on line2D objects.
|
|
42
|
+
- :py:class:`MemorySlots`: Manager for memory slots to store and navigate positions.
|
|
43
|
+
- :py:class:`StateVariables`: Manager for state variables.
|
|
44
|
+
- :py:class:`Events`: Manager for event objects.
|
|
45
|
+
"""
|
|
46
|
+
import os
|
|
47
|
+
import sys
|
|
48
|
+
import shutil
|
|
49
|
+
|
|
50
|
+
from .__version__ import __version__
|
|
51
|
+
|
|
52
|
+
from ._config import (
|
|
53
|
+
get_cache_folder,
|
|
54
|
+
get_clip_folder,
|
|
55
|
+
set_cache_folder,
|
|
56
|
+
set_clip_folder,
|
|
57
|
+
)
|
|
58
|
+
from .assets import (
|
|
59
|
+
AssetContainer,
|
|
60
|
+
Button,
|
|
61
|
+
Buttons,
|
|
62
|
+
MemorySlots,
|
|
63
|
+
Selector,
|
|
64
|
+
Selectors,
|
|
65
|
+
StateButton,
|
|
66
|
+
StateVariable,
|
|
67
|
+
StateVariables,
|
|
68
|
+
ToggleButton,
|
|
69
|
+
)
|
|
70
|
+
from .events import portion, Event, EventData, Events
|
|
71
|
+
|
|
72
|
+
from .core import GenericBrowser
|
|
73
|
+
from .plots import PlotBrowser
|
|
74
|
+
from .signals import SignalBrowser
|
|
75
|
+
from .videos import VideoBrowser, VideoPlotBrowser
|
|
76
|
+
from .components import ComponentBrowser
|
|
77
|
+
|
|
78
|
+
from .opticalflow import lucas_kanade, lucas_kanade_rstc
|
|
79
|
+
from .pointtracking import VideoAnnotation, VideoAnnotations, VideoPointAnnotator
|
|
80
|
+
|
|
81
|
+
from .utils import (
|
|
82
|
+
TextView,
|
|
83
|
+
Video,
|
|
84
|
+
get_palette,
|
|
85
|
+
is_path_exists_or_creatable,
|
|
86
|
+
is_video,
|
|
87
|
+
ticks_from_times,
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
from .examples import (
|
|
91
|
+
get_example_video,
|
|
92
|
+
EventPickerDemo,
|
|
93
|
+
ButtonDemo,
|
|
94
|
+
SelectorDemo,
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _check_ffmpeg():
|
|
99
|
+
def check_command(command):
|
|
100
|
+
"""Check if a command is available in the system's PATH."""
|
|
101
|
+
return shutil.which(command) is not None
|
|
102
|
+
|
|
103
|
+
def print_install_instructions():
|
|
104
|
+
"""Print installation instructions for ffmpeg and ffprobe."""
|
|
105
|
+
if sys.platform.startswith("win"):
|
|
106
|
+
print("\nFFmpeg is not installed or not in PATH.")
|
|
107
|
+
print("Download it from: https://ffmpeg.org/download.html")
|
|
108
|
+
print("After installation, add FFmpeg's 'bin' folder to the system PATH.")
|
|
109
|
+
else:
|
|
110
|
+
print("\nFFmpeg is not installed or not in PATH.")
|
|
111
|
+
print("On Debian/Ubuntu, install it with: sudo apt install ffmpeg")
|
|
112
|
+
print("On macOS, install it with: brew install ffmpeg")
|
|
113
|
+
print("On Fedora, install it with: sudo dnf install ffmpeg")
|
|
114
|
+
print("On Arch Linux, install it with: sudo pacman -S ffmpeg")
|
|
115
|
+
|
|
116
|
+
# Check if ffmpeg and ffprobe are available
|
|
117
|
+
ffmpeg_found = check_command("ffmpeg")
|
|
118
|
+
|
|
119
|
+
if not ffmpeg_found:
|
|
120
|
+
print("Cound not find ffmpeg.")
|
|
121
|
+
print_install_instructions()
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _check_clip_folder():
|
|
125
|
+
if not os.path.exists(get_clip_folder()):
|
|
126
|
+
folder = os.getcwd()
|
|
127
|
+
print(f"Using the current working directory-{folder}-for storing video clips.")
|
|
128
|
+
set_clip_folder(folder)
|
|
129
|
+
print("To change, use datanavigator.set_clip_folder(<folder_name>)")
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
_check_ffmpeg()
|
|
133
|
+
_check_clip_folder()
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "1.0.0"
|
datanavigator/_config.py
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""
|
|
2
|
+
This module provides functions to set and get the paths for the clip and cache folders.
|
|
3
|
+
|
|
4
|
+
The clip folder is used to store video clips, for example, when using VideoBrowser.
|
|
5
|
+
The cache folder is used by the :py:mod:`datanavigator.examples` to write json file containing marked events.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import os
|
|
9
|
+
import logging
|
|
10
|
+
|
|
11
|
+
# Configure logging
|
|
12
|
+
logging.basicConfig(level=logging.INFO)
|
|
13
|
+
|
|
14
|
+
# Use environment variables for default paths
|
|
15
|
+
CLIP_FOLDER: str = os.getenv("CLIP_FOLDER", "C:\\data\\_clipcollection")
|
|
16
|
+
CACHE_FOLDER: str = os.getenv("CACHE_FOLDER", "C:\\data\\_cache")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def set_clip_folder(folder: str) -> None:
|
|
20
|
+
"""Set the path for storing video clips."""
|
|
21
|
+
if not os.path.exists(folder):
|
|
22
|
+
raise ValueError(f"The provided folder path does not exist: {folder}")
|
|
23
|
+
|
|
24
|
+
global CLIP_FOLDER
|
|
25
|
+
CLIP_FOLDER = folder
|
|
26
|
+
logging.info(f"Clip folder set to: {CLIP_FOLDER}")
|
|
27
|
+
|
|
28
|
+
global CACHE_FOLDER
|
|
29
|
+
if not os.path.exists(CACHE_FOLDER):
|
|
30
|
+
logging.info(
|
|
31
|
+
"Setting the cache folder to be the same as the clip folder. To change, use set_cache_folder(<folder_name>)."
|
|
32
|
+
)
|
|
33
|
+
CACHE_FOLDER = folder
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def get_clip_folder() -> str:
|
|
37
|
+
"""Get the current path of the clip folder."""
|
|
38
|
+
return CLIP_FOLDER
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def set_cache_folder(folder: str) -> None:
|
|
42
|
+
"""Set the path for the cache folder."""
|
|
43
|
+
if not os.path.exists(folder):
|
|
44
|
+
raise ValueError(f"The provided folder path does not exist: {folder}")
|
|
45
|
+
|
|
46
|
+
global CACHE_FOLDER
|
|
47
|
+
CACHE_FOLDER = folder
|
|
48
|
+
logging.info(f"Cache folder set to: {CACHE_FOLDER}")
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def get_cache_folder() -> str:
|
|
52
|
+
"""Get the current path of the cache folder."""
|
|
53
|
+
return CACHE_FOLDER
|
datanavigator/assets.py
ADDED
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
"""
|
|
2
|
+
This module provides classes and functions for managing various assets such as buttons, selectors, and state variables in a graphical user interface.
|
|
3
|
+
|
|
4
|
+
Classes:
|
|
5
|
+
Button - Add a 'name' state to a matplotlib widget button.
|
|
6
|
+
StateButton - Store a number/coordinate in a button.
|
|
7
|
+
ToggleButton - Add a toggle button to a matplotlib figure.
|
|
8
|
+
Selector - Select points in a plot using the lasso selection widget.
|
|
9
|
+
StateVariable - Manage state variables with multiple states.
|
|
10
|
+
|
|
11
|
+
AssetContainer - Container for managing assets such as buttons, memory slots, etc.
|
|
12
|
+
|
|
13
|
+
Buttons - Manager for buttons in a matplotlib figure or GUI.
|
|
14
|
+
Selectors - Manager for selector objects for picking points on line2D objects.
|
|
15
|
+
MemorySlots - Manager for memory slots to store and navigate positions.
|
|
16
|
+
StateVariables - Manager for state variables.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import numpy as np
|
|
22
|
+
from matplotlib import lines as mlines
|
|
23
|
+
from matplotlib import pyplot as plt
|
|
24
|
+
from matplotlib.path import Path as mPath
|
|
25
|
+
from matplotlib.widgets import Button as ButtonWidget
|
|
26
|
+
from matplotlib.widgets import LassoSelector as LassoSelectorWidget
|
|
27
|
+
from typing import Any, Callable, List, Optional, Union
|
|
28
|
+
|
|
29
|
+
from .utils import TextView
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class Button(ButtonWidget):
|
|
33
|
+
"""Add a 'name' state to a matplotlib widget button."""
|
|
34
|
+
|
|
35
|
+
def __init__(self, ax, name: str, **kwargs) -> None:
|
|
36
|
+
super().__init__(ax, name, **kwargs)
|
|
37
|
+
self.name = name
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class StateButton(Button):
|
|
41
|
+
"""Store a number/coordinate in a button."""
|
|
42
|
+
|
|
43
|
+
def __init__(self, ax, name: str, start_state: Any, **kwargs) -> None:
|
|
44
|
+
super().__init__(ax, name, **kwargs)
|
|
45
|
+
self.state = start_state # stores something in the state
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class ToggleButton(StateButton):
|
|
49
|
+
"""
|
|
50
|
+
Add a toggle button to a matplotlib figure.
|
|
51
|
+
|
|
52
|
+
For example usage, see PlotBrowser.
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
def __init__(self, ax, name: str, start_state: bool = True, **kwargs) -> None:
|
|
56
|
+
super().__init__(ax, name, start_state, **kwargs)
|
|
57
|
+
self.on_clicked(self.toggle)
|
|
58
|
+
self.set_text()
|
|
59
|
+
|
|
60
|
+
def set_text(self) -> None:
|
|
61
|
+
"""Set the text of the toggle button."""
|
|
62
|
+
self.label._text = f"{self.name}={self.state}"
|
|
63
|
+
|
|
64
|
+
def toggle(self, event=None) -> None:
|
|
65
|
+
"""Toggle the state of the button."""
|
|
66
|
+
self.state = not self.state
|
|
67
|
+
self.set_text()
|
|
68
|
+
|
|
69
|
+
def set_state(self, state: bool) -> None:
|
|
70
|
+
"""Set the state of the button."""
|
|
71
|
+
assert isinstance(state, bool)
|
|
72
|
+
self.state = state
|
|
73
|
+
self.set_text()
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class Selector:
|
|
77
|
+
"""
|
|
78
|
+
Select points in a plot using the lasso selection widget.
|
|
79
|
+
|
|
80
|
+
Indices of selected points are stored in self.sel.
|
|
81
|
+
|
|
82
|
+
Example:
|
|
83
|
+
f, ax = plt.subplots(1, 1)
|
|
84
|
+
ph, = ax.plot(np.random.rand(20))
|
|
85
|
+
plt.show(block=False)
|
|
86
|
+
ls = gui.Lasso(ph)
|
|
87
|
+
ls.start()
|
|
88
|
+
-- play around with selecting points --
|
|
89
|
+
ls.stop() -> disconnects the events
|
|
90
|
+
"""
|
|
91
|
+
|
|
92
|
+
def __init__(self, plot_handle: mlines.Line2D) -> None:
|
|
93
|
+
"""Initialize the selector with a plot handle."""
|
|
94
|
+
assert isinstance(plot_handle, mlines.Line2D)
|
|
95
|
+
self.plot_handle = plot_handle
|
|
96
|
+
self.xdata, self.ydata = plot_handle.get_data()
|
|
97
|
+
self.ax = plot_handle.axes
|
|
98
|
+
(self.overlay_handle,) = self.ax.plot([], [], ".")
|
|
99
|
+
self.sel = np.zeros(self.xdata.shape, dtype=bool)
|
|
100
|
+
self.is_active = False
|
|
101
|
+
|
|
102
|
+
def get_data(self) -> np.ndarray:
|
|
103
|
+
"""Get the data points of the plot."""
|
|
104
|
+
return np.vstack((self.xdata, self.ydata)).T
|
|
105
|
+
|
|
106
|
+
def onselect(self, verts: List[tuple]) -> None:
|
|
107
|
+
"""Select if not previously selected; Unselect if previously selected."""
|
|
108
|
+
selected_ind = mPath(verts).contains_points(self.get_data())
|
|
109
|
+
self.sel = np.logical_xor(selected_ind, self.sel)
|
|
110
|
+
sel_x = list(self.xdata[self.sel])
|
|
111
|
+
sel_y = list(self.ydata[self.sel])
|
|
112
|
+
self.overlay_handle.set_data(sel_x, sel_y)
|
|
113
|
+
plt.draw()
|
|
114
|
+
|
|
115
|
+
def start(self, event=None) -> None:
|
|
116
|
+
"""Start the lasso selection."""
|
|
117
|
+
self.lasso = LassoSelectorWidget(self.plot_handle.axes, self.onselect)
|
|
118
|
+
self.is_active = True
|
|
119
|
+
|
|
120
|
+
def stop(self, event=None) -> None:
|
|
121
|
+
"""Stop the lasso selection."""
|
|
122
|
+
self.lasso.disconnect_events()
|
|
123
|
+
self.is_active = False
|
|
124
|
+
|
|
125
|
+
def toggle(self, event=None) -> None:
|
|
126
|
+
"""Toggle the lasso selection."""
|
|
127
|
+
if self.is_active:
|
|
128
|
+
self.stop(event)
|
|
129
|
+
else:
|
|
130
|
+
self.start(event)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
class AssetContainer:
|
|
134
|
+
"""
|
|
135
|
+
Container for assets such as a button, memoryslot, etc.
|
|
136
|
+
|
|
137
|
+
Args:
|
|
138
|
+
parent (Any): matplotlib figure, or something that has a 'figure' attribute that is a figure.
|
|
139
|
+
"""
|
|
140
|
+
|
|
141
|
+
def __init__(self, parent: Any) -> None:
|
|
142
|
+
self._list: List[Any] = [] # list of assets
|
|
143
|
+
self.parent = parent
|
|
144
|
+
|
|
145
|
+
def __len__(self) -> int:
|
|
146
|
+
return len(self._list)
|
|
147
|
+
|
|
148
|
+
@property
|
|
149
|
+
def names(self) -> List[str]:
|
|
150
|
+
return [x.name for x in self._list]
|
|
151
|
+
|
|
152
|
+
def __getitem__(self, key: Union[int, str]) -> Any:
|
|
153
|
+
"""Return an asset by the name key or by position in the list."""
|
|
154
|
+
if not self._has_names():
|
|
155
|
+
assert isinstance(key, int)
|
|
156
|
+
|
|
157
|
+
if isinstance(key, int) and key not in self.names:
|
|
158
|
+
return self._list[key]
|
|
159
|
+
|
|
160
|
+
return {x.name: x for x in self._list}[key]
|
|
161
|
+
|
|
162
|
+
def _has_names(self) -> bool:
|
|
163
|
+
try:
|
|
164
|
+
self.names
|
|
165
|
+
return True
|
|
166
|
+
except AttributeError:
|
|
167
|
+
return False
|
|
168
|
+
|
|
169
|
+
def add(self, asset: Any) -> Any:
|
|
170
|
+
"""Add an asset to the container."""
|
|
171
|
+
if hasattr(asset, "name"):
|
|
172
|
+
assert asset.name not in self.names
|
|
173
|
+
self._list.append(asset)
|
|
174
|
+
return asset
|
|
175
|
+
|
|
176
|
+
def __contains__(self, item: str) -> bool:
|
|
177
|
+
"""Check if an asset with the given name exists in the container."""
|
|
178
|
+
return item in self.names
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
class Buttons(AssetContainer):
|
|
182
|
+
"""Manager for buttons in a matplotlib figure or GUI (see GenericBrowser for example)."""
|
|
183
|
+
|
|
184
|
+
def add(
|
|
185
|
+
self,
|
|
186
|
+
text: str = "Button",
|
|
187
|
+
action_func: Optional[Union[Callable, List[Callable]]] = None,
|
|
188
|
+
pos: Optional[tuple] = None,
|
|
189
|
+
w: float = 0.25,
|
|
190
|
+
h: float = 0.05,
|
|
191
|
+
buf: float = 0.01,
|
|
192
|
+
type_: str = "Push",
|
|
193
|
+
**kwargs,
|
|
194
|
+
) -> Button:
|
|
195
|
+
"""
|
|
196
|
+
Add a button to the parent figure / object.
|
|
197
|
+
|
|
198
|
+
If pos is provided, then w, h, and buf will be ignored.
|
|
199
|
+
"""
|
|
200
|
+
assert type_ in ("Push", "Toggle")
|
|
201
|
+
nbtn = len(self)
|
|
202
|
+
if pos is None: # start adding at the top left corner
|
|
203
|
+
parent_fig = self.parent.figure
|
|
204
|
+
mul_factor = 6.4 / parent_fig.get_size_inches()[0]
|
|
205
|
+
|
|
206
|
+
btn_w = w * mul_factor
|
|
207
|
+
btn_h = h * mul_factor
|
|
208
|
+
btn_buf = buf
|
|
209
|
+
pos = (
|
|
210
|
+
btn_buf,
|
|
211
|
+
(1 - btn_buf) - ((btn_buf + btn_h) * (nbtn + 1)),
|
|
212
|
+
btn_w,
|
|
213
|
+
btn_h,
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
if type_ == "Toggle":
|
|
217
|
+
b = ToggleButton(plt.axes(pos), text, **kwargs)
|
|
218
|
+
else:
|
|
219
|
+
b = Button(plt.axes(pos), text, **kwargs)
|
|
220
|
+
|
|
221
|
+
if action_func is not None: # more than one can be attached
|
|
222
|
+
if isinstance(action_func, (list, tuple)):
|
|
223
|
+
for af in action_func:
|
|
224
|
+
b.on_clicked(af)
|
|
225
|
+
else:
|
|
226
|
+
b.on_clicked(action_func)
|
|
227
|
+
|
|
228
|
+
return super().add(b)
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
class Selectors(AssetContainer):
|
|
232
|
+
"""Manager for selector objects - for picking points on line2D objects."""
|
|
233
|
+
|
|
234
|
+
def add(self, plot_handle: mlines.Line2D) -> Selector:
|
|
235
|
+
"""Add a selector to the container."""
|
|
236
|
+
return super().add(Selector(plot_handle))
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
class MemorySlots(AssetContainer):
|
|
240
|
+
"""Manager for memory slots to store and navigate positions."""
|
|
241
|
+
|
|
242
|
+
def __init__(self, parent: Any) -> None:
|
|
243
|
+
super().__init__(parent)
|
|
244
|
+
self._list = self.initialize()
|
|
245
|
+
self._memtext = None
|
|
246
|
+
|
|
247
|
+
@staticmethod
|
|
248
|
+
def initialize() -> dict:
|
|
249
|
+
"""Initialize memory slots."""
|
|
250
|
+
return {str(k): None for k in range(1, 10)}
|
|
251
|
+
|
|
252
|
+
def disable(self) -> None:
|
|
253
|
+
"""Disable memory slots."""
|
|
254
|
+
self._list = {}
|
|
255
|
+
|
|
256
|
+
def enable(self) -> None:
|
|
257
|
+
"""Enable memory slots."""
|
|
258
|
+
self._list = self.initialize()
|
|
259
|
+
|
|
260
|
+
def show(self, pos: str = "bottom left") -> None:
|
|
261
|
+
"""Show memory slot text."""
|
|
262
|
+
self._memtext = TextView(self._list, fax=self.parent.figure, pos=pos)
|
|
263
|
+
|
|
264
|
+
def update(self, key: str) -> None:
|
|
265
|
+
"""
|
|
266
|
+
Handle memory slot updates.
|
|
267
|
+
|
|
268
|
+
Initiate when None, go to the slot if it exists, free slot if pressed when it exists.
|
|
269
|
+
key is the event.key triggered by a callback.
|
|
270
|
+
"""
|
|
271
|
+
if self._list[key] is None:
|
|
272
|
+
self._list[key] = self.parent._current_idx
|
|
273
|
+
self.update_display()
|
|
274
|
+
elif self._list[key] == self.parent._current_idx:
|
|
275
|
+
self._list[key] = None
|
|
276
|
+
self.update_display()
|
|
277
|
+
else:
|
|
278
|
+
self.parent._current_idx = self._list[key]
|
|
279
|
+
self.parent.update()
|
|
280
|
+
|
|
281
|
+
def update_display(self) -> None:
|
|
282
|
+
"""Refresh memory slot text if it is not hidden."""
|
|
283
|
+
if self._memtext is not None:
|
|
284
|
+
self._memtext.update(self._list)
|
|
285
|
+
|
|
286
|
+
def hide(self) -> None:
|
|
287
|
+
"""Hide the memory slot text."""
|
|
288
|
+
if self._memtext is not None:
|
|
289
|
+
self._memtext._text.remove()
|
|
290
|
+
self._memtext = None
|
|
291
|
+
|
|
292
|
+
def is_enabled(self) -> bool:
|
|
293
|
+
"""Check if memory slots are enabled."""
|
|
294
|
+
return bool(self._list)
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
class StateVariable:
|
|
298
|
+
"""Manage state variables with multiple states."""
|
|
299
|
+
|
|
300
|
+
def __init__(self, name: str, states: list) -> None:
|
|
301
|
+
self.name = name
|
|
302
|
+
self.states = list(states)
|
|
303
|
+
self._current_state_idx = 0
|
|
304
|
+
|
|
305
|
+
@property
|
|
306
|
+
def current_state(self) -> Any:
|
|
307
|
+
"""Get the current state."""
|
|
308
|
+
return self.states[self._current_state_idx]
|
|
309
|
+
|
|
310
|
+
def n_states(self) -> int:
|
|
311
|
+
"""Get the number of states."""
|
|
312
|
+
return len(self.states)
|
|
313
|
+
|
|
314
|
+
def cycle(self) -> None:
|
|
315
|
+
"""Cycle to the next state."""
|
|
316
|
+
self._current_state_idx = (self._current_state_idx + 1) % self.n_states()
|
|
317
|
+
|
|
318
|
+
def cycle_back(self) -> None:
|
|
319
|
+
"""Cycle to the previous state."""
|
|
320
|
+
self._current_state_idx = (self._current_state_idx - 1) % self.n_states()
|
|
321
|
+
|
|
322
|
+
def set_state(self, state: Union[int, str]) -> None:
|
|
323
|
+
"""Set the state."""
|
|
324
|
+
if isinstance(state, int):
|
|
325
|
+
assert 0 <= state < self.n_states()
|
|
326
|
+
self._current_state_idx = state
|
|
327
|
+
if isinstance(state, str):
|
|
328
|
+
assert state in self.states
|
|
329
|
+
self._current_state_idx = self.states.index(state)
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
class StateVariables(AssetContainer):
|
|
333
|
+
"""Manager for state variables."""
|
|
334
|
+
|
|
335
|
+
def __init__(self, parent: Any) -> None:
|
|
336
|
+
super().__init__(parent)
|
|
337
|
+
self._text = None
|
|
338
|
+
|
|
339
|
+
def asdict(self) -> dict:
|
|
340
|
+
"""Return state variables as a dictionary."""
|
|
341
|
+
return {x.name: x.states for x in self._list}
|
|
342
|
+
|
|
343
|
+
def add(self, name: str, states: list) -> StateVariable:
|
|
344
|
+
"""Add a state variable to the container."""
|
|
345
|
+
assert name not in self.names
|
|
346
|
+
return super().add(StateVariable(name, states))
|
|
347
|
+
|
|
348
|
+
def _get_display_text(self) -> List[str]:
|
|
349
|
+
"""Get the display text for state variables."""
|
|
350
|
+
return ["State variables:"] + [
|
|
351
|
+
f"{x.name} - {x.current_state}" for x in self._list
|
|
352
|
+
]
|
|
353
|
+
|
|
354
|
+
def show(self, pos: str = "bottom right") -> None:
|
|
355
|
+
"""Show state variables text."""
|
|
356
|
+
self._text = TextView(self._get_display_text(), fax=self.parent.figure, pos=pos)
|
|
357
|
+
|
|
358
|
+
def update_display(self, draw: bool = True) -> None:
|
|
359
|
+
"""Update the display of state variables."""
|
|
360
|
+
self._text.update(self._get_display_text())
|
|
361
|
+
if draw:
|
|
362
|
+
plt.draw()
|