######################################
# Created on Jul 5, 2015
#
# @author: Nathan Qian
# @author: Grant Mercer
######################################
import constants
import matplotlib as mpl
import tkMessageBox
from constants import DATEFORMAT, Plot, CONF
from datetime import datetime
from polygon.reader import ShapeReader
from polygon.shape import Shape
from propertiesdialog import PropertyDialog
from db import db
from log.log import logger
[docs]class ShapeManager(object):
"""
Manages all shapes present on the screen, writes to database on
call and provides other export functionality
"""
outline_toggle = True # global var for setting fill of shapes
hide_toggle = True # global var for hiding shapes
shape_count = db.query_unique_tag() # global shape_count for initial shape tag
def __init__(self, figure, canvas, master):
self.__figure = figure # figure to draw to
self.__canvas = canvas # canvas the figure lives on
self.__master = master # CALIPSO
self.__current_plot = Plot.baseplot # default plot
logger.info('Defining initial shape manager')
self.__shape_list = [[Shape(canvas)], # baseplot
[Shape(canvas)], # backscattered
[Shape(canvas)], # depolarized
[Shape(canvas)], # vfm
[Shape(canvas)], # iwp
[Shape(canvas)], # horiz_avg
[Shape(canvas)]] # aerosol_subtype
logger.info("Instantiating Exporting Reader")
self.__current_list = None # aliases shape_list's current plot
self.__current_file = '' # current JSON file, NOT .hdf file!
self.__hdf = '' # hdf file
self.__shapereader = ShapeReader() # internal reader object for exporting
self.__data = {} # data to hold JSON data for exporting
logger.info("Querying database for unique tag")
self.__selected_shapes = [] # shapes that are currently selected
self.__drawing = False # is a free draw shape being drawn now?
[docs] def anchor_rectangle(self, event):
"""
Informs the correct shape list's blank object to plot a corner of a rectangle.
:param event: A backend passed ``matplotlib.backend_bases.MouseEvent`` object
"""
if self.__current_plot == Plot.baseplot:
logger.warning("Cannot draw to BASE_PLOT")
return
if event.xdata and event.ydata:
logger.info('Anchoring %d, %d' % (event.xdata, event.ydata))
self.__current_list[-1].anchor_rectangle(event)
else:
logger.error('Anchor selected is out of range, skipping')
[docs] def clear_lines(self):
"""
Clear any existing lines or unfilled shapes when the 'Free Draw' button
is unpressed. This is fix a bug that is caused by polygons not being
finished but corrupting future shapes.
"""
if self.__current_plot == Plot.baseplot:
return
self.__current_list[-1].clear_unfinished_data()
self.__canvas.show()
[docs] def clear_refs(self):
"""
Clear all references to the current figure, this is called
in the ``Calipso`` class when a plot is to be set as to ensure
no dangling references are left
"""
for shape in self.__current_list[:-1]:
ih = shape.get_itemhandler()
if ih is not None:
ih.remove()
[docs] def delete(self, event):
"""
Delete the specified object from the screen, searches through the
current list to find the artist that was clicked on
:param event: A passed ``matplotlib.backend_bases.PickEvent`` object
"""
shape = event.artist
for item in self.__current_list:
poly = item.get_itemhandler()
if poly == shape:
logger.info('Deleting %s' % item.get_tag())
self.__current_list.remove(item)
break
shape.remove()
self.__canvas.show()
# noinspection PyProtectedMember
[docs] def fill_rectangle(self, event):
"""
Informs the correct shape list's blank object to draw a rectangle to the screen
using the provided coordinates
:param event: A backend passed ``matplotlib.backend_bases.MouseEvent`` object
"""
if self.__current_plot == Plot.baseplot:
logger.warning("Cannot draw to BASE_PLOT")
return
if event.xdata and event.ydata:
if len(self.__current_list[-1].get_coordinates()) is 0:
return
logger.debug('Filling: %d, %d' % (event.xdata, event.ydata))
logger.info('Creating rectangle')
self.__current_list[-1].fill_rectangle(event, self.__current_plot, self.__hdf,
self.__figure, ShapeManager.outline_toggle)
self.__current_list[-1].set_tag(self.generate_tag())
self.__current_list.append(Shape(self.__canvas))
self.__canvas.show()
else:
logger.error('Bounds out of plot range, skipping')
self.__current_list[-1].set_coordinates([])
self.__canvas._tkcanvas.delete(self.__current_list[-1].lastrect)
[docs] def find_shape(self, event):
"""
Return the handle to the shape found via the user clicking on one
:param event: A passed ``matplotlib.backend_bases.PickEvent`` object
"""
target = event.artist
for shape in self.__current_list:
if shape.get_itemhandler() is target:
return shape
@staticmethod
[docs] def generate_tag():
"""
Produces a unique tag for each shape for each session
:rtype: :py:class:`str`
"""
string = "shape" + str(ShapeManager.shape_count)
ShapeManager.shape_count += 1
return string
[docs] def get_count(self):
"""
Get the total amount of objects in existence inside ShapeManager, adds
all lists up and subtracts the empty objects that are always appended
to the end of the lists.
:rtype: :py:class:`int`
"""
return len(self.__shape_list[0]) + len(self.__shape_list[1]) + \
len(self.__shape_list[2]) + len(self.__shape_list[3]) + len(self.__shape_list[4]) + \
len(self.__shape_list[5]) + len(self.__shape_list[6]) - 7
[docs] def get_current_list(self):
"""
Return the current list
.. warning::
This function should **never** be used for any write operation. Using
this function should be for **read only**.
:rtype: :py:class:`list`
"""
return self.__current_list
[docs] def get_hdf(self):
"""
Return the hdf string that is currently being used
:rtype: :py:class:`str`
"""
return self.__hdf
[docs] def get_filename(self):
"""
Return JSON filename string
:rtype: :py:class:`str`
"""
return self.__current_file
[docs] def get_selected_count(self):
"""
Get the total amount of *selected* objects in existence inside ShapeManager
"""
return len(self.__selected_shapes)
[docs] def hide(self):
"""
Hide all current shapes on the plot
"""
logger.info('Settings hide option for all shapes to %s' % str(ShapeManager.hide_toggle))
ShapeManager.hide_toggle = not ShapeManager.hide_toggle
for shape in self.__current_list:
poly = shape.get_itemhandler()
if poly is not None and ShapeManager.hide_toggle:
color = shape.get_color()
poly.set_fill(True)
poly.set_facecolor(color)
poly.set_edgecolor('#000000')
elif poly is not None and not ShapeManager.hide_toggle:
poly.set_fill(False)
poly.set_facecolor('none')
poly.set_edgecolor('none')
self.__canvas.show()
[docs] def is_all_saved(self, plot=None):
"""
Checks if all the shapes have been saved. If plot is None, the method
will check if all shapes in every plot has been saved. If a plot is
specified, then it will only check the shapes in the specified plot.
This method will automatically ignore the last blank shapes.
:param plot: the plot of the shapes to check
"""
if plot is None:
for i in range(len(self.__shape_list)):
for j in range(len(self.__shape_list[i])-1):
if not self.__shape_list[i][j].get_saved():
return False
return True
else:
for i in range(len(self.__shape_list[plot.value])-1):
if not self.__shape_list[plot.value][i].get_saved():
return False
return True
[docs] def outline(self):
"""
Toggle whether current shapes should be outlined or remained filled on
the screen
"""
logger.info('setting all shape fill to %s' % str(ShapeManager.outline_toggle))
ShapeManager.outline_toggle = not ShapeManager.outline_toggle
for shape in self.__current_list:
poly = shape.get_itemhandler()
if poly is not None and ShapeManager.outline_toggle:
poly.set_fill(True)
poly.set_linewidth(1.0)
elif poly is not None and not ShapeManager.outline_toggle:
poly.set_fill(False)
poly.set_linewidth(2.0)
self.__canvas.show()
[docs] def plot_point(self, event):
"""
Plot a single point to the screen for the current shape object,
if other points exist, a line is drawn between then until a
polygon is formed
:param event: A ``matplotlib.backend_bases.MouseEvent`` passed object
"""
if self.__current_plot == Plot.baseplot:
logger.warning('Cannot draw to the base plot')
return
if event.xdata and event.ydata:
logger.info('Plotting point at %.5f, %.5f' % (event.xdata, event.ydata))
check = self.__current_list[-1].plot_point(event, self.__current_plot, self.__hdf,
self.__figure, ShapeManager.outline_toggle)
self.__drawing = True
if check:
self.__current_list[-1].set_tag(self.generate_tag())
self.__current_list.append(Shape(self.__canvas))
self.__drawing = False
self.__canvas.show()
else:
logger.error("Point to plot is out or range, skipping")
def sketch_line(self, event):
if self.__drawing:
self.__current_list[-1].sketch_line(event, self.__figure)
# noinspection PyProtectedMember
[docs] def properties(self, event):
"""
Return the properties.rst of the shape clicked on by the user and create a small
tooltip which displays these properties.rst
:param event: A passed ``matplotlib.backend_bases.PickEvent`` object
"""
target = event.artist
logger.debug("Creating property window")
for shape in self.__current_list:
if shape.get_itemhandler() is target:
# if self.property_window is not None:
# self.destroy_property_window()
PropertyDialog(self.__master.get_root(), shape)
return
logger.warning("Shape not found")
[docs] def read_plot(self, filename='', read_from_str=''):
"""
Reads shapes from either a string or a file in JSON format, and packs the screen
with the shapes parsed. **note:** if a string is passed as *well* as a filename,
the string takes priority
:param str filename: The filename to read valid JSON shapes from
:param str read_from_str: The string to read valid JSON shapes from
"""
if read_from_str != '':
logger.info('Reading JSON from string')
read_data = self.__shapereader.read_from_str_json(read_from_str)
else:
logger.info('Reading JSON from file')
self.__shapereader.set_filename(filename)
read_data = self.__shapereader.read_from_file_json()
# The index [-25:-4] is used so that we only check the time/space of the files, not type
if self.__hdf.rpartition('/')[2][-25:-4] != read_data['hdffile'][-25:-4]: # Do HDF files match?
tkMessageBox.showerror(
'file', 'Shape-associated HDF file \n and current HDF do not match')
logger.error('Shape-associated HDF file and current HDF do not match')
return
for key in constants.plot_type_enum:
# If persistent shapes are used, we want to only load them into backscattered
if CONF.persistent_shapes:
lst = self.__shape_list[constants.plot_type_enum['backscattered']]
else:
lst = self.__shape_list[constants.plot_type_enum[key]]
self.__shapereader.pack_shape(lst, key, self.__canvas, read_from_str)
# The "or CONF.persistent_shapes" allows shapes that don't match the plot to be shown
if self.__current_plot == constants.plot_type_enum[key] or CONF.persistent_shapes:
for shape in lst:
if not shape.is_empty():
logger.info('Shape found in \'%s\', drawing' % key)
shape.redraw(self.__figure, read_data['hdffile'],
ShapeManager.outline_toggle)
self.__canvas.show()
[docs] def reset(self, all_=False):
"""
Clear the screen of any shapes present from the current_list
"""
if all_:
logger.info('clearing all shapes')
self.__shape_list = [[Shape(self.__canvas)], # baseplot
[Shape(self.__canvas)], # backscattered
[Shape(self.__canvas)], # depolarized
[Shape(self.__canvas)], # vfm
[Shape(self.__canvas)], # iwp
[Shape(self.__canvas)], # horiz_avg
[Shape(self.__canvas)]] # aerosol_subtype
else:
logger.info('Resetting ShapeManager')
for shape in self.__current_list:
if not shape.is_empty():
shape.remove()
self.__canvas.show()
idx = self.__shape_list.index(self.__current_list)
self.__shape_list[idx] = [Shape(self.__canvas)]
self.__current_list = self.__shape_list[idx]
[docs] def rubberband(self, event):
"""
Uses a blank shape to draw 'helper rectangles' that outline the final shape of the
object. wrapper function for calling :py:class:`polygon.Shape` method.
:param event: A backend passes ``matplotlib.backend_bases.MouseEvent`` object
"""
if event.button == 1:
if self.__current_plot == Plot.baseplot:
logger.warning("Cannot draw to BASE_PLOT")
return
if len(self.__current_list[-1].get_coordinates()) is 0:
return
self.__current_list[-1].rubberband(event)
[docs] def save_db(self, only_selected=False):
"""
Commit all polygons currently in display to the database. Existing database
objects will simply be updated, while objects not present in the database
will be assigned a new primary key and have an entry generated for them.
Returns ``True`` if success, ``False`` otherwise
:rtype: :py:class:`bool`
"""
if len(self.__current_list) == 1:
logger.error('No shapes found')
return False
today = datetime.utcnow().replace(microsecond=0)
if(only_selected):
db.commit_to_db(self.__selected_shapes, today)
else:
# Must account for dummy object at end of current list
db.commit_to_db(self.__current_list[:-1], today)
return True
[docs] def save_json(self, filename=''):
"""
Save all shapes selected on the screen to a specified JSON object,
if no file is passed the internal file variable is used. There should **never**
arise a case where no file is passed either from the internal or external
parameters, ``Calipso`` has proper error checking.
:param str filename: custom filename to save JSON objects to
"""
if filename != '':
self.__current_file = filename
if not self.__selected_shapes:
logger.warning('No shapes selected, saving empty plot')
today = datetime.utcnow().replace(microsecond=0)
self.__data['time'] = str(today)
self.__data['hdffile'] = self.__hdf.rpartition('/')[2]
shape_dict = {}
for i in range(len(self.__shape_list)):
self.__data[constants.PLOTS[i]] = {}
i = self.__shape_list.index(self.__current_list)
for j in range(len(self.__selected_shapes)):
if not self.__selected_shapes[j].get_saved():
self.__selected_shapes[j].save()
tag = self.__selected_shapes[j].get_tag()
coordinates = self.__selected_shapes[j].get_coordinates()
color = self.__selected_shapes[j].get_color()
attributes = self.__selected_shapes[j].get_attributes()
note = self.__selected_shapes[j].get_notes()
_id = self.__selected_shapes[j].get_id()
time_cords = [mpl.dates.num2date(x[0]) for x in coordinates]
alt_cords = [x[1] for x in coordinates]
blat = self.__selected_shapes[j].get_min_lat()
elat = self.__selected_shapes[j].get_max_lat()
btime = min(time_cords).strftime(DATEFORMAT)
etime = max(time_cords).strftime(DATEFORMAT)
balt = min(alt_cords)
ealt = max(alt_cords)
value = {'coordinates': coordinates, 'blat': blat, 'elat': elat,
'btime': btime, 'etime': etime, 'balt': balt, 'ealt': ealt,
'color': color, 'attributes': attributes, 'notes': note, 'id': _id}
shape_dict[tag] = value
self.__data[constants.PLOTS[i]] = shape_dict
logger.info('Encoding to JSON')
db.encode(self.__current_file, self.__data)
[docs] def save_all_json(self, filename=""):
"""
Same as ``save_json``, but save **all** shapes across **all** plots instead.
:param str filename: custom filename to save JSON objects to
"""
logger.info("Saving all shapes to JSON")
if filename is not None:
self.__current_file = filename
today = datetime.utcnow().replace(microsecond=0)
self.__data['time'] = str(today)
self.__data['hdffile'] = self.__hdf.rpartition('/')[2]
for i in range(len(self.__shape_list)):
shape_dict = {}
for j in range(len(self.__shape_list[i])-1):
if not self.__shape_list[i][j].get_saved():
self.__shape_list[i][j].save()
tag = self.__shape_list[i][j].get_tag()
coordinates = self.__shape_list[i][j].get_coordinates()
lat = self.__shape_list[i][j].generate_lat_range()
color = self.__shape_list[i][j].get_color()
attributes = self.__shape_list[i][j].get_attributes()
note = self.__shape_list[i][j].get_notes()
_id = self.__shape_list[i][j].get_id()
value = {'coordinates': coordinates, 'lat': lat, 'color': color,
'attributes': attributes, 'notes': note, 'id': _id}
shape_dict[tag] = value
self.__data[constants.PLOTS[i]] = shape_dict
logger.info('Encoding to JSON')
db.encode(self.__current_file, self.__data)
[docs] def select_all(self):
"""
Set all objects within the current list as selected. Loops through all
shapes in the plot and sets their highlight as well as adding them to
the internal selected list
"""
logger.info('Selecting %d shapes', len(self.__current_list)-1)
for i in self.__current_list[:-1]:
i.set_highlight(True)
self.__selected_shapes = (self.__current_list[:-1])
self.__canvas.show()
[docs] def deselect_all(self):
"""
Remove selection from all objects on screen. Loops through all shapes
in the plot and sets their highlight to default and resets the internal
selected list
"""
logger.info('Deselecting %d shapes', len(self.__current_list)-1)
for i in self.__current_list[:-1]:
i.set_highlight(False)
self.__selected_shapes = []
self.__canvas.show()
[docs] def select_from_tag(self, tag):
"""
Highlight the shape specified by ``tag``. Ensures to reset
any other objects that may be highlighted. Not to be confused
with ``select(self, event)``, which is for multiple selections
via event objects
:param str tag: The tag of the object
"""
if tag == "" and self.__selected_shapes:
logger.info('Disabling selection for all shapes')
for x in self.__selected_shapes:
# The shape may have been removed, so we should ensure it exists
if x: x.set_highlight(False)
self.__selected_shapes = []
self.__canvas.show()
return
for shape in self.__current_list[:-1]:
if shape.get_tag() == tag:
logger.info('Selecting %s' % tag)
for x in self.__selected_shapes:
if x: x.set_highlight(False)
self.__selected_shapes.append(shape)
shape.set_highlight(True)
break
self.__canvas.show()
[docs] def select_from_event(self, event):
"""
Highlight the selected object and add to internal list of highlighted objects
:param event: A passed ``matplotlib.backend_bases.PickEvent`` object
"""
shape = event.artist
for item in self.__current_list:
poly = item.get_itemhandler()
if poly == shape:
if not item.is_selected():
logger.info('Selecting %s' % item.get_tag())
item.set_highlight(True)
self.__selected_shapes.append(item)
else:
logger.info('Deselecting %s' % item.get_tag())
item.set_highlight(False)
self.__selected_shapes.remove(item)
break
self.__canvas.show()
[docs] def set_current(self, plot, fig):
"""
Set the current view to ``plot``, and draw any shapes that exist in the manager for
this plot. This is called each time a new view is rendered to the screen by
``set_plot`` in *Calipso*
:param int plot: Acceptable plot constant from ``constants.py``
"""
logger.debug('Settings plot to %s' % plot)
self.__figure = fig
self.set_plot(plot)
# Check if persistent shapes, use backscatter as the shapes list if so
if CONF.persistent_shapes:
self.__current_list = self.__shape_list[Plot.backscattered]
if len(self.__current_list) > 1:
logger.info('Redrawing shapes')
for shape in self.__current_list[:-1]:
if not shape.is_empty():
shape.loaded_draw(self.__figure, ShapeManager.outline_toggle)
self.__canvas.show()
[docs] def set_hdf(self, hdf_filename):
"""
Set the internal HDF filename variable
:param str hdf_filename: Name of new HDF filename
"""
self.__hdf = hdf_filename
[docs] def set_plot(self, plot):
"""
Determine which list current_list should alias, also set internal plot
variable
:param constants.Plot plot: Acceptable plot constant from ``constants.py``
"""
if plot == Plot.baseplot:
logger.warning('set_plot called for BASE_PLOT')
self.__current_list = self.__shape_list[Plot.baseplot]
self.__current_plot = Plot.baseplot
elif plot == Plot.backscattered:
logger.info('set_plot to BACKSCATTERED')
self.__current_list = self.__shape_list[Plot.backscattered]
self.__current_plot = Plot.backscattered
elif plot == Plot.depolarized:
logger.info('set_plot to DEPOLARIZED')
self.__current_list = self.__shape_list[Plot.depolarized]
self.__current_plot = Plot.depolarized
elif plot == Plot.vfm:
logger.info('set_plot to VFM')
self.__current_list = self.__shape_list[Plot.vfm]
self.__current_plot = Plot.vfm
elif plot == Plot.iwp:
logger.info('set_plot to IWP')
self.__current_list = self.__shape_list[Plot.iwp]
self.__current_plot = Plot.iwp
elif plot == Plot.horiz_avg:
logger.info('set_plot to HORIZ_AVG')
self.__current_list = self.__shape_list[Plot.horiz_avg]
self.__current_plot = Plot.horiz_avg
elif plot == Plot.horiz_avg:
logger.info('set_plot to AEROSOL SUBTYPE')
self.__current_list = self.__shape_list[Plot.aerosol_subtype]
self.__current_plot = Plot.aerosol_subtype