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