Browse Source

Clean up clusterview old code a little bit, first launch without the grouping stuff.

pull/1/head
Taylor Bockman 5 years ago
parent
commit
4c3be0b80e
  1. 8
      README.md
  2. 13
      TODO.org
  3. 28
      clusterview2.ui
  4. 10
      clusterview2/exceptions.py
  5. 5
      clusterview2/mode.py
  6. 364
      clusterview2/points.py
  7. 136
      clusterview2/ui/mode_handlers.py
  8. 23
      clusterview2/ui/opengl_widget.py
  9. 4
      clusterview2/ui/point_list_widget.py
  10. 26
      clusterview2_ui.py
  11. 143
      main_window.py
  12. 2
      setup.cfg

8
README.md

@ -12,3 +12,11 @@ TODO
Make sure to install the development requirements using `pip install -r requirements-dev.txt`. This will install
all main requirements as well as useful testing and linting tools.
### Regenerating the UI
After modifying the `*.ui` file in Qt Designer run
`pyuic5 clusterview2.ui -o clusterview2_ui.py`
to regenerate the UI python file.

13
TODO.org

@ -4,11 +4,14 @@ TODO:
the math library from kmeans (add a test to dist). Additionally, the point and cluster of the clusterview should
inherit from k-means point and cluster so that it can use the algorithm correctly. All other aspects of point and
cluster in the clusterview program can be kept.
* Extract and improve the overall structure of clusterview so that globals are not used unless absolutely necessary.
* See note in opengl_widget. Make it a static class.
* Do the same thing you did for opengl_widget in mode_handlers, leave the MODE_HANDLERS constant global.
* Port over all tests from clusterview except the math tests. Improve point tests to include weight.
* Turn kmeans into a python package and import it here.
* Remove old clusterview buttons, keep the status window side and saving feature. Add new buttons for weighted and
unweighted clustering.
* Add a property to the point called weight, which is set when you click edit point. It will give a popup that
allows you to specify a point weight.
* Use kmeans to do the stuff.
allows you to specify a point weight. DEFAULT WEIGHT IS 1
* Use kmeans to do the stuff - strip down point x y to refer to it's kmeans parent.
* Weighted cluster mean is rendered as a hollow circle and unweighted k-means mean is a x.
* Saving should save the weights, load should load the weights.
* Make the current context exception string in opengl widget a constant and use that instead.
* Add typing to every function except setters and __XYZ__ functions (__init__ can still have typing)

28
clusterview2.ui

@ -29,7 +29,7 @@
</size>
</property>
<property name="windowTitle">
<string>ClusterView</string>
<string>ClusterView2</string>
</property>
<widget class="QWidget" name="centralwidget">
<layout class="QHBoxLayout" name="horizontalLayout">
@ -139,23 +139,13 @@
</property>
</widget>
</item>
<item row="5" column="0">
<widget class="QPushButton" name="solve_button">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>Solve</string>
</property>
</widget>
</item>
<item row="4" column="0">
<widget class="QPushButton" name="group_button">
<widget class="QPushButton" name="unweighted_clustering_button">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>Group</string>
<string>Unweighted Clustering</string>
</property>
</widget>
</item>
@ -166,6 +156,16 @@
</property>
</widget>
</item>
<item row="5" column="0">
<widget class="QPushButton" name="weighted_clustering_button">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>Weighted Clustering</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
@ -259,7 +259,7 @@
<x>0</x>
<y>0</y>
<width>1280</width>
<height>28</height>
<height>21</height>
</rect>
</property>
<property name="nativeMenuBar">

10
clusterview2/exceptions.py

