|
|
|
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:
|
|
|
|
|
|
|
|
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]
|