From 4c3be0b80eba723420c5331b215e48c23dd2f083 Mon Sep 17 00:00:00 2001 From: Taylor Bockman Date: Tue, 15 Oct 2019 21:05:26 -0700 Subject: [PATCH] Clean up clusterview old code a little bit, first launch without the grouping stuff. --- README.md | 8 + TODO.org | 13 +- clusterview2.ui | 28 +-- clusterview2/exceptions.py | 10 +- clusterview2/mode.py | 5 +- clusterview2/points.py | 364 +++++++++++++++++++++++++++++++++++ clusterview2/ui/mode_handlers.py | 136 ++++++------- clusterview2/ui/opengl_widget.py | 23 ++- clusterview2/ui/point_list_widget.py | 4 +- clusterview2_ui.py | 26 +-- main_window.py | 143 +++++++------- setup.cfg | 2 + 12 files changed, 575 insertions(+), 187 deletions(-) create mode 100644 clusterview2/points.py create mode 100644 setup.cfg 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 = "= 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