|
|
@ -1,6 +1,101 @@ |
|
|
|
|
|
|
|
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<X :{} Y: {} SIZE: {}>".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: |
|
|
|
class Attribute: |
|
|
|
__name = None |
|
|
|
|
|
|
|
__value = None |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def __init__(self, name, value): |
|
|
|
def __init__(self, name, value): |
|
|
|
""" |
|
|
|
""" |
|
|
@ -12,11 +107,35 @@ class Attribute: |
|
|
|
|
|
|
|
|
|
|
|
class PointSet: |
|
|
|
class PointSet: |
|
|
|
""" |
|
|
|
""" |
|
|
|
Useful point set for storing coordinates and attributes. It is |
|
|
|
Useful container for points backed by a set to insure point |
|
|
|
backed by a set to provide nice convenience functions. |
|
|
|
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. |
|
|
|
""" |
|
|
|
""" |
|
|
|
__points = set() |
|
|
|
for point in self.__points: |
|
|
|
__attributes = {} |
|
|
|
yield point |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@property |
|
|
|
|
|
|
|
def point_size(self): |
|
|
|
|
|
|
|
return self.__point_size |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def add_point(self, x, y, attrs=[]): |
|
|
|
def add_point(self, x, y, attrs=[]): |
|
|
|
""" |
|
|
|
""" |
|
|
@ -32,34 +151,43 @@ class PointSet: |
|
|
|
raise ValueError("Attributes in add_point must be an " + |
|
|
|
raise ValueError("Attributes in add_point must be an " + |
|
|
|
"attribute array.") |
|
|
|
"attribute array.") |
|
|
|
|
|
|
|
|
|
|
|
point = (x, y) |
|
|
|
point = Point(x, y, self.__point_size) |
|
|
|
self.__points.add(point) |
|
|
|
self.__points.add(point) |
|
|
|
self.__attributes[point] = attrs |
|
|
|
self.__attributes[point] = attrs |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def remove_point(self, x, y): |
|
|
|
def remove_point(self, x, y): |
|
|
|
""" |
|
|
|
""" |
|
|
|
Removes a point and it's attributes from the point set. |
|
|
|
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. |
|
|
|
""" |
|
|
|
""" |
|
|
|
point = (x, y) |
|
|
|
|
|
|
|
self.__points.discard(point) |
|
|
|
# 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) |
|
|
|
self.__attributes.pop(point) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def attributes(self, x, y): |
|
|
|
def attributes(self, point): |
|
|
|
""" |
|
|
|
""" |
|
|
|
Returns the attribute array for a given point. |
|
|
|
Returns the attribute array for a given point. |
|
|
|
|
|
|
|
|
|
|
|
@param x The x-coordinate of the point. |
|
|
|
@param point The point to get attributes for.. |
|
|
|
@param y The y-coordinate of the point. |
|
|
|
|
|
|
|
""" |
|
|
|
""" |
|
|
|
return self.__attributes[(x, y)] |
|
|
|
return self.__attributes[point] |
|
|
|
|
|
|
|
|
|
|
|
@property |
|
|
|
|
|
|
|
def points(self): |
|
|
|
|
|
|
|
""" |
|
|
|
|
|
|
|
Getter for points. Returns a generator for |
|
|
|
|
|
|
|
looping. |
|
|
|
|
|
|
|
""" |
|
|
|
|
|
|
|
for point in self.__points: |
|
|
|
|
|
|
|
yield point |
|
|
|
|
|
|
|