import random
from PyQt5 . QtCore import QEvent , Qt
from PyQt5 . QtGui import QCursor
from PyQt5 . QtWidgets import QErrorMessage , QInputDialog
from kmeans . algorithms import k_means
from clusterview2 . colors import Color
from clusterview2 . exceptions import ExceededWindowBoundsError
from clusterview2 . mode import Mode
from clusterview2 . ui . opengl_widget import ( set_drawing_event , set_move_bb_top_left ,
set_move_bb_bottom_right , reset_move_bbs ,
viewport_height , viewport_width )
from clusterview2 . point_manager import PointManager
class _ClickFlag :
# This is the first stage. On mouse release it goes to
# SELECTION_MOVE.
NONE = 0
# We are now in selection box mode.
SELECTION_BOX = 1
# Second stage - we have selected a number of points
# and now we are going to track the left mouse button
# to translate those points. After a left click
# this moves to SELECTED_MOVED.
SELECTION_MOVE = 2
# Any subsequent click in this mode will send it back
# to NONE - we are done.
SELECTED_MOVED = 3
# GLOBALS
# Canvas pixel border - empirical, not sure where this is stored officially
_CANVAS_BORDER = 1
# Module level flag for left click events (used to detect a left
# click hold drag)
_left_click_flag = _ClickFlag . NONE
# Variable to track the mouse state during selection movement
_last_mouse_pos = None
# Used to implement mouse dragging when clicked
_left_click_down = False
# TODO: WHEN THE GROUPING ENDS AND THE USER CANCELS THE CENTROID COUNT
# SHOULD BE ZEROED AND REMAINING COLORS SHOULD BE REPOPULATED.
# Count of centroids for comparison with the spin widget
_centroid_count = 0
_remaining_colors = [ c for c in Color if c not in [ Color . BLUE , Color . GREY ] ]
def refresh_point_list ( ctx ) :
"""
Refreshes the point list display .
@param ctx A handle to the window context .
"""
# In order to make some guarantees and avoid duplicate
# data we will clear the point list widget and re-populate
# it using the current _point_set.
ctx . point_list_widget . clear ( )
for p in PointManager . point_set . points :
ctx . point_list_widget . addItem ( f " ( { p . x } , { p . y } ) | Weight: { p . weight } " )
ctx . point_list_widget . update ( )
num_of_points = len ( list ( PointManager . point_set . points ) )
ctx . number_of_points_label . setText ( str ( num_of_points ) )
def _handle_add_point ( ctx , event ) :
"""
Event handler for the add point mode .
Sets the drawing mode for the OpenGL Widget using
` set_drawing_mode ` , converts a point to our point
representation , and adds it to the list .
@param ctx A context handle to the main window .
@param event The click event .
"""
# Update information as needed
_handle_info_updates ( ctx , event )
if ( event . button ( ) == Qt . LeftButton and
event . type ( ) == QEvent . MouseButtonPress ) :
# At this point we can be sure resize_gl has been called
# at least once, so set the viewport properties of the
# point set so it knows the canvas bounds.
PointManager . point_set . viewport_width = viewport_width ( )
PointManager . point_set . viewport_height = viewport_height ( )
# Clear any existing selections
PointManager . point_set . clear_selection ( )
try :
# No attribute at the moment, default point color is Color.GREY.
PointManager . point_set . add_point ( event . x ( ) , event . y ( ) , Color . GREY )
except ExceededWindowBoundsError :
# The user tried to place a point whos edges would be
# on the outside of the window. We will just ignore it.
return
refresh_point_list ( ctx )
set_drawing_event ( event )
ctx . opengl_widget . update ( )
ctx . point_list_widget . update ( )
def _handle_edit_point ( ctx , event ) :
_handle_info_updates ( ctx , event )
PointManager . point_set . clear_selection ( )
if ( event . button ( ) == Qt . LeftButton and
event . type ( ) == QEvent . MouseButtonPress ) :
# See if a point was hit
point = None
for p in PointManager . point_set . points :
if p . hit ( event . x ( ) , event . y ( ) ) :
point = p
break
# Get point weight from user and assign it to the point.
if point is not None :
value , ok = QInputDialog . getDouble ( None , ' Weight ' , ' Weight(Float): ' , 1 , 1 , 3000 , 1 )
if ok :
if not isinstance ( value , float ) :
error_dialog = QErrorMessage ( )
error_dialog . showMessage ( ' Point weight must be a floating point value. ' )
error_dialog . exec_ ( )
else :
point . weight = value
# Store old x, y from event
set_drawing_event ( event )
ctx . update ( )
refresh_point_list ( ctx )
def ogl_keypress_handler ( ctx , event ) :
"""
A keypress handler attached to the OpenGL widget .
It primarily exists to allow the user to cancel selection .
Also allows users to escape from modes .
@param ctx A handle to the window context .
@param event The event associated with this handler .
"""
global _left_click_flag
global _last_mouse_pos
if event . key ( ) == Qt . Key_Escape :
if ctx . mode is Mode . MOVE :
if _left_click_flag is not _ClickFlag . NONE :
_last_mouse_pos = None
_left_click_flag = _ClickFlag . NONE
PointManager . point_set . clear_selection ( )
reset_move_bbs ( )
refresh_point_list ( ctx )
elif ctx . mode is not Mode . OFF :
ctx . mode = Mode . OFF
# Also change the mouse back to normal
ctx . opengl_widget . setCursor ( QCursor ( Qt . CursorShape . ArrowCursor ) )
ctx . status_bar . showMessage ( " " )
ctx . opengl_widget . update ( )
def _handle_move_points ( ctx , event ) :
"""
A relatively complicated state machine that handles the process of
selection , clicking , and dragging .
@param ctx The context to the window .
@param event The event .
"""
global _left_click_flag
global _left_mouse_down
global _last_mouse_pos
set_drawing_event ( event )
_handle_info_updates ( ctx , event )
# If we release the mouse, we want to quickly alert drag mode.
if ( event . button ( ) == Qt . LeftButton and
event . type ( ) == QEvent . MouseButtonRelease ) :
_left_mouse_down = False
# This if statement block is used to set the bounding box for
# drawing and call the selection procedure.
if ( event . button ( ) == Qt . LeftButton and
event . type ( ) == QEvent . MouseButtonPress ) :
_left_mouse_down = True
if _left_click_flag is _ClickFlag . NONE :
_left_click_flag = _ClickFlag . SELECTION_BOX
set_move_bb_top_left ( event . x ( ) , event . y ( ) )
elif ( _left_click_flag is _ClickFlag . SELECTION_BOX
and _left_mouse_down ) :
# We are now in the click-and-hold to signal move
# tracking and translation
_left_click_flag = _ClickFlag . SELECTION_MOVE
_last_mouse_pos = ( event . x ( ) , event . y ( ) )
# Post-selection handlers
if ( _left_click_flag is _ClickFlag . SELECTION_BOX
and event . type ( ) == QEvent . MouseMove ) :
set_move_bb_bottom_right ( event . x ( ) , event . y ( ) )
elif ( _left_click_flag is _ClickFlag . SELECTION_MOVE
and _last_mouse_pos is not None
and _left_mouse_down
and event . type ( ) == QEvent . MouseMove ) :
dx = abs ( _last_mouse_pos [ 0 ] - event . x ( ) )
dy = abs ( _last_mouse_pos [ 1 ] - event . y ( ) )
for p in PointManager . point_set . points :
if p . selected :
# Use the deltas to decide what direction to move.
# We only want to move in small unit increments.
# If we used the deltas directly the points would
# fly off screen quickly as we got farther from our
# start.
try :
if event . x ( ) < _last_mouse_pos [ 0 ] :
p . move ( - dx , 0 )
if event . y ( ) < _last_mouse_pos [ 1 ] :
p . move ( 0 , - dy )
if event . x ( ) > _last_mouse_pos [ 0 ] :
p . move ( dx , 0 )
if event . y ( ) > _last_mouse_pos [ 1 ] :
p . move ( 0 , dy )
except ExceededWindowBoundsError :
# This point has indicated a move would exceed
# it's bounds, so we'll just go to the next
# point.
continue
_last_mouse_pos = ( event . x ( ) , event . y ( ) )
elif ( _left_click_flag is not _ClickFlag . NONE and
event . type ( ) == QEvent . MouseButtonRelease ) :
if _left_click_flag is _ClickFlag . SELECTION_BOX :
set_move_bb_bottom_right ( event . x ( ) , event . y ( ) )
# Satisfy the post condition by resetting the bounding box
reset_move_bbs ( )
ctx . opengl_widget . update ( )
def _handle_delete_point ( ctx , event ) :
_handle_info_updates ( ctx , event )
if ( event . button ( ) == Qt . LeftButton and
event . type ( ) == QEvent . MouseButtonPress ) :
set_drawing_event ( event )
PointManager . point_set . remove_point ( event . x ( ) , event . y ( ) )
refresh_point_list ( ctx )
ctx . opengl_widget . update ( )
ctx . point_list_widget . update ( )
def _handle_info_updates ( ctx , event ) :
"""
Updates data under the " information " header .
@param ctx The context to the main window .
@param event The event .
"""
if event . type ( ) == QEvent . MouseMove :
ctx . mouse_position_label . setText ( f " { event . x ( ) , event . y ( ) } " )
def reset_colors ( ) :
global _remaining_colors
_remaining_colors = [ c for c in Color if c not in [ Color . BLUE , Color . GREY ] ]
for point in PointManager . point_set . points :
point . color = Color . GREY
def generate_random_points ( point_count , x_bound , y_bound ) :
"""
Using the random module of python generate a unique set of xs and ys
to use as points , bounded by the canvas edges .
@param point_count The count of points to generate .
@param x_bound The width bound .
@param y_bound The height bound .
"""
# TODO: The window size should be increased slightly to
# accomodate 3000 points (the maximum) given the point size.
# Work out an algorithm and limit the number of points
# selectable based on the maximum amount of points on the screen
# given the point size.
# First clear the point set
PointManager . point_set . clear ( )
point_size = PointManager . point_set . point_size
# Sample without replacement so points are not duplicated.
xs = random . sample ( range ( point_size , x_bound ) , point_count )
ys = random . sample ( range ( point_size , y_bound ) , point_count )
points = list ( zip ( xs , ys ) )
for point in points :
PointManager . point_set . add_point ( point [ 0 ] , point [ 1 ] , Color . GREY )
def _handle_clustering ( ctx , _ ) :
points = list ( PointManager . point_set . points )
if not ctx . clustering_solved :
clusters = k_means ( points , ctx . number_of_clusters . value ( ) , 0.001 )
# We can leverage the paint function by first assigning every cluster
# a color (for completeness) and then assigning every point in that
# cluster the cluster's color.
for i , cluster in enumerate ( clusters ) :
cluster . color = _remaining_colors [ i ]
for point in cluster . points :
point . color = cluster . color
PointManager . clusters = clusters
ctx . opengl_widget . update ( )
ctx . clustering_solved = True
# Simple dispatcher to make it easy to dispatch the right mode
# function when the OpenGL window is acted on.
MODE_HANDLER_MAP = {
Mode . OFF : _handle_info_updates ,
Mode . LOADED : _handle_info_updates ,
Mode . ADD : _handle_add_point ,
Mode . EDIT : _handle_edit_point ,
Mode . MOVE : _handle_move_points ,
Mode . DELETE : _handle_delete_point ,
Mode . CLUSTERING : _handle_clustering
}