12 changed files with 575 additions and 187 deletions
			
			
		| @ -0,0 +1,364 @@ | |||||||
|  | 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 = "<POINT " | ||||||
|  |         s += f"X: {self.__x} | Y: {self.__y} | " | ||||||
|  |         s += f"SIZE: {self.__point_size} | " | ||||||
|  |         s += f"COLOR: {self.__color} | " | ||||||
|  |         s += f"VIEWPORT_WIDTH: {self.__viewport_width} | " | ||||||
|  |         s += f"VIEWPORT_HEIGHT: {self.__viewport_height}" | ||||||
|  |         s += ">" | ||||||
|  | 
 | ||||||
|  |         return s | ||||||
|  | 
 | ||||||
|  |     def select(self): | ||||||
|  |         """ | ||||||
|  |         Selects the point. | ||||||
|  |         """ | ||||||
|  |         self.__selected = True | ||||||
|  | 
 | ||||||
|  |     def unselect(self): | ||||||
|  |         """ | ||||||
|  |         Unselects the point. | ||||||
|  |         """ | ||||||
|  |         self.__selected = False | ||||||
|  | 
 | ||||||
|  |     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. 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 | ||||||
					Loading…
					
					
				
		Reference in new issue