from math import floor
from kmeans . clustering . point import Point as BasePoint
from clusterview2 . colors import Color
from clusterview2 . exceptions import ExceededWindowBoundsError
class Point ( BasePoint ) :
"""
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 , weight = 1.0 ) :
"""
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
# Unfortunately, it appears decorated property methods are not
# inheirited and instead of redoing everything we will just repeat
# the properties here.
self . _x = x
self . _y = y
self . _cluster = None
self . _weight = weight
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 weight ( self ) :
return self . _weight
@weight . setter
def weight ( self , weight ) :
self . _weight = weight
@property
def cluster ( self ) :
return self . _cluster
@cluster . setter
def cluster ( self , cluster ) :
self . _cluster = cluster
@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 " WEIGHT: { self . _weight } | "
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
def clear_points ( self ) :
self . _points = [ ]
@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 , weight = 1.0 , 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 weight The point weight .
@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. " )
if not isinstance ( weight , float ) :
raise ValueError ( " Point weight must be a float. " )
point = Point ( x , y , color , self . _point_size ,
self . _viewport_width , self . _viewport_height , weight )
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