r"""
Main GUI.
"""
import tkinter as tk
from tkinter import ttk, messagebox, filedialog
import sys
import webbrowser
import pathlib
from typing import Union
import numpy as np
from matplotlib import image
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg, NavigationToolbar2Tk
from matplotlib.figure import Figure
from . import version
from . import settings
from .tests import test_linear, test_ylog, test_loglog, test_xlog
[docs]
class AboutWindow(tk.Toplevel):
r"""Class for about window. See __init__.__doc__."""
def __init__(self, master):
r"""
About window.
Parameters
----------
master: tkinter widget
Container.
"""
super().__init__(master)
self.transient(master)
self.master = master
self.title('About')
self.grab_set()
self.initial_focus = self
self.protocol("WM_DELETE_WINDOW", self._quit)
ws = self.master.winfo_screenwidth()
hs = self.master.winfo_screenheight()
width = int(0.65*ws)
height = int(0.1*hs)
x = int((ws / 2) - (width / 2))
y = int((hs / 2) - (height / 2) - 25)
self.geometry(f'{width}x{height}+{x}+{y}')
self.resizable(height=False, width=False)
self.frame = ttk.Frame(self)
self.frame.pack(fill=tk.BOTH, expand=tk.TRUE)
for i in range(2):
self.frame.grid_rowconfigure(i, weight=1)
for i in range(1):
self.frame.grid_columnconfigure(i, weight=1)
msg = version.__package_name__ + ': ' + version.__version__
label = ttk.Label(self.frame, text=msg)
label.configure(anchor='center')
label.grid(row=0, column=0, sticky='nswe')
msg = 'Python : ' + sys.version.replace('\n', ' ')
label = ttk.Label(self.frame, text=msg)
label.configure(anchor='center')
label.grid(row=1, column=0, sticky='nswe')
self.initial_focus.focus_set()
self.wait_window(self)
def _quit(self):
r"""Close the top level window."""
self.master.focus_set()
self.destroy()
[docs]
class HowToUse(tk.Toplevel):
r"""Class for quick help window. See __init__.__doc__."""
def __init__(self, master):
r"""
How to use window.
Parameters
----------
master: tkinter widget
Container.
"""
super().__init__(master)
self.transient(master)
self.master = master
self.title('How To Use')
self.grab_set()
self.initial_focus = self
self.protocol("WM_DELETE_WINDOW", self._quit)
ws = self.master.winfo_screenwidth()
hs = self.master.winfo_screenheight()
width = int(0.5*ws)
height = int(0.7*hs)
x = int((ws / 2) - (width / 2))
y = int((hs / 2) - (height / 2) - 25)
self.geometry(f'{width}x{height}+{x}+{y}')
kwargs = {'scrolled': 'both'}
self.sframe = ScrolledFrame(self, **kwargs)
msg = self.master.__init__.__doc__.split('Parameters')[0]
label = ttk.Label(self.sframe.frame, text=msg)
label.configure(anchor='w', justify='left')
label.grid(row=0, column=0, sticky='nswe')
def _quit(self):
r"""Close the toplevel window."""
self.master.focus_set()
self.destroy()
#TODO: Use tkinter treeview instead of array of widgets
[docs]
class DataTable(ScrolledFrame):
r"""Scrolled data table. See __init__.__doc__."""
def __init__(self, master, **kwargs):
r"""
Scrolled data table widget.
Parameters
------------
master: tkinter widget
Master container.
kwargs: dict, optional
Keyword arguments for the scrolled frame.
"""
ScrolledFrame.__init__(self, master, **kwargs)
self.pack(expand=tk.TRUE, fill=tk.BOTH)
self._ncols = 8
self._nrows = 0
self._headers = None
self._has_header = False
self._data = None
# do not check number of columns because it is always the same
self._wdg_line = np.zeros(shape=(1, self._ncols), dtype=np.object_)
self._wdg_line_tkvar = np.zeros(shape=(1, self._ncols), dtype=np.object_)
self._wdg_array = np.zeros(shape=(0, self._ncols), dtype=np.object_)
self._wdg_array_tkvar = np.zeros(shape=(0, self._ncols), dtype=np.object_)
[docs]
def set_new_data(self, data):
"""Set new data in the displayed data table.
Parameters
----------
data : structured array, shape=(n,)
Numpy structured array used for registering the extracted data.
"""
nrows = data.shape[0]
# difference between old and new array
d = nrows - self._nrows
if d == 0:
sign = 0
else:
sign = d / abs(d)
self._data = data
self._nrows = self._data.shape[0]
self._ncols = len(self._data.dtype.names)
self._headers = self._data.dtype.names
if not self._has_header:
for j in range(self._ncols):
ttk.Label(self.frame, text=self._headers[j]).grid(row=0, column=j, sticky='nswe')
self._has_header = True
for j in range(self._ncols):
self.frame.grid_columnconfigure(j, weight=1)
for i in range(self._nrows+1):
self.frame.grid_rowconfigure(i, weight=0)
for i in range(abs(d)):
if sign > 0:
self._add_line()
elif sign < 0:
self._remove_line()
self._display()
def _create_line_widget(self):
for j in range(self._ncols):
self._wdg_line_tkvar[0, j] = tk.StringVar()
self._wdg_line[0, j] = ttk.Label(self.frame,
textvariable=self._wdg_line_tkvar[0, j])
def _grid_widgets(self):
for i, j in np.ndindex(self._wdg_array.shape):
self._wdg_array[i, j].grid(row=i+1, column=j, sticky='nswe')
def _add_line(self):
self._create_line_widget()
self._wdg_array = np.append(self._wdg_array, self._wdg_line, axis=0)
self._wdg_array_tkvar = np.append(self._wdg_array_tkvar, self._wdg_line_tkvar, axis=0)
def _remove_line(self):
if self._wdg_array.shape[0] > 0:
for j in range(self._ncols):
self._wdg_array[-1, j].destroy()
self._wdg_array[-1, j] = np.empty
self._wdg_array = np.delete(self._wdg_array, -1, axis=0)
self._wdg_array_tkvar = np.delete(self._wdg_array_tkvar, -1, axis=0)
def _update_widget_values(self):
for i, j in np.ndindex(self._wdg_array.shape):
col = self._headers[j]
value = self._data[col][i]
self._wdg_array_tkvar[i, j].set(value)
def _display(self):
self._update_widget_values()
self._grid_widgets()
[docs]
class DataWindow(tk.Toplevel):
r"""Class for data window. See __init__.__doc__."""
def __init__(self, master):
r"""
How to use window.
Parameters
----------
master: tkinter widget
Container.
"""
super().__init__(master)
self.transient(master)
self.master = master
self.title('Data Table')
self.grab_set()
self.initial_focus = self
self.protocol("WM_DELETE_WINDOW", self._quit)
ws = self.master.winfo_screenwidth()
hs = self.master.winfo_screenheight()
width = int(0.5*ws)
height = int(0.7*hs)
x = int((ws / 2) - (width / 2))
y = int((hs / 2) - (height / 2) - 25)
self.geometry(f'{width}x{height}+{x}+{y}')
kwargs = {'scrolled': 'both'}
self.datatable = DataTable(self, **kwargs)
self.datatable.pack(fill=tk.BOTH, expand=tk.TRUE)
def _quit(self):
self.master.focus_set()
self.destroy()
[docs]
class App(ttk.Frame):
r"""Class for main graphical interface. See __init__.__doc__."""
def __init__(self, master=None):
r"""
The cursor is used to point a specific position in the graph
whereas all operations are done through keyboard combinations or through the main menu.
Legend:
- Red crosses are data points
- Blue crosses are Xmin and Xmax
- Green crosses are Ymin and Ymax
Commands:
* <Ctrl-o> for loading image.
* <Ctrl-a> add data point.
* <Hold a+Left Click> add data point.
* <Left Click> select a data point.
* <Hold Ctrl+Left Click> multiple data point selection.
* <Ctrl-g> set Xmin from last data point or from selected data point.
* <Ctrl-h> set Xmax from last data point or from selected data point.
* <Ctrl-j> set Ymin from last data point or from selected data point.
* <Ctrl-k> set Ymax from last data point or from selected data point
* <Ctrl-l> set all limits from last 4 data points or from last 4 selected data points.
* <Ctrl-n> remove all limits.
* <Ctrl-z> remove last data point.
* <Ctrl-d> remove selected data point.
* <Ctrl-D> remove all data points.
* <Ctrl-m> compute the data points.
* <Ctrl-t> view data table.
* <Ctrl-s> save data points.
* <Ctrl-w> clear all.
Parameters
------------
master: tkinter.Tk instance
Root instanciation of tkinter.
"""
# main frame
ttk.Frame.__init__(self, master)
self.pack(expand=tk.YES, fill=tk.BOTH)
self.master.title('Data Digitizer')
folder = pathlib.Path(__file__).parent
self.master.iconphoto(True, tk.PhotoImage(file=folder / 'icon.png'))
self.master.protocol("WM_DELETE_WINDOW", self.stop)
self.url_download = 'http://www.github.com/MilanSkocic/PyDatadigitizer'
self.url = 'https://milanskocic.github.io/PyDatadigitizer/index.html'
self._filepath = None
# configuration
self.cfg = settings.cfg
# bindings
self.master.bind('<Control-o>', self._cb_open)
self.master.bind('<Control-d>', self._cb_delete_selected)
self.master.bind('<Control-D>', self._cb_delete_all)
self.master.bind('<Control-w>', self._cb_clear)
self.master.bind('<Control-q>', self._cb_quit)
self.master.bind('<Control-g>', self._cb_set_xmin)
self.master.bind('<Control-h>', self._cb_set_xmax)
self.master.bind('<Control-j>', self._cb_set_ymin)
self.master.bind('<Control-k>', self._cb_set_ymax)
self.master.bind('<Control-m>', self._cb_measure)
self.master.bind('<Control-s>', self._cb_save)
self.master.bind('<Control-l>', self._cb_set_all_limits)
self.master.bind('<Control-n>', self._cb_delete_limits)
self.master.bind('<Control-z>', self._cb_undo)
self.master.bind('<Control-t>', self._cb_datatable)
# get screen width and height
ws = self.master.winfo_screenwidth()
hs = self.master.winfo_screenheight()
# master.geometry(("%dx%d")%(ws,hs))
width = int(0.75 * ws)
height = int(0.75 * hs)
x = int((ws / 2) - (width / 2))
y = int((hs / 2) - (height / 2) - 25)
self.master.geometry(f'{width}x{height}+{x}+{y}')
# Grid config
tk.Grid.columnconfigure(self, 0, weight=0)
tk.Grid.columnconfigure(self, 1, weight=1)
tk.Grid.rowconfigure(self, 0, weight=1)
tk.Grid.rowconfigure(self, 1, weight=0)
# flags and variables
section = "FOLDERS"
self._image_folder = self.cfg.get_typed_option(section=section,
option='image folder')
self._image_name = self.cfg.get_typed_option(section=section,
option = 'image name')
self._data_folder = self.cfg.get_typed_option(section=section,
option = 'data folder')
self._data_name = self.cfg.get_typed_option(section=section,
option = 'data name')
self._axes_image = None
self._axes_image_threshold = None
# self._data_indexes = []
self._percentage = 0.01
self._percentage_shift = 0.05
self.R, self.G, self.B, self.alpha = 0, 1, 2, 3
self.dtypes = [('type', 'U32'),
('i', 'i4'),
('j', 'i4'),
('Xpix', 'i4'),
('Ypix', 'i4'),
('x', 'f8'),
('y', 'f8'),
('selected', 'i2')]
self._line = np.zeros(shape=(1,), dtype=self.dtypes)
self._data_array = np.zeros(shape=(0,), dtype=self.dtypes)
self._triggered_event = None
self.row = None
self.col = None
self._ctrl_key_pressed = False
self._a_key_pressed = False
# Menu
self.menubar = tk.Menu(self.master)
self.master.config(menu=self.menubar)
# File Menu
self.file_menu = tk.Menu(self.menubar)
self.menubar.add_cascade(menu=self.file_menu, label='File')
self.file_menu.add_command(label='Load Image <Ctrl-o>', command=self._trigger_load_event)
self.file_menu.add_command(label='Save Data <Ctrl-s>', command=self._trigger_save_event)
self.file_menu.add_command(label='Clear All <Ctrl-w>', command=self._trigger_clearall_event)
self.file_menu.add_command(label='Quit <Ctrl-q>', command=self.stop)
# Data Menu
self.data_menu = tk.Menu(self.menubar)
self.menubar.add_cascade(menu=self.data_menu, label='Data')
self.data_menu.add_command(label='Add <Ctrl-a> or <Hold a+Left Click>',
command=self._trigger_add_event)
self.data_menu.add_command(label='Remove last <Ctrl-z>',
command=self._trigger_undo_event)
self.data_menu.add_command(label='Remove all <Ctrl-D>',
command=self._trigger_delete_all_event)
self.data_menu.add_command(label='Remove selected <Ctrl-d>',
command=self._trigger_delete_selected_event)
self.data_menu.add_separator()
self.data_menu.add_command(label='Set Xmin <Ctrl-g>',
command=self._trigger_xmin_event)
self.data_menu.add_command(label='Set Xmax <Ctrl-h>',
command=self._trigger_xmax_event)
self.data_menu.add_command(label='Set Ymin <Ctrl-j>',
command=self._trigger_ymin_event)
self.data_menu.add_command(label='Set Ymax <Ctrl-k>',
command=self._trigger_ymax_event)
self.data_menu.add_command(label='Set all limits <Ctrl-l>',
command=self._trigger_all_limits_event)
self.data_menu.add_command(label='Remove all limits <Ctrl-n>',
command=self._trigger_delete_all_limits_event)
self.data_menu.add_separator()
self.data_menu.add_command(label='Compute <Ctrl-m>',
command=self._trigger_measure_event)
self.data_menu.add_command(label='View Data <Ctrl-t>',
command=self._trigger_datatable_event)
# Test Menu
self.test_menu = tk.Menu(self.menubar)
self.menubar.add_cascade(menu=self.test_menu, label='Tests')
self.test_menu.add_command(label="Test Linear", command=self._test_linear)
self.test_menu.add_command(label="Test YLog", command=self._test_ylog)
self.test_menu.add_command(label="Test XLog", command=self._test_xlog)
self.test_menu.add_command(label="Test LogLog", command=self._test_loglog)
# Help Menu
self.help_menu = tk.Menu(self.menubar)
self.menubar.add_cascade(menu=self.help_menu, label='Help')
self.help_menu.add_command(label='About', command=self._about)
self.help_menu.add_command(label='How to use', command=self._how_to_use)
self.help_menu.add_command(label='Online Documentation', command=self._online_documentation)
self.help_menu.add_command(label='GitHub Sources', command=self._sources)
# panes
self.left_frame = ttk.Frame(self)
self.left_frame.grid(row=0, column=0, sticky='nswe')
for i in range(2):
tk.Grid.columnconfigure(self.left_frame, i, weight=1)
self.right_frame = ttk.Frame(self)
self.right_frame.grid(row=0, column=1, sticky='nswe')
tk.Grid.columnconfigure(self.right_frame, 0, weight=1)
tk.Grid.rowconfigure(self.right_frame, 0, weight=1)
# figure
self._figframe = FigureFrame(self.right_frame)
self._fig = self._figframe.figure
self._ax = self._figframe.subplot
self._canvas = self._figframe.canvas
self._canvas_widget = self._figframe.canvas.get_tk_widget()
self._ax.set_axis_off()
self._figframe.canvas.mpl_connect("key_press_event", self._cb_key_press)
self._figframe.canvas.mpl_connect("button_press_event", self._cb_button_press)
self._figframe.canvas.mpl_connect('key_release_event', self._cb_key_release)
self._figframe.grid(row=0, column=0, sticky='nswe')
# Help Label
row = 0
style = ttk.Style()
style.configure('Xlimits.TEntry')
style.map('Xlimits.TEntry', foreground=[('focus', 'blue')])
style.configure('Ylimits.TEntry')
style.map('Ylimits.TEntry', foreground=[('focus', 'green')])
style.configure('TestData.TEntry')
style.map('TestData.TEntry', foreground=[('focus', 'red')])
# X Axis
row = row + 1
container = self.left_frame
sep = ttk.Separator(container, orient="horizontal")
sep.grid(row=row, column=0, columnspan=2, sticky='nswe')
row += 1
label = ttk.Label(container, text='X Axis')
label.grid(row=row, column=0, columnspan=2, sticky='nswe')
self._tkvar_log_xscale = tk.BooleanVar()
self._tkvar_log_xscale.set(False)
self._log_xscale_cb = ttk.Checkbutton(container,
variable=self._tkvar_log_xscale,
text='log X scale?',
command=self._xlog_scale)
self._log_xscale_cb.grid(row=row, column=1, sticky='nswe')
row = row + 1
ttk.Label(container, text='Xmin=').grid(row=row, column=0, sticky='nswe')
self._tkvar_xmin = tk.DoubleVar()
self._tkvar_xmin.set(0.0)
self._xmin_entry = ttk.Entry(container,
textvariable=self._tkvar_xmin,
style='Xlimits.TEntry')
self._xmin_entry.grid(row=row, column=1, sticky='nswe')
self._xmin_entry.bind('<Return>', self._cb_measure)
row += 1
ttk.Label(container, text='Xmax=').grid(row=row, column=0, sticky='nswe')
self._tkvar_xmax = tk.DoubleVar()
self._tkvar_xmax.set(1.0)
self._xmax_entry = ttk.Entry(container,
textvariable=self._tkvar_xmax,
style='Xlimits.TEntry')
self._xmax_entry.grid(row=row, column=1, sticky='nswe')
self._xmax_entry.bind('<Return>', self._cb_measure)
row += 1 # X unit entry
ttk.Label(container, text='X Unit').grid(row=row, column=0, sticky='nswe')
self._tkvar_xunit = tk.StringVar()
self._tkvar_xunit.set('a.u.')
self._xunit_entry = ttk.Entry(container,
textvariable=self._tkvar_xunit)
self._xunit_entry.grid(row=row, column=1, sticky='nswe')
self._xunit_entry.bind('<Return>', self._cb_measure)
# Y Axis
row += 1
container = self.left_frame
sep = ttk.Separator(container, orient="horizontal")
sep.grid(row=row, column=0, columnspan=2, sticky='nswe', pady=30)
row += 1
label = ttk.Label(self.left_frame, text='Y Axis')
label.grid(row=row, column=0, columnspan=2, sticky='nswe')
self._tkvar_log_yscale = tk.BooleanVar()
self._tkvar_log_yscale.set(False)
self._log_yscale_cb = ttk.Checkbutton(container,
variable=self._tkvar_log_yscale,
text='log Y scale?',
command=self._ylog_scale)
self._log_yscale_cb.grid(row=row, column=1, sticky='nswe')
row += 1
ttk.Label(self.left_frame, text='Ymin=').grid(row=row, column=0, sticky='nswe')
self._tkvar_ymin = tk.DoubleVar()
self._tkvar_ymin.set(0.0)
self._ymin_entry = ttk.Entry(container,
textvariable=self._tkvar_ymin,
style='Ylimits.TEntry')
self._ymin_entry.grid(row=row, column=1, sticky='nswe')
self._ymin_entry.bind('<Return>', self._cb_measure)
row += 1
label = ttk.Label(self.left_frame, text='Ymax=')
label.grid(row=row, column=0, sticky='nswe')
self._tkvar_ymax = tk.DoubleVar()
self._tkvar_ymax.set(1.0)
self._ymax_entry = ttk.Entry(container,
textvariable=self._tkvar_ymax,
style='Ylimits.TEntry')
self._ymax_entry.grid(row=row, column=1, sticky='nswe')
self._ymax_entry.bind('<Return>', self._cb_measure)
row += 1 # Y unit entry
ttk.Label(container, text='Y Unit').grid(row=row, column=0, sticky='nswe')
self._tkvar_yunit = tk.StringVar()
self._tkvar_yunit.set('a.u.')
self._yunit_entry = ttk.Entry(container,
textvariable=self._tkvar_yunit)
self._yunit_entry.grid(row=row, column=1, sticky='nswe')
self._yunit_entry.bind('<Return>', self._cb_measure)
# Data
row += 1
container = self.left_frame
sep = ttk.Separator(container, orient="horizontal")
sep.grid(row=row, column=0, columnspan=2, sticky='nswe', pady=30)
row += 1
label = ttk.Label(self.left_frame, text='N points=')
label.grid(row=row, column=0, sticky='nswe')
self._tkvar_npoints = tk.IntVar()
self._tkvar_npoints.set(0)
label = ttk.Label(container, textvariable=self._tkvar_npoints)
label.grid(row=row, column=1, sticky='nswe')
row += 1
sep = ttk.Separator(container, orient="horizontal")
sep.grid(row=row, column=0, columnspan=2, sticky='nswe', pady=30)
# Test scale
row += 1
label = ttk.Label(container, text='Test values with defined scale:')
label.grid(row=row, column=0, columnspan=2, sticky='nswe')
row += 1
ttk.Label(container, text='X=').grid(row=row, column=0, sticky='nswe')
self._tkvar_xtest = tk.DoubleVar()
self._tkvar_xtest.set(1)
self._xtest_entry = ttk.Entry(container,
textvariable=self._tkvar_xtest,
style='TestData.TEntry')
self._xtest_entry.grid(row=row, column=1, sticky='nswe')
self._xtest_entry.bind('<Return>', self._cb_test_data)
row += 1
ttk.Label(container, text='Y=').grid(row=row, column=0, sticky='nswe')
self._tkvar_ytest = tk.DoubleVar()
self._tkvar_ytest.set(1)
self._ytest_entry = ttk.Entry(container,
textvariable=self._tkvar_ytest,
style='TestData.TEntry')
self._ytest_entry.grid(row=row, column=1, sticky='nswe')
self._ytest_entry.bind('<Return>', self._cb_test_data)
self._reset_ui()
def _reset_ui(self):
self._tkvar_log_xscale.set(False)
self._tkvar_log_yscale.set(False)
self._tkvar_npoints.set(0)
self._tkvar_xmin.set(0.0)
self._tkvar_xmax.set(1.0)
self._tkvar_ymin.set(0.0)
self._tkvar_ymax.set(1.0)
def _about(self):
AboutWindow(self)
def _how_to_use(self):
HowToUse(self)
def _cb_open(self, event):
self._triggered_event = event
self._open_image()
self._load_image()
def _cb_undo(self, event):
self._triggered_event = event
self._undo()
def _cb_delete_all(self, event):
self._triggered_event = event
self._delete_all()
def _cb_delete_selected(self, event):
self._triggered_event = event
self._delete_selected()
def _cb_delete_limits(self, event):
self._triggered_event = event
self._delete_limits()
def _cb_clear(self, event):
self._triggered_event = event
self._clear_all()
def _cb_key_press(self, event):
self._triggered_event = event
if self._axes_image is not None:
dx = int(self.row * self._percentage_shift)
dy = int(self.col * self._percentage_shift)
if event.key == 'ctrl+a':
if (event.xdata is not None) and (event.ydata is not None):
j = int(round(event.xdata, 0))
i = int(round(event.ydata, 0))
self._add_data(i, j)
elif event.key == 'right':
self._shift_data(direction='right')
elif event.key == 'ctrl+right':
self._shift_data(direction='right', d=dx)
elif event.key == 'left':
self._shift_data(direction='left')
elif event.key == 'ctrl+left':
self._shift_data(direction='left', d=dx)
elif event.key == 'up':
self._shift_data(direction='up')
elif event.key == 'ctrl+up':
self._shift_data(direction='up', d=dy)
elif event.key == 'down':
self._shift_data(direction='down')
elif event.key == 'ctrl+down':
self._shift_data(direction='down', d=dy)
elif event.key == 'control':
self._ctrl_key_pressed = True
elif event.key == 'a':
self._a_key_pressed = True
def _cb_key_release(self, event):
self._triggered_event = event
if self._axes_image is not None:
if event.key == 'control':
self._ctrl_key_pressed = False
elif event.key == 'a':
self._a_key_pressed = False
def _cb_button_press(self, event):
self._triggered_event = event
if self._axes_image is not None:
if event.button == 1:
if (event.xdata is not None) and (event.ydata is not None):
if self._data_array.size:
y = int(round(event.xdata, 0))
x = int(round(event.ydata, 0))
dx_lim = int(self.row * self._percentage)
dy_lim = int(self.col * self._percentage)
dxy_lim = np.sqrt(dx_lim**2 + dy_lim**2)
dx = x - self._data_array['i']
dy = y - self._data_array['j']
dxy = np.sqrt(dx**2 + dy**2)
ix = np.argmin(dxy)
if dxy[ix] <= dxy_lim:
if not self._ctrl_key_pressed:
self._data_array['selected'] = 0
arr = self._data_array['selected'][ix]
self._data_array['selected'][ix] = np.logical_not(arr)
else:
self._data_array['selected'] = 0
if self._a_key_pressed:
self._trigger_add_event()
self._display_data()
self._canvas_widget.focus_set()
def _cb_set_xmin(self, event):
self._triggered_event = event
self._add_limits(which='xmin')
def _cb_set_xmax(self, event):
self._triggered_event = event
self._add_limits(which='xmax')
def _cb_set_ymin(self, event):
self._triggered_event = event
self._add_limits(which='ymin')
def _cb_set_ymax(self, event):
self._triggered_event = event
self._add_limits(which='ymax')
def _cb_measure(self, event):
self._triggered_event = event
self._measure()
def _cb_test_data(self, event):
self._triggered_event = event
self._plot_test_data()
def _cb_set_all_limits(self, event):
self._triggered_event = event
if self._data_array.size >= 4:
self._add_limits(which='ymax')
self._add_limits(which='ymin')
self._add_limits(which='xmax')
self._add_limits(which='xmin')
else:
msg = "You must add at least 4 points \
before setting all limits at once."
messagebox.showinfo("Infos", msg)
def _cb_save(self, event):
self._triggered_event = event
if self._measure():
self._save()
def _cb_datatable(self, event):
self._triggered_event = event
if self._measure():
datawindow = DataWindow(self)
datawindow.datatable.set_new_data(self._data_array)
def _cb_quit(self, event):
self._triggered_event = event
self.stop()
def _trigger_load_event(self):
self.master.event_generate('<Control-o>')
def _trigger_add_event(self):
self._canvas_widget.event_generate('<Control-a>')
def _trigger_undo_event(self):
self._canvas_widget.event_generate('<Control-z>')
def _trigger_save_event(self):
self.master.event_generate('<Control-s>')
def _trigger_xmin_event(self):
self.master.event_generate('<Control-g>')
def _trigger_xmax_event(self):
self.master.event_generate('<Control-h>')
def _trigger_ymin_event(self):
self.master.event_generate('<Control-j>')
def _trigger_ymax_event(self):
self.master.event_generate('<Control-k>')
def _trigger_clearall_event(self):
self.master.event_generate('<Control-w>')
def _trigger_all_limits_event(self):
self.master.event_generate('<Control-l>')
def _trigger_delete_all_event(self):
self.master.event_generate('<Control-D>')
def _trigger_delete_selected_event(self):
self.master.event_generate('<Control-d>')
def _trigger_delete_all_limits_event(self):
self.master.event_generate('<Control-n>')
def _trigger_measure_event(self):
self.master.event_generate('<Control-m>')
def _trigger_datatable_event(self):
self.master.event_generate('<Control-t>')
def _open_image(self):
_filepath = filedialog.askopenfilename(title='Open Plot',
defaultextension='.png',
filetypes=[('png', '.png'),
('jpeg', '.jpeg'),
('tif', '.tif'),
('all files', '.*')],
initialdir=self._image_folder,
parent=self)
if len(_filepath) > 0:
self._filepath = pathlib.Path(_filepath).absolute()
else:
self._filepath = None
def _load_image(self):
r"""load image"""
self._clear_all()
if self._filepath is not None:
image_array = image.imread(str(self._filepath))
shape = image_array.shape
dim = len(shape)
if dim > 1:
self._delete_all()
self._ax.set_axis_on()
self.row, self.col = image_array.shape[0:2]
image_threshold = np.zeros(shape=(self.row, self.col, 4))
self._axes_image = self._ax.imshow(image_array, cmap='Greys_r')
self._axes_image_threshold = self._ax.imshow(image_threshold)
self._ax.relim()
self._canvas.draw()
else:
messagebox.showinfo("Infos", f"{self._filepath} is not a valid image (ndim={dim}).")
self._image_folder = self._filepath.parent
self._image_name = self._filepath.name
def _ij_to_xypix(self, i: int, j: int):
"""Convert matrix indexes i,j into graph pixels."""
xpix = j
ypix = self.row - i
return xpix, ypix
def _xypix_to_ij(self, xpix, ypix):
"""Convert graph pixels into matrix indexes."""
i = self.row - ypix
j = xpix
return i, j
def _add_data(self, i: int, j: int):
r"""Add a point."""
xpix, ypix = self._ij_to_xypix(i, j)
self._line[0] = ('data', i, j, xpix, ypix, 0, 0, 0)
self._data_array = np.append(self._data_array, self._line)
self._display_data()
def _undo(self):
r"""Delete last point."""
indexes = np.argwhere(self._data_array['type'] == 'data')
if indexes.size:
self._data_array = np.delete(self._data_array, indexes[-1])
self._display_data()
def _add_limits(self, which: str):
r"""Set limit from the selected or the available points."""
if self._data_array.size:
indexes = np.argwhere(self._data_array['type'] == which)
selected = np.argwhere(self._data_array['selected'] == 1)
if selected.size >= 1:
self._data_array['type'][indexes] = 'data'
self._data_array['type'][selected[-1]] = which
self._data_array['selected'][selected[-1]] = 0
else:
data_indexes = np.argwhere(self._data_array['type'] == 'data')
if data_indexes.size >= 1:
self._data_array['type'][indexes] = 'data'
self._data_array['type'][data_indexes[-1]] = which
self._data_array['selected'][data_indexes[-1]] = 0
self._display_data()
else:
messagebox.showinfo("Infos", "You must add at least 1 point.")
def _delete_all(self):
r"""Delete all points except the limits."""
indexes = np.argwhere(self._data_array['type'] == 'data')
if indexes.size:
self._data_array = np.delete(self._data_array, indexes)
self._display_data()
def _delete_selected(self):
r"""Delete selected points."""
indexes = np.argwhere((self._data_array['selected'] == 1) \
& (self._data_array['type'] == 'data'))
self._data_array = np.delete(self._data_array, indexes)
self._display_data()
def _delete_limits(self):
r"""Change type from xy lim to data."""
for which in ['xmin', 'xmax', 'ymin', 'ymax']:
indexes = np.argwhere(self._data_array['type'] == which)
if indexes.size:
self._data_array['type'][indexes] = 'data'
self._display_data()
def _clear_all(self):
self._ax.clear()
self._axes_image = None
self._axes_image_threshold = None
self.row = None
self.col = None
self._line = np.zeros(shape=(1,), dtype=self.dtypes)
self._data_array = np.zeros(shape=(0,), dtype=self.dtypes)
self._ax.set_axis_off()
self._reset_ui()
self._refresh()
def _shift_data(self, direction: str, d: int = 1):
if self._data_array.size:
d = int(abs(d))
mask = self._data_array['selected'] == 1
if direction == 'right':
ypix = self._data_array['j'][mask] + d
self._data_array['j'][mask] = ypix % self.col
elif direction == 'left':
ypix = self._data_array['j'][mask] - d
self._data_array['j'][mask] = ypix % self.col
elif direction == 'up':
xpix = self._data_array['i'][mask] - d
self._data_array['i'][mask] = xpix % self.row
elif direction == 'down':
xpix = self._data_array['i'][mask] + d
self._data_array['i'][mask] = xpix % self.row
i, j = self._data_array['i'][mask], self._data_array['j'][mask]
xpix, ypix = self._ij_to_xypix(i, j)
self._data_array['Xpix'][mask] = xpix
self._data_array['Ypix'][mask] = ypix
self._display_data()
def _display_data(self):
array = self._axes_image_threshold.get_array()
array[:, :, :] = 0
channel = self.R
dx = int(self.row * self._percentage)
dy = int(self.col * self._percentage)
# display not displayed
for ix in np.ndindex(self._data_array.shape):
if self._data_array['type'][ix] == 'data':
channel = self.R
elif (self._data_array['type'][ix] == 'xmin') \
| (self._data_array['type'][ix] == 'xmax'):
channel = self.B
elif (self._data_array['type'][ix] == 'ymin') \
| (self._data_array['type'][ix] == 'ymax'):
channel = self.G
x = self._data_array['i'][ix]
y = self._data_array['j'][ix]
if self._data_array['selected'][ix]:
xmask = slice(x - dx*2, x + dx*2 + 1)
ymask = slice(y - dy*2, y + dy*2 + 1)
else:
xmask = slice(x - dx, x + dx + 1)
ymask = slice(y - dy, y + dy + 1)
array[xmask, y, self.alpha] = 1
array[xmask, y, channel] = 1
array[x, ymask, self.alpha] = 1
array[x, ymask, channel] = 1
mask = self._data_array['type'] == 'data'
self._tkvar_npoints.set(mask.sum())
self._axes_image_threshold.set_array(array)
self._refresh()
def _xlog_scale(self):
if self._tkvar_log_xscale.get():
if (self._tkvar_xmin.get() <= 0.0) or (self._tkvar_xmax.get() <= 0.0):
self._tkvar_log_xscale.set(False)
messagebox.showwarning("Warning", "X limits must be greater than 0 in log scales.")
else:
self._measure()
else:
self._measure()
def _ylog_scale(self):
if self._tkvar_log_yscale.get():
if (self._tkvar_ymin.get() <= 0.0) or (self._tkvar_ymax.get() <= 0.0):
self._tkvar_log_yscale.set(False)
messagebox.showwarning("Warning", "Y limits must be greater than 0 in log scales.")
else:
self._measure()
else:
self._measure()
def _xy_pix_limits(self):
mask_xmin = self._data_array['type'] == 'xmin'
mask_xmax = self._data_array['type'] == 'xmax'
mask_ymin = self._data_array['type'] == 'ymin'
mask_ymax = self._data_array['type'] == 'ymax'
if (mask_xmin.sum() == 1.0) and (mask_xmax.sum() == 1.0) and (mask_ymin.sum() == 1.0) and (
mask_ymax.sum() == 1.0):
xpix_min = self._data_array['Xpix'][mask_xmin][0]
xpix_max = self._data_array['Xpix'][mask_xmax][0]
ypix_min = self._data_array['Ypix'][mask_ymin][0]
ypix_max = self._data_array['Ypix'][mask_ymax][0]
else:
raise ValueError('X limits and Y limits must be set.')
return xpix_min, xpix_max, ypix_min, ypix_max
def _xy_values_limits(self):
r"""if an error happens a tk.TclError will be raised."""
try:
xvalue_min = self._tkvar_xmin.get()
xvalue_max = self._tkvar_xmax.get()
yvalue_min = self._tkvar_ymin.get()
yvalue_max = self._tkvar_ymax.get()
except tk.TclError as tclerror:
raise ValueError("Xmin, Xmax, Ymin and Ymax must be floats.") from tclerror
return xvalue_min, xvalue_max, yvalue_min, yvalue_max
def _xy_test_values(self):
r"""if an error happens a tk.TclError will be raised."""
try:
xtext_value = self._tkvar_xtest.get()
ytext_value = self._tkvar_ytest.get()
except tk.TclError as tclerror:
raise ValueError("x and y must be floats.") from tclerror
return xtext_value, ytext_value
def _measure(self):
r"""
x and y positions are indicated as matrix indexes:
row index x is for y axis and column index y is for x axis
"""
flag = False
try:
xpix_min, xpix_max, ypix_min, ypix_max = self._xy_pix_limits()
xvalue_min, xvalue_max, yvalue_min, yvalue_max = self._xy_values_limits()
xpix = self._data_array['Xpix']
ypix = self._data_array['Ypix']
which = 'linear'
unit = f'{self._xunit_entry.get()}/pixel'
if self._tkvar_log_xscale.get():
which = 'log'
unit = f'{self._xunit_entry.get():s}/pixel (log scale)'
trans = Transform(values_min=xvalue_min,
values_max=xvalue_max,
pix_min=xpix_min,
pix_max=xpix_max,
which=which)
self._data_array['x'] = trans.backward(xpix)
msg = f'{trans.backward_scale}' + ' ' + unit
self._ax.set_xlabel(msg)
which = 'linear'
unit = f'{self._yunit_entry.get()}/pixel'
if self._tkvar_log_yscale.get():
which = 'log'
unit = f'{self._yunit_entry.get()}/pixel (log scale)'
trans = Transform(values_min=yvalue_min,
values_max=yvalue_max,
pix_min=ypix_min,
pix_max=ypix_max,
which=which)
self._data_array['y'] = trans.backward(ypix)
msg = f'{trans.backward_scale}' + ' ' + unit
self._ax.set_ylabel(msg)
flag = True
self._refresh()
except ValueError as err:
messagebox.showwarning('Warning', err)
return flag
def _plot_test_data(self):
r"""
x and y positions are indicated as matrix indexes:
row index x is for y axis and column index y is for x axis
"""
flag = False
try:
xpix_min, xpix_max, ypix_min, ypix_max = self._xy_pix_limits()
xvalue_min, xvalue_max, yvalue_min, yvalue_max = self._xy_values_limits()
xtest_value, ytest_value = self._xy_test_values()
which = 'linear'
if self._tkvar_log_xscale.get():
which = 'log'
trans = Transform(values_min=xvalue_min,
values_max=xvalue_max,
pix_min=xpix_min,
pix_max=xpix_max,
which=which)
xpix = trans.forward(xtest_value)
which = 'linear'
if self._tkvar_log_yscale.get():
which = 'log'
trans = Transform(values_min=yvalue_min,
values_max=yvalue_max,
pix_min=ypix_min,
pix_max=ypix_max,
which=which)
ypix = trans.forward(ytest_value)
flag = True
i,j = self._xypix_to_ij(xpix, ypix)
self._add_data(i, j)
self._refresh()
except ValueError as err:
messagebox.showwarning('Warning', err)
return flag
def _save(self):
"""Save data."""
_filepath = filedialog.asksaveasfilename(title='Open Plot',
defaultextension='.txt',
filetypes=[('txt', '.txt'),
('all files', '.*')],
initialdir=self._data_folder,
parent=self)
if len(_filepath) > 0:
filepath = pathlib.Path(_filepath).absolute()
info = version.__package_name__ + "-" + version.__version__
names = list(self._data_array.dtype.names)
names[5] = names[5] + f' /{self._xunit_entry.get()}'
names[6] = names[6] + f' /{self._yunit_entry.get()}'
col_names = '\t'.join(names)
header = '\n'.join((info, col_names))
mask = self._data_array['type'] == 'data'
mask_sort = np.argsort(self._data_array['x'][mask])
sorted_data = self._data_array[mask][mask_sort].copy()
self._data_array[mask][mask_sort] = sorted_data
np.savetxt(filepath, X=self._data_array,
header=header,
fmt=('%s', '%d', '%d', '%d', '%d', '%.6e', '%.6e', '%d'),
delimiter='\t',
comments='#')
self._data_folder = filepath.parent
self._data_name = filepath.name
def _refresh(self):
"""Refresh plot."""
self._canvas.draw()
self._canvas_widget.focus_set()
def _test_linear(self):
"""Test linear scale."""
self._filepath = test_linear()
self._load_image()
def _test_ylog(self):
"""Test semi-log scale."""
self._filepath = test_ylog()
self._load_image()
def _test_xlog(self):
"""Test semi-log scale."""
self._filepath = test_xlog()
self._load_image()
def _test_loglog(self):
"""Test semi-log scale."""
self._filepath = test_loglog()
self._load_image()
def _online_documentation(self):
"""Display online documentation."""
browser = webbrowser.get()
browser.open(self.url)
def _sources(self):
"""Display sources."""
browser = webbrowser.get()
browser.open(self.url_download)
[docs]
def stop(self):
r"""
Stop the main tk loop.
"""
if messagebox.askyesno("Exit", "Do you want to quit the application?"):
section = 'FOLDERS'
self.cfg.set(section=section,
option='image folder',
value=str(self._image_folder))
self.cfg.set(section=section,
option='image name',
value=self._image_name)
self.cfg.set(section=section,
option='data folder',
value=str(self._data_folder))
self.cfg.set(section=section,
option='data name',
value=self._data_name)
settings.save_cfg()
self.master.quit()
self.master.destroy()
[docs]
def run(self):
r"""
Start the application.
"""
self.mainloop()