@ -2,12 +2,15 @@ from PyQt5.QtWidgets import QErrorMessage
from clusterview2.mode import Mode
class ExceededWindowBoundsError(Exception):
pass
class InvalidStateError(Exception):
pass
class InvalidModeError(Exception):
"""
An exception to specify an invalid mode has been provided.
@ -20,12 +23,13 @@ class InvalidModeError(Exception):
"""
if not isinstance(mode, Mode):
raise ValueError("Mode argument to InvalidMode must be of " +
" type mode")
raise ValueError('Mode argument to InvalidMode must be of ' +
' type mode')
# Mode cases for invalid mode
if mode == Mode.OFF:
super().__init__("You must select a mode before continuing.")
super().__init__('You must select a mode before continuing.')
def handle_exceptions(func):
"""

5
clusterview2/mode.py

@ -13,5 +13,6 @@ class Mode(Enum):
MOVE = 3
DELETE = 4
LOADED = 5
CHOOSE_CENTROIDS = 6 # TODO: Can replace with choose weighted or something
GROUP = 7
CHOOSE_CENTROIDS = 6
UNWEIGHTED_CLUSTERING = 7
WEIGHTED_CLUSTERING = 8

364
clusterview2/points.py

@ -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

136
clusterview2/ui/mode_handlers.py

@ -12,7 +12,7 @@ from .opengl_widget import (set_drawing_event, set_move_bb_top_left,
from clusterview2.point_manager import PointManager
class __ClickFlag:
class _ClickFlag:
# This is the first stage. On mouse release it goes to
# SELECTION_MOVE.
@ -35,23 +35,23 @@ class __ClickFlag:
# GLOBALS
# Canvas pixel border - empirical, not sure where this is stored officially
__CANVAS_BORDER = 1
_CANVAS_BORDER = 1
# Module level flag for left click events (used to detect a left
# click hold drag)
__left_click_flag = __ClickFlag.NONE
_left_click_flag = _ClickFlag.NONE
# Variable to track the mouse state during selection movement
__last_mouse_pos = None
_last_mouse_pos = None
# Used to implement mouse dragging when clicked
__left_click_down = False
_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]]
_centroid_count = 0
_remaining_colors = [c for c in Color if c not in [Color.BLUE, Color.GREY]]
def refresh_point_list(ctx):
@ -62,7 +62,7 @@ def refresh_point_list(ctx):
"""
# 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.
# it using the current _point_set.
ctx.point_list_widget.clear()
for p in PointManager.point_set.points:
@ -71,7 +71,7 @@ def refresh_point_list(ctx):
ctx.point_list_widget.update()
def __handle_add_point(ctx, event):
def _handle_add_point(ctx, event):
"""
Event handler for the add point mode.
@ -84,7 +84,7 @@ def __handle_add_point(ctx, event):
"""
# Update information as needed
__handle_info_updates(ctx, event)
_handle_info_updates(ctx, event)
if (event.button() == Qt.LeftButton and
event.type() == QEvent.MouseButtonPress):
@ -114,7 +114,7 @@ def __handle_add_point(ctx, event):
ctx.point_list_widget.update()
def __handle_edit_point(ctx, event):
def _handle_edit_point(ctx, event):
# TODO: This function and delete definitely need to make sure they are
# on a point we have.
#
@ -127,7 +127,7 @@ def __handle_edit_point(ctx, event):
# Should move the associated point in the list to the new location if
# applicable.
__handle_info_updates(ctx, event)
_handle_info_updates(ctx, event)
PointManager.point_set.clear_selection()
# Store old x, y from event
@ -147,16 +147,16 @@ def ogl_keypress_handler(ctx, event):
@param ctx A handle to the window context.
@param event The event associated with this handler.
"""
global __left_click_flag
global __last_mouse_pos
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:
if _left_click_flag is not _ClickFlag.NONE:
__last_mouse_pos = None
_last_mouse_pos = None
__left_click_flag = __ClickFlag.NONE
_left_click_flag = _ClickFlag.NONE
PointManager.point_set.clear_selection()
reset_move_bbs()
refresh_point_list(ctx)
@ -171,7 +171,7 @@ def ogl_keypress_handler(ctx, event):
ctx.opengl_widget.update()
def __handle_move_points(ctx, event):
def _handle_move_points(ctx, event):
"""
A relatively complicated state machine that handles the process of
selection, clicking, and dragging.
@ -180,52 +180,52 @@ def __handle_move_points(ctx, event):
@param event The event.
"""
global __left_click_flag
global __left_mouse_down
global __last_mouse_pos
global _left_click_flag
global _left_mouse_down
global _last_mouse_pos
set_drawing_event(event)
__handle_info_updates(ctx, 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
_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
_left_mouse_down = True
if __left_click_flag is __ClickFlag.NONE:
__left_click_flag = __ClickFlag.SELECTION_BOX
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):
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())
_left_click_flag = _ClickFlag.SELECTION_MOVE
_last_mouse_pos = (event.x(), event.y())
# Post-selection handlers
if (__left_click_flag is __ClickFlag.SELECTION_BOX
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
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())
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:
@ -235,13 +235,13 @@ def __handle_move_points(ctx, event):
# fly off screen quickly as we got farther from our
# start.
try:
if event.x() < __last_mouse_pos[0]:
if event.x() < _last_mouse_pos[0]:
p.move(-dx, 0)
if event.y() < __last_mouse_pos[1]:
if event.y() < _last_mouse_pos[1]:
p.move(0, -dy)
if event.x() > __last_mouse_pos[0]:
if event.x() > _last_mouse_pos[0]:
p.move(dx, 0)
if event.y() > __last_mouse_pos[1]:
if event.y() > _last_mouse_pos[1]:
p.move(0, dy)
except ExceededWindowBoundsError:
@ -250,12 +250,12 @@ def __handle_move_points(ctx, event):
# point.
continue
__last_mouse_pos = (event.x(), event.y())
_last_mouse_pos = (event.x(), event.y())
elif (__left_click_flag is not __ClickFlag.NONE and
elif (_left_click_flag is not _ClickFlag.NONE and
event.type() == QEvent.MouseButtonRelease):
if __left_click_flag is __ClickFlag.SELECTION_BOX:
if _left_click_flag is _ClickFlag.SELECTION_BOX:
set_move_bb_bottom_right(event.x(), event.y())
@ -265,9 +265,9 @@ def __handle_move_points(ctx, event):
ctx.opengl_widget.update()
def __handle_delete_point(ctx, event):
def _handle_delete_point(ctx, event):
__handle_info_updates(ctx, event)
_handle_info_updates(ctx, event)
if (event.button() == Qt.LeftButton and
event.type() == QEvent.MouseButtonPress):
@ -282,7 +282,7 @@ def __handle_delete_point(ctx, event):
ctx.point_list_widget.update()
def __handle_info_updates(ctx, event):
def _handle_info_updates(ctx, event):
"""
Updates data under the "information" header.
@ -293,19 +293,19 @@ def __handle_info_updates(ctx, event):
ctx.mouse_position_label.setText(f"{event.x(), event.y()}")
def __handle_choose_centroids(ctx, event):
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
global _centroid_count
global _remaining_colors
__handle_info_updates(ctx, event)
_handle_info_updates(ctx, event)
if __centroid_count == ctx.number_of_centroids.value():
if _centroid_count == ctx.number_of_centroids.value():
# We have specified the number of centroids required
return
@ -326,32 +326,32 @@ def __handle_choose_centroids(ctx, event):
# Centroids must be unique
return
__centroid_count += 1
_centroid_count += 1
color = random.choice(__remaining_colors)
__remaining_colors.remove(color)
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():
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.unweighted_clustering_button.setEnabled(True)
ctx.weighted_clustering_button.setEnabled(True)
ctx.opengl_widget.update()
def reset_centroid_count_and_colors():
global __centroid_count
global __remaining_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]]
_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
@ -392,11 +392,11 @@ def generate_random_points(point_count, x_bound, y_bound):
# 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
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
}

