Source code for polygon.manager

######################################
#    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