Source code for polygon.shape

######################################
#    Created on Jul 5, 2015
#
#    @author: Nathan Qian
#    @author: Grant Mercer
######################################

import random

import matplotlib as mpl
import matplotlib.lines as mlines
import numpy as np
from constants import Plot, TAGS
from log.log import logger
from matplotlib.patches import Polygon
from tools.linearalgebra import tuple_to_nparray, is_intersecting, \
    get_intersection, nparray_to_tuple


# noinspection PyProtectedMember
[docs]class Shape(object): """ Displays the polygon objects onto the canvas by supplying draw methods and maintaining internal information on the shape. Draws to matplotlib backend """ def __init__(self, canvas=None, tag='', color=''): self.__canvas = canvas self.__coordinates = [] self.__tag = tag self.__color = color self.__item_handler = None self.__plot = Plot.baseplot self.__hdf = None self.__attributes = [] self.__note = '' self.__id = None self.__prev_x = 1.0 self.__prev_y = 1.0 self.__lines = [] self.__saved = False self.__selected = False self.__drawing_line = None # temporary line for free draw
[docs] def add_attribute(self, attr): """ Append a passed attribute onto the internal attribute list :param str attr: An attribute enum """ if attr in TAGS: self.__attributes.append(attr) self.__saved = False else: logger.error('Caught invalid attribute for adding \'%s\'' % attr)
[docs] def anchor_rectangle(self, event): """ Establishes a corner of a rectangle as an anchor for when the user drags the cursor to create a rectangle. Used in 'Draw Rect' button :param event: A matplotlib backend event object """ self.__coordinates.append((event.xdata, event.ydata)) self.__prev_x = event.x self.__prev_y = self.__canvas.figure.bbox.height - event.y
[docs] def clear_lines(self): """ Remove any existing lines and clear the shape data. This is called so the lines don't remain on the screen if the user unclicks the toggleable button. """ for line in self.__lines: line.remove()
[docs] def clear_unfinished_data(self): """ In the event the user is plotting points and decides to switch buttons without finishing the free draw polygon, the lines must be cleared and data must be reset. this function will ensure any unfinished data is cleared for future shape drawing. """ if self.__can_draw() != -1: return for line in self.__lines: line.remove() self.__coordinates = []
[docs] def draw(self, fig, fl, plot=Plot.baseplot, fill=False): """ Draw the shape to the canvas, onto the passed figure. Only fill the object if the *fill* parameter is set to ``True`` :param fig: A ``SubplotAxes`` object from the matplotlib backend :param fl: A string representing the HDF path :param plot: ``constants.Plot`` enum specifying which plot the object belongs to :param bool fill: ``False`` for fill, ``True`` for outline """ logger.info("Drawing polygon") # Generates a random color r = lambda: random.randint(0, 255) clr = '#%02X%02X%02X' % (r(), r(), r()) self.__color = clr self.__plot = plot self.__hdf = fl self.__item_handler = \ Polygon(self.__coordinates, facecolor=clr, fill=fill, picker=5) if self.__selected: self.set_highlight(True) fig.add_patch(self.__item_handler)
[docs] def fill_rectangle(self, event, plot, fl, fig, fill=False): """ Draws the rectangle and stores the coordinates of the rectangle internally. Used in 'Draw Rect' button. Forwards argument parameters to ``draw`` :param fig: Figure to draw canvas to :param bool fill: Whether to fill or no fill the shape """ try: self.lastrect except AttributeError: pass else: self.__canvas._tkcanvas.delete(self.lastrect) del self.lastrect if event.xdata is not None and event.ydata is not None: beg = self.__coordinates[0] self.__coordinates.append((event.xdata, beg[1])) self.__coordinates.append((event.xdata, event.ydata)) self.__coordinates.append((beg[0], event.ydata)) self.draw(fig, fl, plot, fill) else: self.__coordinates = []
def generate_lat_range(self): axes = self.__canvas.figure.get_axes() labels = [x.get_xlabel() for x in axes] lat = axes[labels.index(u'Latitude')] time = axes[labels.index(u'Time')] min_ = lat.transData.inverted().transform( time.transData.transform(np.array(min(self.__coordinates))))[0] max_ = lat.transData.inverted().transform( time.transData.transform(np.array(max(self.__coordinates))))[0] return '%.4f - %.4f' % (min_, max_)
[docs] def get_attributes(self): """ Return attributes list maintained by shape :rtype: :py:class:`list` """ return self.__attributes
[docs] def get_color(self): """ Return the hexdecimal color value :rtype: :py:class:`str` """ return self.__color
[docs] def get_coordinates(self): """ Return the list of coordinates internally maintained by shape :rtype: :py:class:`list` """ return self.__coordinates
[docs] def get_id(self): """ Return the database ID of shape :rtype: :py:class:`int` """ return self.__id
[docs] def get_itemhandler(self): """ Return the item handler object to the actual backend base :rtype: :py:class:`matplotlib.patches.polygon` """ return self.__item_handler
def get_max_lat(self): axes = self.__canvas.figure.get_axes() labels = [x.get_xlabel() for x in axes] lat = axes[labels.index(u'Latitude')] time = axes[labels.index(u'Time')] max_ = lat.transData.inverted().transform( time.transData.transform(np.array(max(self.__coordinates))))[0] return max_ def get_min_lat(self): axes = self.__canvas.figure.get_axes() labels = [x.get_xlabel() for x in axes] lat = axes[labels.index(u'Latitude')] time = axes[labels.index(u'Time')] min_ = lat.transData.inverted().transform( time.transData.transform(np.array(min(self.__coordinates))))[0] return min_
[docs] def get_notes(self): """ Return the notes string internally maintained by shape :rtype: :py:class:`str` """ return self.__note
[docs] def get_plot(self): """ Return the plot type :rtype: :py:class:`int` """ return self.__plot
[docs] def get_hdf(self): """ Return the file used :rtype: :py:class:`str` """ return self.__hdf
[docs] def get_saved(self): """ Returns if the shape has been saved or not """ return self.__saved
[docs] def get_tag(self): """ Return the program Tag of shape :rtype: :py:class:`str` """ return self.__tag
def in_x_extent(self, x): time_cords = [pair[0] for pair in self.__coordinates] if min(time_cords) <= x <= max(time_cords): return True else: return False def in_y_extent(self, y): altitude_cords = [pair[1] for pair in self.__coordinates] if min(altitude_cords) <= y <= max(altitude_cords): return True else: return False
[docs] def is_attribute(self, attr): """ Return ``True`` if *attr* is inside the attributes list, ``False`` otherwise. :param str attr: :rtype: :py:class:`bool` """ for item in self.__attributes: if attr == item: logger.info('Found attribute') return True return False
[docs] def is_empty(self): """ Return ``True`` if empty, ``False`` otherwise """ if len(self.__coordinates) == 0: return True return False
[docs] def is_selected(self): """ Return a boolean value based on whether the object is currently highlighted in the figure. Uses ``__selected`` :rtype: :py:class:`bool` """ return self.__selected
[docs] def loaded_draw(self, fig, fill): """ Called in the case of panning the plot, since panning the plot invalidates the previous figure, the figures must first be cleared and the shapes are removed. Loaded draw draws the shapes back into view using a new figure. :param fig: A ``SubplotAxes`` object to add the patch to :param bool fill: Boolean value whether to have the shape filled in when drawn to or not """ self.__item_handler = \ Polygon(self.__coordinates, facecolor=self.__color, fill=fill, picker=5) if self.__selected: self.set_highlight(True) fig.add_patch(self.__item_handler)
[docs] def paint(self, color): """ Changes the color of the shape and saves it internally :param color: the new color of the shape """ self.set_color(color) self.__saved = False
[docs] def plot_point(self, event, plot, fl, fig, fill=False): """ Plot a single point to the shape, connect any previous existing points and fill to a shape if the current coordinate intersects the beginning point. :param event: A ``matplotlib.backend_bases.MouseEvent`` passed object :param plot: an integer indicating which plot it was draw on :param fl: A string representing the HDF it was drawn on :param fig: The figure to be drawing the canvas to :param bool fill: Whether the shape will have a solid fill or not """ self.__coordinates.append((event.xdata, event.ydata)) logger.debug("Plotted point at (%0.5f, %0.5f)", event.xdata, event.ydata) if len(self.__coordinates) > 1: logger.debug("Drawing line from plot") self.__lines.append( mlines.Line2D((self.__prev_x, event.xdata), (self.__prev_y, event.ydata), linewidth=2.0, color='#000000')) fig.add_artist(self.__lines[-1]) self.__canvas.show() if len(self.__coordinates) > 3: index = self.__can_draw() if index > -1: logger.debug("Creating polygon from points") a1 = tuple_to_nparray(self.__coordinates[index]) a2 = tuple_to_nparray(self.__coordinates[index+1]) b1 = tuple_to_nparray(self.__coordinates[-1]) b2 = tuple_to_nparray(self.__coordinates[-2]) x = get_intersection(a1, a2, b1, b2) pair = nparray_to_tuple(x) self.__coordinates[index] = pair del self.__coordinates[:index] self.__coordinates.pop() for line in self.__lines: line.remove() self.__drawing_line.remove() self.__drawing_line = None self.__lines = [] self.draw(fig, fl, plot=plot, fill=fill) self.__plot = plot self.__hdf = fl return True self.__prev_x = event.xdata self.__prev_y = event.ydata
def sketch_line(self, event, fig): if self.__drawing_line: self.__drawing_line.remove() self.__drawing_line = \ mlines.Line2D((self.__prev_x, event.xdata), (self.__prev_y, event.ydata), linewidth=2.0, color='#000000') fig.add_artist(self.__drawing_line) self.__canvas.show() return def close_polygon(self, event, plot, fl, fig, fill=False): if len(self.__coordinates) > 3: index = self.__can_draw() if index > -1: logger.debug("Creating polygon from points") a1 = tuple_to_nparray(self.__coordinates[index]) a2 = tuple_to_nparray(self.__coordinates[index + 1]) b1 = tuple_to_nparray(self.__coordinates[-1]) b2 = tuple_to_nparray(self.__coordinates[-2]) x = get_intersection(a1, a2, b1, b2) pair = nparray_to_tuple(x) self.__coordinates[index] = pair del self.__coordinates[:index] self.__coordinates.pop() for line in self.__lines: line.remove() self.__drawing_line.remove() self.__drawing_line = None self.__lines = [] self.draw(fig, plot, fill) self.__plot = plot self.__hdf = fl return True else: logger.warning('Not enough points')
[docs] def redraw(self, fig, fl, fill): """ Function to draw the shape in the event the shape *may* or *may not* already be drawn. Checks if the image already exists, if not draws the image :param fig: A ``SubplotAxes`` object to add the patch to :param fl: A string representing the HDF file :param bool fill: Boolean value whether to have the shape filled in when drawn or not """ if self.__item_handler is not None and self.__item_handler.is_figure_set(): self.__item_handler.remove() self.__item_handler = \ Polygon(self.__coordinates, facecolor=self.__color, fill=fill, picker=5) if self.__selected: self.set_highlight(True) self.__hdf = fl fig.add_patch(self.__item_handler)
[docs] def remove(self): """ Wrapper function to internally call matplotlib backend to remove the shape from the figure """ if self.__item_handler is None: self.clear_lines() else: self.__item_handler.remove()
[docs] def remove_attribute(self, attr): """ Remove an attribute as specified in ``constants.py`` from the internal attributes variable :param str attr: """ if attr in TAGS: self.__attributes.remove(attr) self.__saved = False else: logger.error('Caught invalid attribute for removal \'%s\'' % attr)
# noinspection PyAttributeOutsideInit
[docs] def rubberband(self, event): """ Draws a temporary helper rectangle that outlines the final shape of the rectangle for the user. This draws to **screen** coordiantes, so backend is not needed here. :param event: A ``matplotlib.backend_bases.MouseEvent`` forwarded object. """ try: self.lastrect except AttributeError: pass else: self.__canvas._tkcanvas.delete(self.lastrect) height = self.__canvas.figure.bbox.height y2 = height-event.y self.lastrect = self.__canvas._tkcanvas.create_rectangle(self.__prev_x, self.__prev_y, event.x, y2)
[docs] def save(self): """ Marks the shape as saved """ self.__saved = True
[docs] def set_attributes(self, attributes_list): """ Set the internal list of attributes to a custom passed list :param list attributes_list: """ for i in attributes_list: if i not in TAGS: logger.error('Caught invalid attribute for setting \'%s\'' % i) return self.__attributes = attributes_list
[docs] def set_color(self, color): """ Set internal color variable :param str color: Valid hexadecimal color value """ self.__color = color
[docs] def set_coordinates(self, coordinates): """ Pass a list of coordinates to set to the shape to. :param list coordinates: """ self.__coordinates = coordinates
[docs] def set_highlight(self, highlight): """ Set the ``linewidth`` and ``linestyle`` attributes of a the internal item handler. Highlights if *highlight* is ``True``, otherwise sets to normal outline. :param bool highlight: """ if highlight: self.__selected = True self.__item_handler.set_linewidth(3.0) self.__item_handler.set_linestyle('dashed') else: self.__selected = False self.__item_handler.set_linewidth(1.0) self.__item_handler.set_linestyle('solid')
[docs] def set_id(self, _id): """ Set the database ID of the shape. **unsafe** to use outside letting database call this. :param int _id: Database primary key """ self.__id = _id
[docs] def set_notes(self, note): """ Pass a string containing new notes to set the shape to :param str note: New note string """ self.__note = note
[docs] def set_plot(self, plot): """ Manually set the new value of the internal plot variable. **unsafe** :param constants.Plot plot: Plot value """ self.__plot = plot
[docs] def set_hdf(self, fl): """ Manually set the value of the internal file variable :param fl: HDF file path """ self.__hdf = fl
[docs] def set_tag(self, tag): """ Set internal tag variable :param str tag: """ self.__tag = tag
def __can_draw(self): if not self.__coordinates: logger.warning('Attempting to ask to draw empty shape, probably just ' + \ 'toggling a button after using free draw? See ticket #92') return -1 b1 = tuple_to_nparray(self.__coordinates[-1]) b2 = tuple_to_nparray(self.__coordinates[-2]) for i in range(len(self.__coordinates)-3): a1 = tuple_to_nparray(self.__coordinates[i]) a2 = tuple_to_nparray(self.__coordinates[i+1]) if is_intersecting(a1, a2, b1, b2): logger.debug("Polygon labeled for draw") return i return -1 def __str__(self): logger.debug('Stringing %s' % self.__tag) time_cords = [mpl.dates.num2date(x[0]).strftime('%H:%M:%S') for x in self.__coordinates] altitude_cords = [x[1] for x in self.__coordinates] string = 'Name:\n\t%s\n' % self.__tag string += 'Time Scale:\n\t%s - %s\n' % (min(time_cords), max(time_cords)) string += 'Latitude Scale:\n\t%s\n' % self.generate_lat_range() string += 'Altitude Scale:\n\t%.4f km - %.4f km\n' % (min(altitude_cords), max(altitude_cords)) string += 'Color:\n\t%s\n' % self.__color if len(self.__attributes) > 0: string += 'Attributes:\n' for item in self.__attributes: string += '\t%s\n' % item if self.__note != '': string += 'Notes:\n\t%s' % self.__note return string