|
|
|
import random
|
|
|
|
|
|
|
|
from PyQt5.QtCore import QEvent, Qt
|
|
|
|
from PyQt5.QtGui import QCursor
|
|
|
|
|
|
|
|
from .algorithms import Algorithms
|
|
|
|
from .colors import Color
|
|
|
|
from .exceptions import ExceededWindowBoundsError
|
|
|
|
from .mode import Mode
|
|
|
|
from .opengl_widget import (set_drawing_event, set_move_bb_top_left,
|
|
|
|
set_move_bb_bottom_right, reset_move_bbs,
|
|
|
|
viewport_height, viewport_width)
|
|
|
|
from .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
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# 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("({}, {})".format(p.x, p.y))
|
|
|
|
|
|
|
|
ctx.point_list_widget.update()
|
|
|
|
|
|
|
|
|
|
|
|
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):
|
|
|
|
# TODO: This function and delete definitely need to make sure they are
|
|
|
|
# on a point we have.
|
|
|
|
#
|
|
|
|
# Since points are unique consider a hashmap of points to make O(1)
|
|
|
|
# lookups for addition and deletion. This list can be maintained here
|
|
|
|
# in this module. It should be a dictionary - from point to
|
|
|
|
# attributes in the case of algorithms that require points to have
|
|
|
|
# weights or something.
|
|
|
|
#
|
|
|
|
# Should move the associated point in the list to the new location if
|
|
|
|
# applicable.
|
|
|
|
|
|
|
|
__handle_info_updates(ctx, event)
|
|
|
|
PointManager.point_set.clear_selection()
|
|
|
|
|
|
|
|
# Store old x, y from event
|
|
|
|
set_drawing_event(event)
|
|
|
|
ctx.update()
|
|
|
|
# after this remove the point from the list
|
|
|
|
|
|
|
|
|
|
|
|
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 __handle_choose_centroids(ctx, event):
|
|
|
|
"""
|
|
|
|
Similar to move in terms of selecting points, however this
|
|
|
|
function assigns a random color up to the maximum number
|
|
|
|
of centroids, and after the maximum number has been selected it will
|
|
|
|
enable the group button.
|
|
|
|
"""
|
|
|
|
global __centroid_count
|
|
|
|
global __remaining_colors
|
|
|
|
|
|
|
|
__handle_info_updates(ctx, event)
|
|
|
|
|
|
|
|
if __centroid_count == ctx.number_of_centroids.value():
|
|
|
|
# We have specified the number of centroids required
|
|
|
|
return
|
|
|
|
|
|
|
|
if (event.button() == Qt.LeftButton and
|
|
|
|
event.type() == QEvent.MouseButtonPress):
|
|
|
|
|
|
|
|
point = None
|
|
|
|
|
|
|
|
for test_point in PointManager.point_set.points:
|
|
|
|
if test_point.hit(event.x(), event.y()):
|
|
|
|
point = test_point
|
|
|
|
|
|
|
|
if point is None:
|
|
|
|
# No point was found on the click, do nothing
|
|
|
|
return
|
|
|
|
|
|
|
|
if point in PointManager.centroids:
|
|
|
|
# Centroids must be unique
|
|
|
|
return
|
|
|
|
|
|
|
|
__centroid_count += 1
|
|
|
|
|
|
|
|
color = random.choice(__remaining_colors)
|
|
|
|
__remaining_colors.remove(color)
|
|
|
|
|
|
|
|
point.color = color
|
|
|
|
|
|
|
|
# Recolor the point and restash the point in centroids
|
|
|
|
PointManager.centroids.append(point)
|
|
|
|
|
|
|
|
if __centroid_count == ctx.number_of_centroids.value():
|
|
|
|
# Prevent the user from changing the centroids
|
|
|
|
ctx.number_of_centroids.setEnabled(False)
|
|
|
|
ctx.choose_centroids_button.setEnabled(False)
|
|
|
|
ctx.group_button.setEnabled(True)
|
|
|
|
|
|
|
|
ctx.opengl_widget.update()
|
|
|
|
|
|
|
|
|
|
|
|
def group(ctx):
|
|
|
|
"""
|
|
|
|
Group handler. Basically just a wrapper around the distance
|
|
|
|
grouping algorithm that recolors points.
|
|
|
|
|
|
|
|
This is one of the only functions that operates only on a button click
|
|
|
|
and as a result is made public since the dispatcher is not needed.
|
|
|
|
"""
|
|
|
|
groups = Algorithms.euclidean_grouping(PointManager.centroids,
|
|
|
|
PointManager.point_set)
|
|
|
|
|
|
|
|
for centroid_group in groups:
|
|
|
|
for point in centroid_group.points:
|
|
|
|
point.color = centroid_group.centroid.color
|
|
|
|
|
|
|
|
ctx.opengl_widget.update()
|
|
|
|
|
|
|
|
|
|
|
|
def reset_centroid_count_and_colors():
|
|
|
|
global __centroid_count
|
|
|
|
global __remaining_colors
|
|
|
|
|
|
|
|
__centroid_count = 0
|
|
|
|
__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)
|
|
|
|
|
|
|
|
|
|
|
|
# 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.CHOOSE_CENTROIDS: __handle_choose_centroids
|
|
|
|
}
|