diff --git a/README.md b/README.md
index 3bbf649..8db6d64 100644
--- a/README.md
+++ b/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.
diff --git a/TODO.org b/TODO.org
index 0580c9b..7750051 100644
--- a/TODO.org
+++ b/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)
diff --git a/clusterview2.ui b/clusterview2.ui
index 51d9afb..4d5c2f7 100644
--- a/clusterview2.ui
+++ b/clusterview2.ui
@@ -29,7 +29,7 @@
- ClusterView
+ ClusterView2
@@ -139,23 +139,13 @@
- -
-
-
- false
-
-
- Solve
-
-
-
-
-
+
false
- Group
+ Unweighted Clustering
@@ -166,6 +156,16 @@
+ -
+
+
+ false
+
+
+ Weighted Clustering
+
+
+
@@ -259,7 +259,7 @@
0
0
1280
- 28
+ 21
diff --git a/clusterview2/exceptions.py b/clusterview2/exceptions.py
index 5710393..ee0dfdc 100644
--- a/clusterview2/exceptions.py
+++ b/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):
"""
diff --git a/clusterview2/mode.py b/clusterview2/mode.py
index c4b1ff4..e54cbe9 100644
--- a/clusterview2/mode.py
+++ b/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
diff --git a/clusterview2/points.py b/clusterview2/points.py
new file mode 100644
index 0000000..db933fb
--- /dev/null
+++ b/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 = ""
+
+ 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
diff --git a/clusterview2/ui/mode_handlers.py b/clusterview2/ui/mode_handlers.py
index 0980e6e..857f068 100644
--- a/clusterview2/ui/mode_handlers.py
+++ b/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
}
diff --git a/clusterview2/ui/opengl_widget.py b/clusterview2/ui/opengl_widget.py
index 36a9614..d52659e 100644
--- a/clusterview2/ui/opengl_widget.py
+++ b/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)
diff --git a/clusterview2/ui/point_list_widget.py b/clusterview2/ui/point_list_widget.py
index 6df8574..12ff331 100644
--- a/clusterview2/ui/point_list_widget.py
+++ b/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
diff --git a/clusterview2_ui.py b/clusterview2_ui.py
index 4fad721..a14fa48 100644
--- a/clusterview2_ui.py
+++ b/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"))
diff --git a/main_window.py b/main_window.py
index 1d84087..4860a64 100644
--- a/main_window.py
+++ b/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)
diff --git a/setup.cfg b/setup.cfg
new file mode 100644
index 0000000..6deafc2
--- /dev/null
+++ b/setup.cfg
@@ -0,0 +1,2 @@
+[flake8]
+max-line-length = 120