23
clusterview2/ui/opengl_widget.py

@ -43,6 +43,10 @@ __current_context = None
__current_event = None
# TODO: This should live inside of a class as static methods with the
# globals moved into the static scope to make this nicer...once you
# get it running before doing kmeans make this modification.
def set_drawing_context(ctx):
"""
Sets the drawing context so that drawing functions can properly
@ -63,8 +67,8 @@ def set_drawing_event(event):
global __current_event
if __current_context is None:
raise InvalidStateError("Drawing context must be set before setting " +
"drawing mode")
raise InvalidStateError('Drawing context must be set before setting ' +
'drawing mode')
if event is not None:
__current_event = event
@ -180,12 +184,13 @@ def paint_gl():
__current_context.mode is Mode.DELETE or
__current_context.mode is Mode.LOADED or
__current_context.mode is Mode.CHOOSE_CENTROIDS or
__current_context.mode is Mode.GROUP):
__current_context.mode is Mode.UNWEIGHTED_CLUSTERING or
__current_context.mode is Mode.WEIGHTED_CLUSTERING):
draw_points(PointManager.point_set)
elif __current_context.mode is Mode.EDIT:
raise NotImplementedError("Drawing for EDIT not implemented.")
raise NotImplementedError('Drawing for EDIT not implemented.')
elif __current_context.mode is Mode.MOVE:
# We have to repeatedly draw the points while we are showing the
@ -298,11 +303,11 @@ def draw_selection_box(color):
global __current_context
if __current_context is None:
raise InvalidStateError("Drawing context must be set before setting " +
"drawing mode")
raise InvalidStateError('Drawing context must be set before setting ' +
'drawing mode')
if not isinstance(color, Color):
raise ValueError("Color must exist in the Color enumeration")
raise ValueError('Color must exist in the Color enumeration')
if __move_bb_top_left is None or __move_bb_bottom_right is None:
# Nothing to draw.
@ -367,8 +372,8 @@ def draw_points(point_set):
global __current_context
if __current_context is None:
raise InvalidStateError("Drawing context must be set before setting " +
"drawing mode")
raise InvalidStateError('Drawing context must be set before setting ' +
'drawing mode')
glViewport(0, 0, __WIDTH, __HEIGHT)
glPointSize(PointManager.point_set.point_size)

4
clusterview2/ui/point_list_widget.py

@ -8,7 +8,7 @@ is defined in the clusterview_ui.py file.
from clusterview2.point_manager import PointManager
def __string_point_to_point(str_point):
def _string_point_to_point(str_point):
"""
In the QListWidget points are stored as strings
because of the way Qt has list items defined.
@ -39,7 +39,7 @@ def item_click_handler(ctx, item):
@param ctx The context.
@param item The clicked item.
"""
point = __string_point_to_point(item.text())
point = _string_point_to_point(item.text())
# TODO: Super slow linear search, should write a find_point function
# on the point_set in order to speed this up since PointSet

26
clusterview2_ui.py

@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Form implementation generated from reading ui file 'clusterview.ui'
# Form implementation generated from reading ui file 'clusterview2.ui'
#
# Created by: PyQt5 UI code generator 5.13.0
#
@ -78,17 +78,17 @@ class Ui_MainWindow(object):
self.choose_centroids_button.setEnabled(True)
self.choose_centroids_button.setObjectName("choose_centroids_button")
self.formLayout.setWidget(3, QtWidgets.QFormLayout.LabelRole, self.choose_centroids_button)
self.solve_button = QtWidgets.QPushButton(self.groupBox_3)
self.solve_button.setEnabled(False)
self.solve_button.setObjectName("solve_button")
self.formLayout.setWidget(5, QtWidgets.QFormLayout.LabelRole, self.solve_button)
self.group_button = QtWidgets.QPushButton(self.groupBox_3)
self.group_button.setEnabled(False)
self.group_button.setObjectName("group_button")
self.formLayout.setWidget(4, QtWidgets.QFormLayout.LabelRole, self.group_button)
self.unweighted_clustering_button = QtWidgets.QPushButton(self.groupBox_3)
self.unweighted_clustering_button.setEnabled(False)
self.unweighted_clustering_button.setObjectName("unweighted_clustering_button")
self.formLayout.setWidget(4, QtWidgets.QFormLayout.LabelRole, self.unweighted_clustering_button)
self.reset_button = QtWidgets.QPushButton(self.groupBox_3)
self.reset_button.setObjectName("reset_button")
self.formLayout.setWidget(6, QtWidgets.QFormLayout.LabelRole, self.reset_button)
self.weighted_clustering_button = QtWidgets.QPushButton(self.groupBox_3)
self.weighted_clustering_button.setEnabled(False)
self.weighted_clustering_button.setObjectName("weighted_clustering_button")
self.formLayout.setWidget(5, QtWidgets.QFormLayout.LabelRole, self.weighted_clustering_button)
self.verticalLayout.addWidget(self.groupBox_3)
spacerItem = QtWidgets.QSpacerItem(20, 20, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Fixed)
self.verticalLayout.addItem(spacerItem)
@ -117,7 +117,7 @@ class Ui_MainWindow(object):
self.horizontalLayout.addLayout(self.verticalLayout)
MainWindow.setCentralWidget(self.centralwidget)
self.menubar = QtWidgets.QMenuBar(MainWindow)
self.menubar.setGeometry(QtCore.QRect(0, 0, 1280, 28))
self.menubar.setGeometry(QtCore.QRect(0, 0, 1280, 21))
self.menubar.setNativeMenuBar(True)
self.menubar.setObjectName("menubar")
self.menu_file = QtWidgets.QMenu(self.menubar)
@ -167,14 +167,14 @@ class Ui_MainWindow(object):
def retranslateUi(self, MainWindow):
_translate = QtCore.QCoreApplication.translate
MainWindow.setWindowTitle(_translate("MainWindow", "ClusterView"))
MainWindow.setWindowTitle(_translate("MainWindow", "ClusterView2"))
self.groupBox.setTitle(_translate("MainWindow", "Point List"))
self.groupBox_3.setTitle(_translate("MainWindow", "Solver"))
self.label_2.setText(_translate("MainWindow", "Centroids"))
self.choose_centroids_button.setText(_translate("MainWindow", "Choose Centroids"))
self.solve_button.setText(_translate("MainWindow", "Solve"))
self.group_button.setText(_translate("MainWindow", "Group"))
self.unweighted_clustering_button.setText(_translate("MainWindow", "Unweighted Clustering"))
self.reset_button.setText(_translate("MainWindow", "Reset"))
self.weighted_clustering_button.setText(_translate("MainWindow", "Weighted Clustering"))
self.groupBox_2.setTitle(_translate("MainWindow", "Canvas Information"))
self.label.setText(_translate("MainWindow", "Mouse Position:"))
self.menu_file.setTitle(_translate("MainWindow", "File"))

143
main_window.py

@ -15,6 +15,7 @@ from clusterview2.ui.mode_handlers import (MODE_HANDLER_MAP,
from clusterview2.ui.opengl_widget import (clear_selection, initialize_gl,
mouse_leave, paint_gl, resize_gl,
set_drawing_context)
from clusterview2.points import PointSet
from clusterview2.point_manager import PointManager
from clusterview2.ui.point_list_widget import item_click_handler
from clusterview2_ui import Ui_MainWindow
@ -29,25 +30,25 @@ class MainWindow(QMainWindow, Ui_MainWindow):
# This is a static mode variable since there will only ever
# be one MainWindow.
__mode = Mode.OFF
_mode = Mode.OFF
def __init__(self, parent=None):
super(MainWindow, self).__init__(parent)
self.setupUi(self)
# Size of point for drawing
self.__point_size = 8
self._point_size = 8
# TODO: THESE ARE HARD CODED TO THE CURRENT QT WIDGET SIZES
# FIX THIS PROPERLY WITH A RESIZE EVENT DETECT.
# PointManager is a class that is filled with static methods
# designed for managing state.
self.__viewport_width = 833
self.__viewport_height = 656
self._viewport_width = 833
self._viewport_height = 656
PointManager.point_set = PointSet(self.__point_size,
self.__viewport_width,
self.__viewport_height)
PointManager.point_set = PointSet(self._point_size,
self._viewport_width,
self._viewport_height)
# Spin box should only allow the number of centroids to be no
# greater than the number of supported colors minus 2 to exclude
@ -82,11 +83,11 @@ class MainWindow(QMainWindow, Ui_MainWindow):
self.point_list_widget.itemClicked.connect(partial(item_click_handler,
self))
self.choose_centroids_button.clicked.connect(self.__choose_centroids)
self.choose_centroids_button.clicked.connect(self._choose_centroids)
self.group_button.clicked.connect(self.__group)
self.unweighted_clustering_button.clicked.connect(self._unweighted_clustering)
self.reset_button.clicked.connect(self.__reset_grouping)
self.reset_button.clicked.connect(self._reset)
# -----------------------------------------------
# OpenGL Graphics Handlers are set
@ -100,108 +101,108 @@ class MainWindow(QMainWindow, Ui_MainWindow):
# -------------------------------------
# UI Handlers
# -------------------------------------
self.action_add_points.triggered.connect(self.__add_points)
self.action_edit_points.triggered.connect(self.__edit_points)
self.action_delete_points.triggered.connect(self.__delete_points)
self.action_move_points.triggered.connect(self.__move_points)
self.action_add_points.triggered.connect(self._add_points)
self.action_edit_points.triggered.connect(self._edit_points)
self.action_delete_points.triggered.connect(self._delete_points)
self.action_move_points.triggered.connect(self._move_points)
(self.action_generate_random_points
.triggered.connect(self.__generate_random_points))
.triggered.connect(self._generate_random_points))
self.action_save_point_configuration.triggered.connect(
self.__save_points_file)
self._save_points_file)
self.action_load_point_configuration.triggered.connect(
self.__open_points_file)
self._open_points_file)
self.action_exit.triggered.connect(self.__close_event)
self.action_exit.triggered.connect(self._close_event)
# Override handler for mouse press so we can draw points based on
# the OpenGL coordinate system inside of the OpenGL Widget.
self.opengl_widget.mousePressEvent = self.__ogl_click_dispatcher
self.opengl_widget.mouseMoveEvent = self.__ogl_click_dispatcher
self.opengl_widget.mouseReleaseEvent = self.__ogl_click_dispatcher
self.opengl_widget.mousePressEvent = self._ogl_click_dispatcher
self.opengl_widget.mouseMoveEvent = self._ogl_click_dispatcher
self.opengl_widget.mouseReleaseEvent = self._ogl_click_dispatcher
# -----------------------------------------------------------------
# Mode changers - these will be used to signal the action in the
# OpenGL Widget.
# -----------------------------------------------------------------
def __off_mode(self):
self.__mode = Mode.OFF
def _off_mode(self):
self._mode = Mode.OFF
self.opengl_widget.setCursor(QCursor(Qt.CursorShape.ArrowCursor))
self.status_bar.showMessage("")
self.status_bar.showMessage('')
clear_selection()
self.opengl_widget.update()
def __add_points(self):
self.__mode = Mode.ADD
def _add_points(self):
self._mode = Mode.ADD
self.opengl_widget.setCursor(QCursor(Qt.CursorShape.CrossCursor))
self.status_bar.showMessage("ADD MODE")
self.status_bar.showMessage('ADD MODE')
clear_selection()
self.opengl_widget.update()
def __edit_points(self):
self.__mode = Mode.EDIT
def _edit_points(self):
self._mode = Mode.EDIT
self.opengl_widget.setCursor(QCursor(Qt.CursorShape.CrossCursor))
self.status_bar.showMessage("EDIT MODE")
self.status_bar.showMessage('EDIT MODE')
clear_selection()
self.opengl_widget.update()
def __delete_points(self):
self.__mode = Mode.DELETE
def _delete_points(self):
self._mode = Mode.DELETE
self.opengl_widget.setCursor(QCursor(
Qt.CursorShape.PointingHandCursor))
self.status_bar.showMessage("DELETE MODE")
self.status_bar.showMessage('DELETE MODE')
clear_selection()
self.opengl_widget.update()
def __move_points(self):
self.__mode = Mode.MOVE
def _move_points(self):
self._mode = Mode.MOVE
self.opengl_widget.setCursor(QCursor(Qt.CursorShape.SizeAllCursor))
self.status_bar.showMessage("MOVE MODE - PRESS ESC OR SWITCH MODES" +
"TO CANCEL SELECTION")
self.status_bar.showMessage('MOVE MODE - PRESS ESC OR SWITCH MODES ' +
'TO CANCEL SELECTION')
clear_selection()
self.opengl_widget.update()
def __choose_centroids(self):
self.__mode = Mode.CHOOSE_CENTROIDS
def _choose_centroids(self):
self._mode = Mode.CHOOSE_CENTROIDS
self.opengl_widget.setCursor(QCursor(Qt.CursorShape.CrossCursor))
self.status_bar.showMessage("CHOOSE CENTROIDS")
self.status_bar.showMessage('CHOOSE CENTROIDS')
clear_selection()
self.opengl_widget.update()
def __group(self):
self.__mode = Mode.GROUP
def _unweighted_clustering(self):
self._mode = Mode.UNWEIGHTED_CLUSTERING
self.opengl_widget.setCursor(QCursor(Qt.CursorShape.ArrowCursor))
self.status_bar.showMessage("GROUPING")
self.status_bar.showMessage('UNWEIGHTED CLUSTERING')
clear_selection()
group(self)
self.__off_mode()
# unweighted_clustering(self)
self._off_mode()
self.opengl_widget.update()
def __reset_grouping(self):
self.__off_mode()
def _reset(self):
self._off_mode()
self.number_of_centroids.setEnabled(True)
self.number_of_centroids.setValue(0)
self.choose_centroids_button.setEnabled(True)
self.solve_button.setEnabled(False)
self.group_button.setEnabled(False)
self.unweighted_clustering_button.setEnabled(False)
self.weighted_clustering_button.setEnabled(False)
PointManager.centroids = []
reset_centroid_count_and_colors()
def __generate_random_points(self):
value, ok = QInputDialog.getInt(self, "Number of Points",
"Number of Points:", 30, 30, 3000, 1)
def _generate_random_points(self):
value, ok = QInputDialog.getInt(self, 'Number of Points',
'Number of Points:', 30, 30, 3000, 1)
if ok:
self.__mode = Mode.ADD
self._mode = Mode.ADD
generate_random_points(value,
(self.__viewport_width - self.__point_size),
(self.__viewport_height - self.__point_size)
(self._viewport_width - self._point_size),
(self._viewport_height - self._point_size)
)
self.__mode = Mode.OFF
self._mode = Mode.OFF
self.opengl_widget.update()
@ -209,27 +210,27 @@ class MainWindow(QMainWindow, Ui_MainWindow):
@property
def mode(self):
"""
""""
Function designed to be used from a context
to get the current mode.
"""
return self.__mode
return self._mode
@mode.setter
def mode(self, mode):
self.__mode = mode
self._mode = mode
def __close_event(self, event):
def _close_event(self, event):
import sys
sys.exit(0)
def __open_points_file(self):
def _open_points_file(self):
ofile, _ = QFileDialog.getOpenFileName(self,
"Open Point Configuration",
"",
"JSON files (*.json)")
'Open Point Configuration',
'',
'JSON files (*.json)')
if ofile:
self.__mode = Mode.LOADED
self._mode = Mode.LOADED
PointManager.load(ofile)
@ -237,17 +238,17 @@ class MainWindow(QMainWindow, Ui_MainWindow):
refresh_point_list(self)
def __save_points_file(self):
def _save_points_file(self):
file_name, _ = (QFileDialog.
getSaveFileName(self,
"Save Point Configuration",
"",
"JSON Files (*.json)"))
'Save Point Configuration',
'',
'JSON Files (*.json)'))
if file_name:
PointManager.save(file_name)
@handle_exceptions
def __ogl_click_dispatcher(self, event):
def _ogl_click_dispatcher(self, event):
"""
Mode dispatcher for click actions on the OpenGL widget.
"""
@ -256,4 +257,4 @@ class MainWindow(QMainWindow, Ui_MainWindow):
# OpenGL event. The context passed to these functions allows
# them to modify on screen widgets such as the QOpenGLWidget and
# QListWidget.
MODE_HANDLER_MAP[self.__mode](self, event)
MODE_HANDLER_MAP[self._mode](self, event)

2
setup.cfg

@ -0,0 +1,2 @@
[flake8]
max-line-length = 120
Loading…
Cancel
Save