from math import floor from .colors import Color from .exceptions import ExceededWindowBoundsError # TODO: THIS WILL NEED TO BE MODIFIED TO INHEIRIT THE KMEANS POINT CLASS. class Point: """ A class representing a point. A point has a point_size bounding box around it. """ def __init__(self, x, y, color, point_size, viewport_width, viewport_height): """ Initializes a new point with a point_size bounding box, viewport awareness, and a color. Initialized with additional viewport data to make sure the move function refuses to move a point outside the screen. @param x The x-coordinate. @param y The y-coordinate. @param color The color of the point. @param point_size The size of the point in pixels. @param viewport_width The width of the viewport. @param viewport_height The height of the viewport. """ if not isinstance(color, Color): raise ValueError("Point must be initialized with a color of " + "type Color.") self.__point_size = point_size self.__x = x self.__y = y self.__color = color self.__viewport_width = viewport_width self.__viewport_height = viewport_height self.__calculate_hitbox() self.__check_window_bounds(x, y) self.__selected = False self.__attributes = [] @property def x(self): return self.__x @property def y(self): return self.__y @property def point_size(self): return self.__point_size @property def selected(self): return self.__selected @property def color(self): return self.__color @color.setter def color(self, color): if not isinstance(color, Color): raise ValueError('Point color must be of type Color.') self.__color = color @property def attributes(self): return self.__attributes def add_attribute(self, attr): self.__attributes.append(attr) def __calculate_hitbox(self): """ Calculates the hit box for the point given the current position (center) and the point size. """ half_point = floor(self.point_size / 2.0) self.__top_left_corner = (self.__x - half_point, self.__y + half_point) self.__bottom_right_corner = (self.__x + half_point, self.__y - half_point) def __check_window_bounds(self, x, y): """ Simple window bound check that raises an exception when the point (x, y) exceeds the known viewport bounds. @param x The x-coordinate under test. @param y The y-coordinate under test. @raises ExceededWindowBoundsError If the viewport bounds are exceeded. """ half_point = floor(self.point_size / 2.0) # Screen size in pixels is always positive # We need to include the half point here because # the (x, y) for a point is the center of the square and we # do not want the EDGES to exceed the viewport bounds. if (x > self.__viewport_width - half_point or y > self.__viewport_height - half_point or x < half_point or y < half_point): raise ExceededWindowBoundsError def move(self, dx, dy): """ Adds the deltas dx and dy to the point. @param dx The delta in the x direction. @param dy The delta in the y direction. """ self.__check_window_bounds(self.__x + dx, self.__y + dy) self.__x += dx self.__y += dy # It's important to note as we move the point we need to # make sure we are constantly updating it's hitbox. self.__calculate_hitbox() 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.__color == other.color and self.__attributes == other.attributes and self.__point_size == other.point_size) def __repr__(self): # For some reason I had to split this instead of using one giant # string chained with `+` inside of `()`. s = "= 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. Since points are not hashable (they are modified in place by move) we are forced to back the PointSet with an array. However, it is still a "set" in the "uniqueness among all points" sense because `add_point` will reject a point with a duplicate center. """ def __init__(self, point_size, viewport_width, viewport_height): """ Initializes a point container with points of size point_size. @param point_size The size of the points. @param viewport_width The width of the viewport for bounds calculations. @param viewport_height The height of the viewport for bounds calculations. """ self.__points = [] self.__point_size = point_size self.__viewport_width = viewport_width self.__viewport_height = viewport_height def __eq__(self, other): other_points = list(other.points) return (self.__points == other_points and self.__point_size == other.point_size and self.__viewport_width == other.viewport_width and self.__viewport_height == other.viewport_height) def __repr__(self): s = [] for p in self.__points: s.append(str(p)) return ",".join(s) def clear(self): self.__points = [] @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 @property def viewport_height(self): return self.__viewport_height @property def viewport_width(self): return self.__viewport_width @viewport_height.setter def viewport_height(self, height): self.__viewport_height = height @viewport_width.setter def viewport_width(self, width): self.__viewport_width = width def empty(self): return len(self.__points) == 0 def clear_selection(self): """ Handy helper function to clear all selected points. """ for p in self.__points: p.unselect() def add_point(self, x, y, color, 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 color The color of the point. @param attr An optional attribute. @raises ExceededWindowBoundsError If the point could not be constructed because it would be outside the window bounds. """ if attrs != [] and not all(isinstance(x, Attribute) for x in attrs): raise ValueError("Attributes in add_point must be an " + "attribute array.") if not isinstance(color, Color): raise ValueError("Point color must be a Color enum.") point = Point(x, y, color, self.__point_size, self.__viewport_width, self.__viewport_height) for attr in attrs: point.add_attribute(attr) if point in self.__points: # Silently reject a duplicate point (same center). return self.__points.append(point) def remove_point(self, x, y): """ Removes a point 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. """ for p in self.__points: if p.hit(x, y): self.__points.remove(p) def groups(self): """ Returns a map from color to point representing each point's group membership based on color. """ g = {} for p in self.__points: if p.color not in g: # Create the key for the group color since it does # not exist. g[p.color] = [] g[p.color].append(p) return g