from math import floor class Point: """ A class representing a point. A point has a point_size bounding box around it. """ def __init__(self, x, y, point_size): """ Initializes a new point with a point_size bounding box. @param point_size The size of the point in pixels. @param x The x-coordinate. @param y The y-coordinate. """ self.__point_size = point_size self.__x = x self.__y = y half_point = floor(point_size / 2) self.__top_left_corner = (self.__x - half_point, self.__y + half_point) self.__bottom_right_corner = (self.__x + half_point, self.__y - half_point) @property def x(self): return self.__x @property def y(self): return self.__y @property def point_size(self): return self.__point_size def __eq__(self, other): """ Override for class equality. @param other The other object. """ return (self.__x == other.x and self.__y == other.y and self.__point_size == other.point_size) def __hash__(self): """ Overridden hashing function so it can be used as a dictionary key for attributes. """ return hash((self.__x, self.__y, self.__point_size)) def __repr__(self): return "POINT".format(self.__x, self.__y, self.__point_size) def hit(self, x, y): """ Determines if the point was hit inside of it's bounding box. The condition for hit is simple - consider the following bounding box: ------------- | | | (x,y) | | | ------------- Where the clicked location is in the center. Then the top left corner is defined as (x - half_point_size, y + half_point_size) and the bottom corner is (x + half_point_size, y - half_point_size) So long as x and y are greater than the top left and less than the top right it is considered a hit. This function is necessary for properly deleting and selecting points. """ return (x >= self.__top_left_corner[0] and x <= self.__bottom_right_corner[0] and y <= self.__top_left_corner[1] and y >= self.__bottom_right_corner[1]) class Attribute: def __init__(self, name, value): """ Initializes an attribute. """ self.__name = name self.__value = value class PointSet: """ Useful container for points backed by a set to insure point uniqueness. """ def __init__(self, point_size): """ Initializes a point container with points of size point_size. @param point_size The size of the points. """ self.__points = set() self.__attributes = {} self.__point_size = point_size @property def points(self): """ Getter for points. Returns a generator for looping. """ for point in self.__points: yield point @property def point_size(self): return self.__point_size def add_point(self, x, y, attrs=[]): """ Adds a point in screen coordinates and an optional attribute to the list. @param x The x-coordinate. @param y The y-coordinate. @param attr An optional attribute. """ if attrs != [] and not all(isinstance(x, Attribute) for x in attrs): raise ValueError("Attributes in add_point must be an " + "attribute array.") point = Point(x, y, self.__point_size) self.__points.add(point) self.__attributes[point] = attrs def remove_point(self, x, y): """ Removes a point and it's attributes from the point set based on a bounding box calculation. Removing a point is an exercise is determining which points have been hit, and then pulling them out of the list. If two points have a section overlapping, and the user clicks the overlapped section, both points will be removed. Currently O(n). @param x The x-coordinate. @param y The y-coordinate. """ # Find points that match matched = set(p for p in self.__points if p.hit(x, y)) # In place set difference self.__points = self.__points - matched # Remove associated attributes for point in matched: self.__attributes.pop(point) def attributes(self, point): """ Returns the attribute array for a given point. @param point The point to get attributes for.. """ return self.__attributes[point]