Taylor Bockman
5 years ago
18 changed files with 1898 additions and 1 deletions
@ -1,3 +1,14 @@ |
|||||||
# clusterview2 |
# clusterview2 |
||||||
|
|
||||||
The sequel to clusterview. Built around the point and cluster structure of the kmeans project, aims to improve upon the design and structural weakness of clusterview and add many interesting interactive ways to explore kmeans. |
The sequel to clusterview. Built around the point and cluster structure of the kmeans project, |
||||||
|
aims to improve upon the design and structural weakness of clusterview and add many interesting interactive ways |
||||||
|
to explore k-means. |
||||||
|
|
||||||
|
## Usage |
||||||
|
|
||||||
|
TODO |
||||||
|
|
||||||
|
## Development |
||||||
|
|
||||||
|
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. |
||||||
|
@ -0,0 +1,14 @@ |
|||||||
|
TODO: |
||||||
|
|
||||||
|
* Port over and improve clusterview and it's opengl stuff. It should use k-means point and cluster, as well as |
||||||
|
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. |
||||||
|
* 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. |
||||||
|
* Weighted cluster mean is rendered as a hollow circle and unweighted k-means mean is a x. |
@ -0,0 +1,18 @@ |
|||||||
|
import sys |
||||||
|
|
||||||
|
from PyQt5.QtWidgets import QApplication |
||||||
|
|
||||||
|
from main_window import MainWindow |
||||||
|
|
||||||
|
|
||||||
|
def main(): |
||||||
|
app = QApplication(sys.argv) |
||||||
|
|
||||||
|
window = MainWindow() |
||||||
|
window.show() |
||||||
|
|
||||||
|
sys.exit(app.exec_()) |
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__': |
||||||
|
main() |
@ -0,0 +1,380 @@ |
|||||||
|
<?xml version="1.0" encoding="UTF-8"?> |
||||||
|
<ui version="4.0"> |
||||||
|
<class>MainWindow</class> |
||||||
|
<widget class="QMainWindow" name="MainWindow"> |
||||||
|
<property name="geometry"> |
||||||
|
<rect> |
||||||
|
<x>0</x> |
||||||
|
<y>0</y> |
||||||
|
<width>1280</width> |
||||||
|
<height>720</height> |
||||||
|
</rect> |
||||||
|
</property> |
||||||
|
<property name="sizePolicy"> |
||||||
|
<sizepolicy hsizetype="Maximum" vsizetype="Minimum"> |
||||||
|
<horstretch>0</horstretch> |
||||||
|
<verstretch>0</verstretch> |
||||||
|
</sizepolicy> |
||||||
|
</property> |
||||||
|
<property name="minimumSize"> |
||||||
|
<size> |
||||||
|
<width>1280</width> |
||||||
|
<height>720</height> |
||||||
|
</size> |
||||||
|
</property> |
||||||
|
<property name="maximumSize"> |
||||||
|
<size> |
||||||
|
<width>1280</width> |
||||||
|
<height>720</height> |
||||||
|
</size> |
||||||
|
</property> |
||||||
|
<property name="windowTitle"> |
||||||
|
<string>ClusterView</string> |
||||||
|
</property> |
||||||
|
<widget class="QWidget" name="centralwidget"> |
||||||
|
<layout class="QHBoxLayout" name="horizontalLayout"> |
||||||
|
<item> |
||||||
|
<widget class="QOpenGLWidget" name="opengl_widget"> |
||||||
|
<property name="sizePolicy"> |
||||||
|
<sizepolicy hsizetype="Expanding" vsizetype="Preferred"> |
||||||
|
<horstretch>0</horstretch> |
||||||
|
<verstretch>0</verstretch> |
||||||
|
</sizepolicy> |
||||||
|
</property> |
||||||
|
<property name="maximumSize"> |
||||||
|
<size> |
||||||
|
<width>900</width> |
||||||
|
<height>16777215</height> |
||||||
|
</size> |
||||||
|
</property> |
||||||
|
</widget> |
||||||
|
</item> |
||||||
|
<item> |
||||||
|
<layout class="QVBoxLayout" name="verticalLayout"> |
||||||
|
<item> |
||||||
|
<widget class="QGroupBox" name="groupBox"> |
||||||
|
<property name="sizePolicy"> |
||||||
|
<sizepolicy hsizetype="Maximum" vsizetype="Minimum"> |
||||||
|
<horstretch>0</horstretch> |
||||||
|
<verstretch>0</verstretch> |
||||||
|
</sizepolicy> |
||||||
|
</property> |
||||||
|
<property name="minimumSize"> |
||||||
|
<size> |
||||||
|
<width>100</width> |
||||||
|
<height>0</height> |
||||||
|
</size> |
||||||
|
</property> |
||||||
|
<property name="maximumSize"> |
||||||
|
<size> |
||||||
|
<width>200</width> |
||||||
|
<height>200</height> |
||||||
|
</size> |
||||||
|
</property> |
||||||
|
<property name="title"> |
||||||
|
<string>Point List</string> |
||||||
|
</property> |
||||||
|
<layout class="QGridLayout" name="gridLayout"> |
||||||
|
<item row="0" column="0"> |
||||||
|
<widget class="QListWidget" name="point_list_widget"> |
||||||
|
<property name="sizePolicy"> |
||||||
|
<sizepolicy hsizetype="Expanding" vsizetype="Expanding"> |
||||||
|
<horstretch>0</horstretch> |
||||||
|
<verstretch>0</verstretch> |
||||||
|
</sizepolicy> |
||||||
|
</property> |
||||||
|
<property name="minimumSize"> |
||||||
|
<size> |
||||||
|
<width>100</width> |
||||||
|
<height>0</height> |
||||||
|
</size> |
||||||
|
</property> |
||||||
|
</widget> |
||||||
|
</item> |
||||||
|
</layout> |
||||||
|
</widget> |
||||||
|
</item> |
||||||
|
<item> |
||||||
|
<widget class="QGroupBox" name="groupBox_3"> |
||||||
|
<property name="title"> |
||||||
|
<string>Solver</string> |
||||||
|
</property> |
||||||
|
<layout class="QFormLayout" name="formLayout"> |
||||||
|
<item row="0" column="0"> |
||||||
|
<widget class="QLabel" name="label_2"> |
||||||
|
<property name="text"> |
||||||
|
<string>Centroids</string> |
||||||
|
</property> |
||||||
|
</widget> |
||||||
|
</item> |
||||||
|
<item row="0" column="1"> |
||||||
|
<widget class="QSpinBox" name="number_of_centroids"> |
||||||
|
<property name="sizePolicy"> |
||||||
|
<sizepolicy hsizetype="Fixed" vsizetype="Fixed"> |
||||||
|
<horstretch>0</horstretch> |
||||||
|
<verstretch>0</verstretch> |
||||||
|
</sizepolicy> |
||||||
|
</property> |
||||||
|
<property name="minimumSize"> |
||||||
|
<size> |
||||||
|
<width>50</width> |
||||||
|
<height>26</height> |
||||||
|
</size> |
||||||
|
</property> |
||||||
|
<property name="maximumSize"> |
||||||
|
<size> |
||||||
|
<width>50</width> |
||||||
|
<height>16777215</height> |
||||||
|
</size> |
||||||
|
</property> |
||||||
|
</widget> |
||||||
|
</item> |
||||||
|
<item row="3" column="0"> |
||||||
|
<widget class="QPushButton" name="choose_centroids_button"> |
||||||
|
<property name="enabled"> |
||||||
|
<bool>true</bool> |
||||||
|
</property> |
||||||
|
<property name="text"> |
||||||
|
<string>Choose Centroids</string> |
||||||
|
</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"> |
||||||
|
<property name="enabled"> |
||||||
|
<bool>false</bool> |
||||||
|
</property> |
||||||
|
<property name="text"> |
||||||
|
<string>Group</string> |
||||||
|
</property> |
||||||
|
</widget> |
||||||
|
</item> |
||||||
|
<item row="6" column="0"> |
||||||
|
<widget class="QPushButton" name="reset_button"> |
||||||
|
<property name="text"> |
||||||
|
<string>Reset</string> |
||||||
|
</property> |
||||||
|
</widget> |
||||||
|
</item> |
||||||
|
</layout> |
||||||
|
</widget> |
||||||
|
</item> |
||||||
|
<item> |
||||||
|
<spacer name="verticalSpacer_2"> |
||||||
|
<property name="orientation"> |
||||||
|
<enum>Qt::Vertical</enum> |
||||||
|
</property> |
||||||
|
<property name="sizeType"> |
||||||
|
<enum>QSizePolicy::Fixed</enum> |
||||||
|
</property> |
||||||
|
<property name="sizeHint" stdset="0"> |
||||||
|
<size> |
||||||
|
<width>20</width> |
||||||
|
<height>20</height> |
||||||
|
</size> |
||||||
|
</property> |
||||||
|
</spacer> |
||||||
|
</item> |
||||||
|
<item> |
||||||
|
<widget class="QGroupBox" name="groupBox_2"> |
||||||
|
<property name="title"> |
||||||
|
<string>Canvas Information</string> |
||||||
|
</property> |
||||||
|
<layout class="QGridLayout" name="gridLayout_2"> |
||||||
|
<item row="0" column="3"> |
||||||
|
<widget class="QLabel" name="mouse_position_label"> |
||||||
|
<property name="sizePolicy"> |
||||||
|
<sizepolicy hsizetype="Fixed" vsizetype="Preferred"> |
||||||
|
<horstretch>0</horstretch> |
||||||
|
<verstretch>0</verstretch> |
||||||
|
</sizepolicy> |
||||||
|
</property> |
||||||
|
<property name="minimumSize"> |
||||||
|
<size> |
||||||
|
<width>100</width> |
||||||
|
<height>0</height> |
||||||
|
</size> |
||||||
|
</property> |
||||||
|
<property name="text"> |
||||||
|
<string/> |
||||||
|
</property> |
||||||
|
</widget> |
||||||
|
</item> |
||||||
|
<item row="1" column="0"> |
||||||
|
<spacer name="verticalSpacer"> |
||||||
|
<property name="orientation"> |
||||||
|
<enum>Qt::Vertical</enum> |
||||||
|
</property> |
||||||
|
<property name="sizeHint" stdset="0"> |
||||||
|
<size> |
||||||
|
<width>20</width> |
||||||
|
<height>20</height> |
||||||
|
</size> |
||||||
|
</property> |
||||||
|
</spacer> |
||||||
|
</item> |
||||||
|
<item row="0" column="0"> |
||||||
|
<widget class="QLabel" name="label"> |
||||||
|
<property name="text"> |
||||||
|
<string>Mouse Position:</string> |
||||||
|
</property> |
||||||
|
</widget> |
||||||
|
</item> |
||||||
|
<item row="0" column="2"> |
||||||
|
<spacer name="horizontalSpacer"> |
||||||
|
<property name="orientation"> |
||||||
|
<enum>Qt::Horizontal</enum> |
||||||
|
</property> |
||||||
|
<property name="sizeType"> |
||||||
|
<enum>QSizePolicy::Fixed</enum> |
||||||
|
</property> |
||||||
|
<property name="sizeHint" stdset="0"> |
||||||
|
<size> |
||||||
|
<width>20</width> |
||||||
|
<height>20</height> |
||||||
|
</size> |
||||||
|
</property> |
||||||
|
</spacer> |
||||||
|
</item> |
||||||
|
</layout> |
||||||
|
</widget> |
||||||
|
</item> |
||||||
|
</layout> |
||||||
|
</item> |
||||||
|
</layout> |
||||||
|
</widget> |
||||||
|
<widget class="QMenuBar" name="menubar"> |
||||||
|
<property name="geometry"> |
||||||
|
<rect> |
||||||
|
<x>0</x> |
||||||
|
<y>0</y> |
||||||
|
<width>1280</width> |
||||||
|
<height>28</height> |
||||||
|
</rect> |
||||||
|
</property> |
||||||
|
<property name="nativeMenuBar"> |
||||||
|
<bool>true</bool> |
||||||
|
</property> |
||||||
|
<widget class="QMenu" name="menu_file"> |
||||||
|
<property name="title"> |
||||||
|
<string>File</string> |
||||||
|
</property> |
||||||
|
<addaction name="action_load_point_configuration"/> |
||||||
|
<addaction name="action_save_point_configuration"/> |
||||||
|
<addaction name="separator"/> |
||||||
|
<addaction name="action_exit"/> |
||||||
|
</widget> |
||||||
|
<widget class="QMenu" name="menu_help"> |
||||||
|
<property name="title"> |
||||||
|
<string>Help</string> |
||||||
|
</property> |
||||||
|
</widget> |
||||||
|
<addaction name="menu_file"/> |
||||||
|
<addaction name="menu_help"/> |
||||||
|
</widget> |
||||||
|
<widget class="QStatusBar" name="status_bar"/> |
||||||
|
<widget class="QToolBar" name="tool_bar"> |
||||||
|
<property name="windowTitle"> |
||||||
|
<string>toolBar</string> |
||||||
|
</property> |
||||||
|
<property name="movable"> |
||||||
|
<bool>false</bool> |
||||||
|
</property> |
||||||
|
<attribute name="toolBarArea"> |
||||||
|
<enum>LeftToolBarArea</enum> |
||||||
|
</attribute> |
||||||
|
<attribute name="toolBarBreak"> |
||||||
|
<bool>false</bool> |
||||||
|
</attribute> |
||||||
|
<addaction name="action_generate_random_points"/> |
||||||
|
<addaction name="action_add_points"/> |
||||||
|
<addaction name="action_move_points"/> |
||||||
|
<addaction name="action_edit_points"/> |
||||||
|
<addaction name="action_delete_points"/> |
||||||
|
</widget> |
||||||
|
<action name="action_add_points"> |
||||||
|
<property name="text"> |
||||||
|
<string>Add Points</string> |
||||||
|
</property> |
||||||
|
<property name="toolTip"> |
||||||
|
<string>Enables point adding mode.</string> |
||||||
|
</property> |
||||||
|
<property name="shortcut"> |
||||||
|
<string>Ctrl+A</string> |
||||||
|
</property> |
||||||
|
</action> |
||||||
|
<action name="action_edit_points"> |
||||||
|
<property name="text"> |
||||||
|
<string>Edit Points</string> |
||||||
|
</property> |
||||||
|
<property name="toolTip"> |
||||||
|
<string>Enables point editing mode.</string> |
||||||
|
</property> |
||||||
|
<property name="shortcut"> |
||||||
|
<string>Ctrl+E</string> |
||||||
|
</property> |
||||||
|
</action> |
||||||
|
<action name="action_delete_points"> |
||||||
|
<property name="text"> |
||||||
|
<string>Delete Points</string> |
||||||
|
</property> |
||||||
|
<property name="toolTip"> |
||||||
|
<string>Enables point deletion mode.</string> |
||||||
|
</property> |
||||||
|
<property name="shortcut"> |
||||||
|
<string>Ctrl+D</string> |
||||||
|
</property> |
||||||
|
</action> |
||||||
|
<action name="action_solve"> |
||||||
|
<property name="text"> |
||||||
|
<string>Solve</string> |
||||||
|
</property> |
||||||
|
<property name="toolTip"> |
||||||
|
<string>Opens the solve dialog to choose a solving solution.</string> |
||||||
|
</property> |
||||||
|
<property name="shortcut"> |
||||||
|
<string>Ctrl+S</string> |
||||||
|
</property> |
||||||
|
</action> |
||||||
|
<action name="action_move_points"> |
||||||
|
<property name="text"> |
||||||
|
<string>Move Points</string> |
||||||
|
</property> |
||||||
|
<property name="toolTip"> |
||||||
|
<string>Enables the movement of a selection of points.</string> |
||||||
|
</property> |
||||||
|
</action> |
||||||
|
<action name="action_save_point_configuration"> |
||||||
|
<property name="text"> |
||||||
|
<string>Save Point Configuration</string> |
||||||
|
</property> |
||||||
|
</action> |
||||||
|
<action name="action_load_point_configuration"> |
||||||
|
<property name="text"> |
||||||
|
<string>Load Point Configuration</string> |
||||||
|
</property> |
||||||
|
</action> |
||||||
|
<action name="action_exit"> |
||||||
|
<property name="text"> |
||||||
|
<string>Exit</string> |
||||||
|
</property> |
||||||
|
</action> |
||||||
|
<action name="action_generate_random_points"> |
||||||
|
<property name="text"> |
||||||
|
<string>Generate Random Points</string> |
||||||
|
</property> |
||||||
|
</action> |
||||||
|
</widget> |
||||||
|
<resources/> |
||||||
|
<connections/> |
||||||
|
</ui> |
@ -0,0 +1,27 @@ |
|||||||
|
from enum import Enum |
||||||
|
|
||||||
|
|
||||||
|
class Color(str, Enum): |
||||||
|
BLUE = 'BLUE' |
||||||
|
BLACK = 'BLACK' |
||||||
|
GREY = 'GREY' |
||||||
|
RED = 'RED' |
||||||
|
ORANGE = 'ORANGE' |
||||||
|
PURPLE = 'PURPLE' |
||||||
|
|
||||||
|
@classmethod |
||||||
|
def count(cls): |
||||||
|
return len(cls.__members__) |
||||||
|
|
||||||
|
|
||||||
|
# A simple map from Color -> RGBA 4-Tuple |
||||||
|
# Note: The color values in the tuple are not RGB, but |
||||||
|
# rather OpenGL percentage values for RGB. |
||||||
|
COLOR_TO_RGBA = { |
||||||
|
Color.GREY: (0.827, 0.827, 0.826, 0.0), |
||||||
|
Color.BLUE: (0.118, 0.565, 1.0, 0.0), |
||||||
|
Color.BLACK: (0.0, 0.0, 0.0, 0.0), |
||||||
|
Color.RED: (1.0, 0.0, 0.0, 0.0), |
||||||
|
Color.ORANGE: (0.98, 0.625, 0.12, 0.0), |
||||||
|
Color.PURPLE: (0.60, 0.40, 0.70, 0.0) |
||||||
|
} |
@ -0,0 +1,53 @@ |
|||||||
|
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. |
||||||
|
""" |
||||||
|
|
||||||
|
def __init__(self, mode): |
||||||
|
""" |
||||||
|
Initializes the InvalidMode exception with a |
||||||
|
mode. |
||||||
|
""" |
||||||
|
|
||||||
|
if not isinstance(mode, 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.") |
||||||
|
|
||||||
|
def handle_exceptions(func): |
||||||
|
""" |
||||||
|
A decorator designed to make exceptions thrown |
||||||
|
from a function easier to handle. |
||||||
|
|
||||||
|
The result will be that all exceptions coming from |
||||||
|
the decorated function will be caught and displayed |
||||||
|
as a error message box. |
||||||
|
|
||||||
|
Usage: |
||||||
|
|
||||||
|
@handle_exceptions |
||||||
|
def my_qt_func(): |
||||||
|
raises SomeException |
||||||
|
""" |
||||||
|
def wrapped(*args, **kwargs): |
||||||
|
try: |
||||||
|
return func(*args, **kwargs) |
||||||
|
except Exception as e: |
||||||
|
error_dialog = QErrorMessage() |
||||||
|
error_dialog.showMessage(str(e)) |
||||||
|
error_dialog.exec_() |
||||||
|
|
||||||
|
return wrapped |
@ -0,0 +1,17 @@ |
|||||||
|
from enum import Enum |
||||||
|
|
||||||
|
|
||||||
|
class Mode(Enum): |
||||||
|
""" |
||||||
|
Class to make it easier to figure out what mode |
||||||
|
we are operating in when the OpenGL window is |
||||||
|
clicked. |
||||||
|
""" |
||||||
|
OFF = 0 |
||||||
|
ADD = 1 |
||||||
|
EDIT = 2 |
||||||
|
MOVE = 3 |
||||||
|
DELETE = 4 |
||||||
|
LOADED = 5 |
||||||
|
CHOOSE_CENTROIDS = 6 # TODO: Can replace with choose weighted or something |
||||||
|
GROUP = 7 |
@ -0,0 +1,59 @@ |
|||||||
|
import json |
||||||
|
|
||||||
|
from clusterview2.colors import Color |
||||||
|
from clusterview2.points import PointSet |
||||||
|
|
||||||
|
|
||||||
|
class PointManager(): |
||||||
|
""" |
||||||
|
A state class that represents the absolute state of the |
||||||
|
world in regards to points. |
||||||
|
""" |
||||||
|
|
||||||
|
point_set = None |
||||||
|
centroids = [] |
||||||
|
|
||||||
|
@staticmethod |
||||||
|
def load(location): |
||||||
|
""" |
||||||
|
Loads the JSON file from the location and populates point_set |
||||||
|
with it's contents. |
||||||
|
|
||||||
|
@param location The location of the JSON file. |
||||||
|
""" |
||||||
|
with open(location) as json_file: |
||||||
|
data = json.load(json_file) |
||||||
|
|
||||||
|
PointManager.point_set = PointSet(data['point_size'], |
||||||
|
data['viewport_width'], |
||||||
|
data['viewport_height']) |
||||||
|
|
||||||
|
for point in data['points']: |
||||||
|
# We will need to cast the string representation of color |
||||||
|
# back into a Color enum. |
||||||
|
PointManager.point_set.add_point(point['x'], point['y'], |
||||||
|
Color(point['color'])) |
||||||
|
|
||||||
|
@staticmethod |
||||||
|
def save(location): |
||||||
|
""" |
||||||
|
Persists the point_set as a JSON file at location. |
||||||
|
|
||||||
|
@param location The persistence location. |
||||||
|
""" |
||||||
|
|
||||||
|
data = {} |
||||||
|
data['point_size'] = PointManager.point_set.point_size |
||||||
|
data['viewport_width'] = PointManager.point_set.viewport_width |
||||||
|
data['viewport_height'] = PointManager.point_set.viewport_height |
||||||
|
data['points'] = [] |
||||||
|
|
||||||
|
for p in PointManager.point_set.points: |
||||||
|
data['points'].append({ |
||||||
|
'x': p.x, |
||||||
|
'y': p.y, |
||||||
|
'color': p.color |
||||||
|
}) |
||||||
|
|
||||||
|
with open(location, 'w') as out_file: |
||||||
|
json.dump(data, out_file) |
@ -0,0 +1,402 @@ |
|||||||
|
import random |
||||||
|
|
||||||
|
from PyQt5.QtCore import QEvent, Qt |
||||||
|
from PyQt5.QtGui import QCursor |
||||||
|
|
||||||
|
from clusterview2.colors import Color |
||||||
|
from clusterview2.exceptions import ExceededWindowBoundsError |
||||||
|
from clusterview2.mode import Mode |
||||||
|
from .opengl_widget import (set_drawing_event, set_move_bb_top_left, |
||||||
|
set_move_bb_bottom_right, reset_move_bbs, |
||||||
|
viewport_height, viewport_width) |
||||||
|
from clusterview2.point_manager import PointManager |
||||||
|
|
||||||
|
|
||||||
|
class __ClickFlag: |
||||||
|
|
||||||
|
# This is the first stage. On mouse release it goes to |
||||||
|
# SELECTION_MOVE. |
||||||
|
NONE = 0 |
||||||
|
|
||||||
|
# We are now in selection box mode. |
||||||
|
SELECTION_BOX = 1 |
||||||
|
|
||||||
|
# Second stage - we have selected a number of points |
||||||
|
# and now we are going to track the left mouse button |
||||||
|
# to translate those points. After a left click |
||||||
|
# this moves to SELECTED_MOVED. |
||||||
|
SELECTION_MOVE = 2 |
||||||
|
|
||||||
|
# Any subsequent click in this mode will send it back |
||||||
|
# to NONE - we are done. |
||||||
|
SELECTED_MOVED = 3 |
||||||
|
|
||||||
|
|
||||||
|
# GLOBALS |
||||||
|
|
||||||
|
# Canvas pixel border - empirical, not sure where this is stored officially |
||||||
|
__CANVAS_BORDER = 1 |
||||||
|
|
||||||
|
# Module level flag for left click events (used to detect a left |
||||||
|
# click hold drag) |
||||||
|
__left_click_flag = __ClickFlag.NONE |
||||||
|
|
||||||
|
# Variable to track the mouse state during selection movement |
||||||
|
__last_mouse_pos = None |
||||||
|
|
||||||
|
# Used to implement mouse dragging when clicked |
||||||
|
__left_click_down = False |
||||||
|
|
||||||
|
# TODO: WHEN THE GROUPING ENDS AND THE USER CANCELS THE CENTROID COUNT |
||||||
|
# SHOULD BE ZEROED AND REMAINING COLORS SHOULD BE REPOPULATED. |
||||||
|
# Count of centroids for comparison with the spin widget |
||||||
|
__centroid_count = 0 |
||||||
|
__remaining_colors = [c for c in Color if c not in [Color.BLUE, Color.GREY]] |
||||||
|
|
||||||
|
|
||||||
|
def refresh_point_list(ctx): |
||||||
|
""" |
||||||
|
Refreshes the point list display. |
||||||
|
|
||||||
|
@param ctx A handle to the window context. |
||||||
|
""" |
||||||
|
# In order to make some guarantees and avoid duplicate |
||||||
|
# data we will clear the point list widget and re-populate |
||||||
|
# it using the current __point_set. |
||||||
|
ctx.point_list_widget.clear() |
||||||
|
|
||||||
|
for p in PointManager.point_set.points: |
||||||
|
ctx.point_list_widget.addItem("({}, {})".format(p.x, p.y)) |
||||||
|
|
||||||
|
ctx.point_list_widget.update() |
||||||
|
|
||||||
|
|
||||||
|
def __handle_add_point(ctx, event): |
||||||
|
""" |
||||||
|
Event handler for the add point mode. |
||||||
|
|
||||||
|
Sets the drawing mode for the OpenGL Widget using |
||||||
|
`set_drawing_mode`, converts a point to our point |
||||||
|
representation, and adds it to the list. |
||||||
|
|
||||||
|
@param ctx A context handle to the main window. |
||||||
|
@param event The click event. |
||||||
|
""" |
||||||
|
|
||||||
|
# Update information as needed |
||||||
|
__handle_info_updates(ctx, event) |
||||||
|
|
||||||
|
if (event.button() == Qt.LeftButton and |
||||||
|
event.type() == QEvent.MouseButtonPress): |
||||||
|
|
||||||
|
# At this point we can be sure resize_gl has been called |
||||||
|
# at least once, so set the viewport properties of the |
||||||
|
# point set so it knows the canvas bounds. |
||||||
|
PointManager.point_set.viewport_width = viewport_width() |
||||||
|
PointManager.point_set.viewport_height = viewport_height() |
||||||
|
|
||||||
|
# Clear any existing selections |
||||||
|
PointManager.point_set.clear_selection() |
||||||
|
|
||||||
|
try: |
||||||
|
# No attribute at the moment, default point color is Color.GREY. |
||||||
|
PointManager.point_set.add_point(event.x(), event.y(), Color.GREY) |
||||||
|
except ExceededWindowBoundsError: |
||||||
|
# The user tried to place a point whos edges would be |
||||||
|
# on the outside of the window. We will just ignore it. |
||||||
|
return |
||||||
|
|
||||||
|
refresh_point_list(ctx) |
||||||
|
|
||||||
|
set_drawing_event(event) |
||||||
|
|
||||||
|
ctx.opengl_widget.update() |
||||||
|
ctx.point_list_widget.update() |
||||||
|
|
||||||
|
|
||||||
|
def __handle_edit_point(ctx, event): |
||||||
|
# TODO: This function and delete definitely need to make sure they are |
||||||
|
# on a point we have. |
||||||
|
# |
||||||
|
# Since points are unique consider a hashmap of points to make O(1) |
||||||
|
# lookups for addition and deletion. This list can be maintained here |
||||||
|
# in this module. It should be a dictionary - from point to |
||||||
|
# attributes in the case of algorithms that require points to have |
||||||
|
# weights or something. |
||||||
|
# |
||||||
|
# Should move the associated point in the list to the new location if |
||||||
|
# applicable. |
||||||
|
|
||||||
|
__handle_info_updates(ctx, event) |
||||||
|
PointManager.point_set.clear_selection() |
||||||
|
|
||||||
|
# Store old x, y from event |
||||||
|
set_drawing_event(event) |
||||||
|
ctx.update() |
||||||
|
# after this remove the point from the list |
||||||
|
|
||||||
|
|
||||||
|
def ogl_keypress_handler(ctx, event): |
||||||
|
""" |
||||||
|
A keypress handler attached to the OpenGL widget. |
||||||
|
|
||||||
|
It primarily exists to allow the user to cancel selection. |
||||||
|
|
||||||
|
Also allows users to escape from modes. |
||||||
|
|
||||||
|
@param ctx A handle to the window context. |
||||||
|
@param event The event associated with this handler. |
||||||
|
""" |
||||||
|
global __left_click_flag |
||||||
|
global __last_mouse_pos |
||||||
|
|
||||||
|
if event.key() == Qt.Key_Escape: |
||||||
|
if ctx.mode is Mode.MOVE: |
||||||
|
if __left_click_flag is not __ClickFlag.NONE: |
||||||
|
|
||||||
|
__last_mouse_pos = None |
||||||
|
|
||||||
|
__left_click_flag = __ClickFlag.NONE |
||||||
|
PointManager.point_set.clear_selection() |
||||||
|
reset_move_bbs() |
||||||
|
refresh_point_list(ctx) |
||||||
|
|
||||||
|
elif ctx.mode is not Mode.OFF: |
||||||
|
ctx.mode = Mode.OFF |
||||||
|
|
||||||
|
# Also change the mouse back to normal |
||||||
|
ctx.opengl_widget.setCursor(QCursor(Qt.CursorShape.ArrowCursor)) |
||||||
|
ctx.status_bar.showMessage("") |
||||||
|
|
||||||
|
ctx.opengl_widget.update() |
||||||
|
|
||||||
|
|
||||||
|
def __handle_move_points(ctx, event): |
||||||
|
""" |
||||||
|
A relatively complicated state machine that handles the process of |
||||||
|
selection, clicking, and dragging. |
||||||
|
|
||||||
|
@param ctx The context to the window. |
||||||
|
@param event The event. |
||||||
|
""" |
||||||
|
|
||||||
|
global __left_click_flag |
||||||
|
global __left_mouse_down |
||||||
|
global __last_mouse_pos |
||||||
|
|
||||||
|
set_drawing_event(event) |
||||||
|
|
||||||
|
__handle_info_updates(ctx, event) |
||||||
|
|
||||||
|
# If we release the mouse, we want to quickly alert drag mode. |
||||||
|
if (event.button() == Qt.LeftButton and |
||||||
|
event.type() == QEvent.MouseButtonRelease): |
||||||
|
|
||||||
|
__left_mouse_down = False |
||||||
|
|
||||||
|
# This if statement block is used to set the bounding box for |
||||||
|
# drawing and call the selection procedure. |
||||||
|
if (event.button() == Qt.LeftButton and |
||||||
|
event.type() == QEvent.MouseButtonPress): |
||||||
|
|
||||||
|
__left_mouse_down = True |
||||||
|
|
||||||
|
if __left_click_flag is __ClickFlag.NONE: |
||||||
|
__left_click_flag = __ClickFlag.SELECTION_BOX |
||||||
|
|
||||||
|
set_move_bb_top_left(event.x(), event.y()) |
||||||
|
|
||||||
|
elif (__left_click_flag is __ClickFlag.SELECTION_BOX |
||||||
|
and __left_mouse_down): |
||||||
|
# We are now in the click-and-hold to signal move |
||||||
|
# tracking and translation |
||||||
|
__left_click_flag = __ClickFlag.SELECTION_MOVE |
||||||
|
__last_mouse_pos = (event.x(), event.y()) |
||||||
|
|
||||||
|
# Post-selection handlers |
||||||
|
if (__left_click_flag is __ClickFlag.SELECTION_BOX |
||||||
|
and event.type() == QEvent.MouseMove): |
||||||
|
|
||||||
|
set_move_bb_bottom_right(event.x(), event.y()) |
||||||
|
|
||||||
|
elif (__left_click_flag is __ClickFlag.SELECTION_MOVE |
||||||
|
and __last_mouse_pos is not None |
||||||
|
and __left_mouse_down |
||||||
|
and event.type() == QEvent.MouseMove): |
||||||
|
|
||||||
|
dx = abs(__last_mouse_pos[0] - event.x()) |
||||||
|
dy = abs(__last_mouse_pos[1] - event.y()) |
||||||
|
|
||||||
|
for p in PointManager.point_set.points: |
||||||
|
if p.selected: |
||||||
|
# Use the deltas to decide what direction to move. |
||||||
|
# We only want to move in small unit increments. |
||||||
|
# If we used the deltas directly the points would |
||||||
|
# fly off screen quickly as we got farther from our |
||||||
|
# start. |
||||||
|
try: |
||||||
|
if event.x() < __last_mouse_pos[0]: |
||||||
|
p.move(-dx, 0) |
||||||
|
if event.y() < __last_mouse_pos[1]: |
||||||
|
p.move(0, -dy) |
||||||
|
if event.x() > __last_mouse_pos[0]: |
||||||
|
p.move(dx, 0) |
||||||
|
if event.y() > __last_mouse_pos[1]: |
||||||
|
p.move(0, dy) |
||||||
|
|
||||||
|
except ExceededWindowBoundsError: |
||||||
|
# This point has indicated a move would exceed |
||||||
|
# it's bounds, so we'll just go to the next |
||||||
|
# point. |
||||||
|
continue |
||||||
|
|
||||||
|
__last_mouse_pos = (event.x(), event.y()) |
||||||
|
|
||||||
|
elif (__left_click_flag is not __ClickFlag.NONE and |
||||||
|
event.type() == QEvent.MouseButtonRelease): |
||||||
|
|
||||||
|
if __left_click_flag is __ClickFlag.SELECTION_BOX: |
||||||
|
|
||||||
|
set_move_bb_bottom_right(event.x(), event.y()) |
||||||
|
|
||||||
|
# Satisfy the post condition by resetting the bounding box |
||||||
|
reset_move_bbs() |
||||||
|
|
||||||
|
ctx.opengl_widget.update() |
||||||
|
|
||||||
|
|
||||||
|
def __handle_delete_point(ctx, event): |
||||||
|
|
||||||
|
__handle_info_updates(ctx, event) |
||||||
|
|
||||||
|
if (event.button() == Qt.LeftButton and |
||||||
|
event.type() == QEvent.MouseButtonPress): |
||||||
|
|
||||||
|
set_drawing_event(event) |
||||||
|
|
||||||
|
PointManager.point_set.remove_point(event.x(), event.y()) |
||||||
|
|
||||||
|
refresh_point_list(ctx) |
||||||
|
|
||||||
|
ctx.opengl_widget.update() |
||||||
|
ctx.point_list_widget.update() |
||||||
|
|
||||||
|
|
||||||
|
def __handle_info_updates(ctx, event): |
||||||
|
""" |
||||||
|
Updates data under the "information" header. |
||||||
|
|
||||||
|
@param ctx The context to the main window. |
||||||
|
@param event The event. |
||||||
|
""" |
||||||
|
if event.type() == QEvent.MouseMove: |
||||||
|
ctx.mouse_position_label.setText(f"{event.x(), event.y()}") |
||||||
|
|
||||||
|
|
||||||
|
def __handle_choose_centroids(ctx, event): |
||||||
|
""" |
||||||
|
Similar to move in terms of selecting points, however this |
||||||
|
function assigns a random color up to the maximum number |
||||||
|
of centroids, and after the maximum number has been selected it will |
||||||
|
enable the group button. |
||||||
|
""" |
||||||
|
global __centroid_count |
||||||
|
global __remaining_colors |
||||||
|
|
||||||
|
__handle_info_updates(ctx, event) |
||||||
|
|
||||||
|
if __centroid_count == ctx.number_of_centroids.value(): |
||||||
|
# We have specified the number of centroids required |
||||||
|
return |
||||||
|
|
||||||
|
if (event.button() == Qt.LeftButton and |
||||||
|
event.type() == QEvent.MouseButtonPress): |
||||||
|
|
||||||
|
point = None |
||||||
|
|
||||||
|
for test_point in PointManager.point_set.points: |
||||||
|
if test_point.hit(event.x(), event.y()): |
||||||
|
point = test_point |
||||||
|
|
||||||
|
if point is None: |
||||||
|
# No point was found on the click, do nothing |
||||||
|
return |
||||||
|
|
||||||
|
if point in PointManager.centroids: |
||||||
|
# Centroids must be unique |
||||||
|
return |
||||||
|
|
||||||
|
__centroid_count += 1 |
||||||
|
|
||||||
|
color = random.choice(__remaining_colors) |
||||||
|
__remaining_colors.remove(color) |
||||||
|
|
||||||
|
point.color = color |
||||||
|
|
||||||
|
# Recolor the point and restash the point in centroids |
||||||
|
PointManager.centroids.append(point) |
||||||
|
|
||||||
|
if __centroid_count == ctx.number_of_centroids.value(): |
||||||
|
# Prevent the user from changing the centroids |
||||||
|
ctx.number_of_centroids.setEnabled(False) |
||||||
|
ctx.choose_centroids_button.setEnabled(False) |
||||||
|
ctx.group_button.setEnabled(True) |
||||||
|
|
||||||
|
ctx.opengl_widget.update() |
||||||
|
|
||||||
|
|
||||||
|
def reset_centroid_count_and_colors(): |
||||||
|
global __centroid_count |
||||||
|
global __remaining_colors |
||||||
|
|
||||||
|
__centroid_count = 0 |
||||||
|
__remaining_colors = [c for c in Color if |
||||||
|
c not in [Color.BLUE, Color.GREY]] |
||||||
|
|
||||||
|
for point in PointManager.point_set.points: |
||||||
|
point.color = Color.GREY |
||||||
|
|
||||||
|
|
||||||
|
def generate_random_points(point_count, x_bound, y_bound): |
||||||
|
""" |
||||||
|
Using the random module of python generate a unique set of xs and ys |
||||||
|
to use as points, bounded by the canvas edges. |
||||||
|
|
||||||
|
@param point_count The count of points to generate. |
||||||
|
@param x_bound The width bound. |
||||||
|
@param y_bound The height bound. |
||||||
|
""" |
||||||
|
|
||||||
|
# TODO: The window size should be increased slightly to |
||||||
|
# accomodate 3000 points (the maximum) given the point size. |
||||||
|
# Work out an algorithm and limit the number of points |
||||||
|
# selectable based on the maximum amount of points on the screen |
||||||
|
# given the point size. |
||||||
|
|
||||||
|
# First clear the point set |
||||||
|
PointManager.point_set.clear() |
||||||
|
|
||||||
|
point_size = PointManager.point_set.point_size |
||||||
|
|
||||||
|
# Sample without replacement so points are not duplicated. |
||||||
|
xs = random.sample(range(point_size, x_bound), point_count) |
||||||
|
|
||||||
|
ys = random.sample(range(point_size, y_bound), point_count) |
||||||
|
|
||||||
|
points = list(zip(xs, ys)) |
||||||
|
|
||||||
|
for point in points: |
||||||
|
PointManager.point_set.add_point(point[0], point[1], Color.GREY) |
||||||
|
|
||||||
|
|
||||||
|
# Simple dispatcher to make it easy to dispatch the right mode |
||||||
|
# function when the OpenGL window is acted on. |
||||||
|
MODE_HANDLER_MAP = { |
||||||
|
Mode.OFF: __handle_info_updates, |
||||||
|
Mode.LOADED: __handle_info_updates, |
||||||
|
Mode.ADD: __handle_add_point, |
||||||
|
Mode.EDIT: __handle_edit_point, |
||||||
|
Mode.MOVE: __handle_move_points, |
||||||
|
Mode.DELETE: __handle_delete_point, |
||||||
|
Mode.CHOOSE_CENTROIDS: __handle_choose_centroids |
||||||
|
} |
@ -0,0 +1,389 @@ |
|||||||
|
""" |
||||||
|
This module defines functions that need to be overwritten |
||||||
|
in order for OpenGL to work with the main window. This |
||||||
|
module is named the same as the actual widget in order |
||||||
|
to make namespacing consistent. |
||||||
|
|
||||||
|
To be clear, the actual widget is defined in the UI |
||||||
|
generated code - `clusterview_ui.py`. The functions |
||||||
|
here are imported as overrides to the OpenGL functions of |
||||||
|
that widget. |
||||||
|
|
||||||
|
It should be split up into a few more separate files eventually... |
||||||
|
Probably even into it's own module folder. |
||||||
|
""" |
||||||
|
|
||||||
|
from OpenGL.GL import (glBegin, glClearColor, glColor3f, |
||||||
|
glEnd, GL_LINE_LOOP, GL_POINTS, |
||||||
|
glPointSize, glVertex3f, glViewport) |
||||||
|
|
||||||
|
from clusterview2.colors import Color, COLOR_TO_RGBA |
||||||
|
from clusterview2.exceptions import (handle_exceptions, |
||||||
|
InvalidStateError) |
||||||
|
from clusterview2.mode import Mode |
||||||
|
from clusterview2.point_manager import PointManager |
||||||
|
|
||||||
|
# Constants set based on the size of the window. |
||||||
|
__BOTTOM_LEFT = (0, 0) |
||||||
|
__WIDTH = None |
||||||
|
__HEIGHT = None |
||||||
|
|
||||||
|
# State variables for a move selection bounding box. |
||||||
|
# There are always reset to None after a selection has been made. |
||||||
|
__move_bb_top_left = None |
||||||
|
__move_bb_bottom_right = None |
||||||
|
|
||||||
|
# Module-global state variables for our drawing |
||||||
|
# state machine. |
||||||
|
# |
||||||
|
# Below functions have to mark these as `global` so |
||||||
|
# the interpreter knows that the variables are not |
||||||
|
# function local. |
||||||
|
__current_context = None |
||||||
|
__current_event = None |
||||||
|
|
||||||
|
|
||||||
|
def set_drawing_context(ctx): |
||||||
|
""" |
||||||
|
Sets the drawing context so that drawing functions can properly |
||||||
|
interact with the widget. |
||||||
|
""" |
||||||
|
global __current_context |
||||||
|
|
||||||
|
__current_context = ctx |
||||||
|
|
||||||
|
|
||||||
|
def set_drawing_event(event): |
||||||
|
""" |
||||||
|
State machine event management function. |
||||||
|
|
||||||
|
@param event The event. |
||||||
|
""" |
||||||
|
global __current_context |
||||||
|
global __current_event |
||||||
|
|
||||||
|
if __current_context is None: |
||||||
|
raise InvalidStateError("Drawing context must be set before setting " + |
||||||
|
"drawing mode") |
||||||
|
|
||||||
|
if event is not None: |
||||||
|
__current_event = event |
||||||
|
|
||||||
|
|
||||||
|
def mouse_leave(ctx, event): |
||||||
|
""" |
||||||
|
The leave event for the OpenGL widget to properly reset the mouse |
||||||
|
position label. |
||||||
|
|
||||||
|
@param ctx The context. |
||||||
|
@param event The event. |
||||||
|
""" |
||||||
|
ctx.mouse_position_label.setText('') |
||||||
|
|
||||||
|
|
||||||
|
def set_move_bb_top_left(x, y): |
||||||
|
""" |
||||||
|
Called to set the move bounding box's top left corner. |
||||||
|
|
||||||
|
@param x The x-coordinate. |
||||||
|
@param y The y-coordinate. |
||||||
|
""" |
||||||
|
global __move_bb_top_left |
||||||
|
|
||||||
|
__move_bb_top_left = (x, y) |
||||||
|
|
||||||
|
|
||||||
|
def set_move_bb_bottom_right(x, y): |
||||||
|
""" |
||||||
|
Called to set the move bounding box's bottom right corner. |
||||||
|
|
||||||
|
@param x The x-coordinate. |
||||||
|
@param y The y-coordinate. |
||||||
|
""" |
||||||
|
global __move_bb_bottom_right |
||||||
|
|
||||||
|
__move_bb_bottom_right = (x, y) |
||||||
|
|
||||||
|
|
||||||
|
def get_bb_top_left(): |
||||||
|
return __move_bb_top_left |
||||||
|
|
||||||
|
|
||||||
|
def get_bb_bottom_right(): |
||||||
|
return __move_bb_bottom_right |
||||||
|
|
||||||
|
|
||||||
|
def reset_move_bbs(): |
||||||
|
global __move_bb_top_left |
||||||
|
global __move_bb_bottom_right |
||||||
|
|
||||||
|
__move_bb_top_left = None |
||||||
|
__move_bb_bottom_right = None |
||||||
|
|
||||||
|
|
||||||
|
def initialize_gl(): |
||||||
|
""" |
||||||
|
Initializes the OpenGL context on the Window. |
||||||
|
""" |
||||||
|
|
||||||
|
# Set white background |
||||||
|
glClearColor(255, 255, 255, 0) |
||||||
|
|
||||||
|
|
||||||
|
def resize_gl(w, h): |
||||||
|
""" |
||||||
|
OpenGL resize handler used to get the current viewport size. |
||||||
|
|
||||||
|
@param w The new width. |
||||||
|
@param h The new height. |
||||||
|
""" |
||||||
|
global __WIDTH |
||||||
|
global __HEIGHT |
||||||
|
|
||||||
|
__WIDTH = __current_context.opengl_widget.width() |
||||||
|
__HEIGHT = __current_context.opengl_widget.height() |
||||||
|
|
||||||
|
|
||||||
|
def viewport_width(): |
||||||
|
return __WIDTH |
||||||
|
|
||||||
|
|
||||||
|
def viewport_height(): |
||||||
|
return __HEIGHT |
||||||
|
|
||||||
|
|
||||||
|
@handle_exceptions |
||||||
|
def paint_gl(): |
||||||
|
""" |
||||||
|
Stock PaintGL function from OpenGL that switches |
||||||
|
on the current mode to determine what action to |
||||||
|
perform on the current event. |
||||||
|
""" |
||||||
|
if(__current_context.mode is Mode.OFF and |
||||||
|
not PointManager.point_set.empty()): |
||||||
|
|
||||||
|
# We want to redraw on any change to Mode.OFF so points are preserved - |
||||||
|
# without this, any switch to Mode.OFF will cause a blank screen to |
||||||
|
# render. |
||||||
|
draw_points(PointManager.point_set) |
||||||
|
|
||||||
|
if (__current_context.mode in [Mode.ADD, Mode.EDIT, |
||||||
|
Mode.MOVE, Mode.DELETE] and |
||||||
|
__current_event is None and PointManager.point_set.empty()): |
||||||
|
return |
||||||
|
|
||||||
|
if (__current_context.mode in [Mode.ADD, Mode.EDIT, Mode.DELETE] and |
||||||
|
PointManager.point_set.empty()): |
||||||
|
return |
||||||
|
|
||||||
|
if (__current_context.mode is Mode.ADD or |
||||||
|
__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): |
||||||
|
|
||||||
|
draw_points(PointManager.point_set) |
||||||
|
|
||||||
|
elif __current_context.mode is Mode.EDIT: |
||||||
|
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 |
||||||
|
# move box. |
||||||
|
if not PointManager.point_set.empty(): |
||||||
|
draw_points(PointManager.point_set) |
||||||
|
|
||||||
|
draw_selection_box(Color.BLACK) |
||||||
|
|
||||||
|
if (__move_bb_top_left is not None and |
||||||
|
__move_bb_bottom_right is not None): |
||||||
|
|
||||||
|
# Mark points that are selected in the bounding box |
||||||
|
# and draw them using the normal function |
||||||
|
highlight_selection() |
||||||
|
draw_points(PointManager.point_set) |
||||||
|
|
||||||
|
|
||||||
|
def __clamp_x(x): |
||||||
|
""" |
||||||
|
X-coordinate clamping function that goes from mouse coordinates to |
||||||
|
OpenGL coordinates. |
||||||
|
|
||||||
|
@param x The x-coordinate to clamp. |
||||||
|
@returns The clamped x coordinate. |
||||||
|
""" |
||||||
|
x_w = (x / (__WIDTH / 2.0) - 1.0) |
||||||
|
return x_w |
||||||
|
|
||||||
|
|
||||||
|
def __clamp_y(y): |
||||||
|
""" |
||||||
|
Y-coordinate clamping function that goes from mouse coordinates to |
||||||
|
OpenGL coordinates. |
||||||
|
|
||||||
|
@param y The y-coordinate to clamp. |
||||||
|
@returns The clamped y coordinate. |
||||||
|
""" |
||||||
|
y_w = -1.0 * (y / (__HEIGHT / 2.0) - 1.0) |
||||||
|
return y_w |
||||||
|
|
||||||
|
|
||||||
|
def box_hit(tx, ty, x1, y1, x2, y2): |
||||||
|
""" |
||||||
|
Calculates whether or not a given point collides with the given bounding |
||||||
|
box. |
||||||
|
|
||||||
|
@param tx The target x. |
||||||
|
@param ty The target y. |
||||||
|
@param x1 The top left x. |
||||||
|
@param y1 The top left y. |
||||||
|
@param x2 The bottom left x. |
||||||
|
@param y2 The bottom left y. |
||||||
|
""" |
||||||
|
|
||||||
|
# The box in this case is flipped - the user started at the bottom right |
||||||
|
# corner. Pixel-wise top left is (0, 0) and bottom right is |
||||||
|
# (screen_x, screen_y) |
||||||
|
if x1 > x2 and y1 > y2: |
||||||
|
return (tx <= x1 and |
||||||
|
tx >= x2 and |
||||||
|
ty <= y1 and |
||||||
|
ty >= y2) |
||||||
|
|
||||||
|
# The box in this case started from the top right |
||||||
|
if x1 > x2 and y1 < y2: |
||||||
|
return (tx <= x1 and |
||||||
|
tx >= x2 and |
||||||
|
ty >= y1 and |
||||||
|
ty <= y2) |
||||||
|
|
||||||
|
# The box in this case started from the bottom left |
||||||
|
if x1 < x2 and y1 > y2: |
||||||
|
return (tx >= x1 and |
||||||
|
tx <= x2 and |
||||||
|
ty <= y1 and |
||||||
|
ty >= y2) |
||||||
|
|
||||||
|
# Normal condition: Box starts from the top left |
||||||
|
return (tx >= x1 and |
||||||
|
tx <= x2 and |
||||||
|
ty >= y1 and |
||||||
|
ty <= y2) |
||||||
|
|
||||||
|
|
||||||
|
def highlight_selection(): |
||||||
|
""" |
||||||
|
Given the current move bounding box, highlights any points inside it. |
||||||
|
""" |
||||||
|
|
||||||
|
top_left = get_bb_top_left() |
||||||
|
bottom_right = get_bb_bottom_right() |
||||||
|
|
||||||
|
for point in PointManager.point_set.points: |
||||||
|
if box_hit(point.x, point.y, top_left[0], top_left[1], |
||||||
|
bottom_right[0], bottom_right[1]): |
||||||
|
|
||||||
|
point.select() |
||||||
|
else: |
||||||
|
point.unselect() |
||||||
|
|
||||||
|
|
||||||
|
def draw_selection_box(color): |
||||||
|
""" |
||||||
|
When the move bounding box state is populated and the mode is set |
||||||
|
to MODE.Move this function will draw the selection bounding box. |
||||||
|
|
||||||
|
@param color The color Enum. |
||||||
|
""" |
||||||
|
global __current_context |
||||||
|
|
||||||
|
if __current_context is None: |
||||||
|
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") |
||||||
|
|
||||||
|
if __move_bb_top_left is None or __move_bb_bottom_right is None: |
||||||
|
# Nothing to draw. |
||||||
|
return |
||||||
|
|
||||||
|
ct = COLOR_TO_RGBA[color] |
||||||
|
|
||||||
|
glViewport(0, 0, __WIDTH, __HEIGHT) |
||||||
|
|
||||||
|
# Top right corner has the same x as the bottom right |
||||||
|
# and same y as the top left. |
||||||
|
top_right_corner = (__move_bb_bottom_right[0], __move_bb_top_left[1]) |
||||||
|
|
||||||
|
# Bottom left corner has the same x as the top left and |
||||||
|
# same y as the bottom right. |
||||||
|
bottom_left_corner = (__move_bb_top_left[0], __move_bb_bottom_right[1]) |
||||||
|
|
||||||
|
glBegin(GL_LINE_LOOP) |
||||||
|
glColor3f(ct[0], ct[1], ct[2]) |
||||||
|
|
||||||
|
glVertex3f(__clamp_x(__move_bb_top_left[0]), |
||||||
|
__clamp_y(__move_bb_top_left[1]), |
||||||
|
0.0) |
||||||
|
|
||||||
|
glVertex3f(__clamp_x(top_right_corner[0]), |
||||||
|
__clamp_y(top_right_corner[1]), |
||||||
|
0.0) |
||||||
|
|
||||||
|
glVertex3f(__clamp_x(__move_bb_bottom_right[0]), |
||||||
|
__clamp_y(__move_bb_bottom_right[1]), |
||||||
|
0.0) |
||||||
|
|
||||||
|
glVertex3f(__clamp_x(bottom_left_corner[0]), |
||||||
|
__clamp_y(bottom_left_corner[1]), |
||||||
|
0.0) |
||||||
|
|
||||||
|
glEnd() |
||||||
|
|
||||||
|
|
||||||
|
def clear_selection(): |
||||||
|
""" |
||||||
|
A helper designed to be called from the main window |
||||||
|
in order to clear the selection internal to the graphics |
||||||
|
and mode files. This way you dont have to do something |
||||||
|
before the selection clears. |
||||||
|
""" |
||||||
|
if not PointManager.point_set.empty(): |
||||||
|
PointManager.point_set.clear_selection() |
||||||
|
|
||||||
|
|
||||||
|
def draw_points(point_set): |
||||||
|
""" |
||||||
|
Simple point drawing function. |
||||||
|
|
||||||
|
Given a coordinate (x, y), and a Color enum this |
||||||
|
function will draw the given point with the given |
||||||
|
color. |
||||||
|
|
||||||
|
@param point_set The PointSet to draw. |
||||||
|
@param color The Color Enum. |
||||||
|
""" |
||||||
|
global __current_context |
||||||
|
|
||||||
|
if __current_context is None: |
||||||
|
raise InvalidStateError("Drawing context must be set before setting " + |
||||||
|
"drawing mode") |
||||||
|
|
||||||
|
glViewport(0, 0, __WIDTH, __HEIGHT) |
||||||
|
glPointSize(PointManager.point_set.point_size) |
||||||
|
|
||||||
|
glBegin(GL_POINTS) |
||||||
|
for point in point_set.points: |
||||||
|
|
||||||
|
if point.selected: |
||||||
|
blue = COLOR_TO_RGBA[Color.BLUE] |
||||||
|
glColor3f(blue[0], blue[1], blue[2]) |
||||||
|
else: |
||||||
|
ct = COLOR_TO_RGBA[point.color] |
||||||
|
glColor3f(ct[0], ct[1], ct[2]) |
||||||
|
|
||||||
|
glVertex3f(__clamp_x(point.x), |
||||||
|
__clamp_y(point.y), |
||||||
|
0.0) # Z is currently fixed to 0 |
||||||
|
glEnd() |
@ -0,0 +1,53 @@ |
|||||||
|
""" |
||||||
|
Similar to the opengl_widget module, this module defines |
||||||
|
helper functions for the point_list_widget. It is named |
||||||
|
the same for convenience. The actual point_list_widget |
||||||
|
is defined in the clusterview_ui.py file. |
||||||
|
""" |
||||||
|
|
||||||
|
from clusterview2.point_manager import PointManager |
||||||
|
|
||||||
|
|
||||||
|
def __string_point_to_point(str_point): |
||||||
|
""" |
||||||
|
In the QListWidget points are stored as strings |
||||||
|
because of the way Qt has list items defined. |
||||||
|
|
||||||
|
@param str_point The string of the form (x, y) to convert. |
||||||
|
""" |
||||||
|
|
||||||
|
# 1. Split |
||||||
|
elems = str_point.split(",") |
||||||
|
|
||||||
|
# 2. Take elements "(x" and "y)" and remove their first and |
||||||
|
# last characters, respectively. Note that for y this |
||||||
|
# function expects there to be a space after the comma. |
||||||
|
x = elems[0][1:] |
||||||
|
y = elems[1][1:-1] |
||||||
|
|
||||||
|
return (int(x), int(y)) |
||||||
|
|
||||||
|
|
||||||
|
def item_click_handler(ctx, item): |
||||||
|
""" |
||||||
|
Handles an item becoming clicked in the list. |
||||||
|
|
||||||
|
This function is designed to be partially applied with the |
||||||
|
main_window context in order to be able to trigger an opengl_widget |
||||||
|
refresh. |
||||||
|
|
||||||
|
@param ctx The context. |
||||||
|
@param item The clicked item. |
||||||
|
""" |
||||||
|
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 |
||||||
|
# is backed by a set anyway. |
||||||
|
for p in PointManager.point_set.points: |
||||||
|
if p.x == point[0] and p.y == point[1]: |
||||||
|
p.select() |
||||||
|
else: |
||||||
|
p.unselect() |
||||||
|
|
||||||
|
ctx.opengl_widget.update() |
@ -0,0 +1,200 @@ |
|||||||
|
# -*- coding: utf-8 -*- |
||||||
|
|
||||||
|
# Form implementation generated from reading ui file 'clusterview.ui' |
||||||
|
# |
||||||
|
# Created by: PyQt5 UI code generator 5.13.0 |
||||||
|
# |
||||||
|
# WARNING! All changes made in this file will be lost! |
||||||
|
|
||||||
|
|
||||||
|
from PyQt5 import QtCore, QtGui, QtWidgets |
||||||
|
|
||||||
|
|
||||||
|
class Ui_MainWindow(object): |
||||||
|
def setupUi(self, MainWindow): |
||||||
|
MainWindow.setObjectName("MainWindow") |
||||||
|
MainWindow.resize(1280, 720) |
||||||
|
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Maximum, QtWidgets.QSizePolicy.Minimum) |
||||||
|
sizePolicy.setHorizontalStretch(0) |
||||||
|
sizePolicy.setVerticalStretch(0) |
||||||
|
sizePolicy.setHeightForWidth(MainWindow.sizePolicy().hasHeightForWidth()) |
||||||
|
MainWindow.setSizePolicy(sizePolicy) |
||||||
|
MainWindow.setMinimumSize(QtCore.QSize(1280, 720)) |
||||||
|
MainWindow.setMaximumSize(QtCore.QSize(1280, 720)) |
||||||
|
self.centralwidget = QtWidgets.QWidget(MainWindow) |
||||||
|
self.centralwidget.setObjectName("centralwidget") |
||||||
|
self.horizontalLayout = QtWidgets.QHBoxLayout(self.centralwidget) |
||||||
|
self.horizontalLayout.setObjectName("horizontalLayout") |
||||||
|
self.opengl_widget = QtWidgets.QOpenGLWidget(self.centralwidget) |
||||||
|
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Preferred) |
||||||
|
sizePolicy.setHorizontalStretch(0) |
||||||
|
sizePolicy.setVerticalStretch(0) |
||||||
|
sizePolicy.setHeightForWidth(self.opengl_widget.sizePolicy().hasHeightForWidth()) |
||||||
|
self.opengl_widget.setSizePolicy(sizePolicy) |
||||||
|
self.opengl_widget.setMaximumSize(QtCore.QSize(900, 16777215)) |
||||||
|
self.opengl_widget.setObjectName("opengl_widget") |
||||||
|
self.horizontalLayout.addWidget(self.opengl_widget) |
||||||
|
self.verticalLayout = QtWidgets.QVBoxLayout() |
||||||
|
self.verticalLayout.setObjectName("verticalLayout") |
||||||
|
self.groupBox = QtWidgets.QGroupBox(self.centralwidget) |
||||||
|
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Maximum, QtWidgets.QSizePolicy.Minimum) |
||||||
|
sizePolicy.setHorizontalStretch(0) |
||||||
|
sizePolicy.setVerticalStretch(0) |
||||||
|
sizePolicy.setHeightForWidth(self.groupBox.sizePolicy().hasHeightForWidth()) |
||||||
|
self.groupBox.setSizePolicy(sizePolicy) |
||||||
|
self.groupBox.setMinimumSize(QtCore.QSize(100, 0)) |
||||||
|
self.groupBox.setMaximumSize(QtCore.QSize(200, 200)) |
||||||
|
self.groupBox.setObjectName("groupBox") |
||||||
|
self.gridLayout = QtWidgets.QGridLayout(self.groupBox) |
||||||
|
self.gridLayout.setObjectName("gridLayout") |
||||||
|
self.point_list_widget = QtWidgets.QListWidget(self.groupBox) |
||||||
|
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding) |
||||||
|
sizePolicy.setHorizontalStretch(0) |
||||||
|
sizePolicy.setVerticalStretch(0) |
||||||
|
sizePolicy.setHeightForWidth(self.point_list_widget.sizePolicy().hasHeightForWidth()) |
||||||
|
self.point_list_widget.setSizePolicy(sizePolicy) |
||||||
|
self.point_list_widget.setMinimumSize(QtCore.QSize(100, 0)) |
||||||
|
self.point_list_widget.setObjectName("point_list_widget") |
||||||
|
self.gridLayout.addWidget(self.point_list_widget, 0, 0, 1, 1) |
||||||
|
self.verticalLayout.addWidget(self.groupBox) |
||||||
|
self.groupBox_3 = QtWidgets.QGroupBox(self.centralwidget) |
||||||
|
self.groupBox_3.setObjectName("groupBox_3") |
||||||
|
self.formLayout = QtWidgets.QFormLayout(self.groupBox_3) |
||||||
|
self.formLayout.setObjectName("formLayout") |
||||||
|
self.label_2 = QtWidgets.QLabel(self.groupBox_3) |
||||||
|
self.label_2.setObjectName("label_2") |
||||||
|
self.formLayout.setWidget(0, QtWidgets.QFormLayout.LabelRole, self.label_2) |
||||||
|
self.number_of_centroids = QtWidgets.QSpinBox(self.groupBox_3) |
||||||
|
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed) |
||||||
|
sizePolicy.setHorizontalStretch(0) |
||||||
|
sizePolicy.setVerticalStretch(0) |
||||||
|
sizePolicy.setHeightForWidth(self.number_of_centroids.sizePolicy().hasHeightForWidth()) |
||||||
|
self.number_of_centroids.setSizePolicy(sizePolicy) |
||||||
|
self.number_of_centroids.setMinimumSize(QtCore.QSize(50, 26)) |
||||||
|
self.number_of_centroids.setMaximumSize(QtCore.QSize(50, 16777215)) |
||||||
|
self.number_of_centroids.setObjectName("number_of_centroids") |
||||||
|
self.formLayout.setWidget(0, QtWidgets.QFormLayout.FieldRole, self.number_of_centroids) |
||||||
|
self.choose_centroids_button = QtWidgets.QPushButton(self.groupBox_3) |
||||||
|
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.reset_button = QtWidgets.QPushButton(self.groupBox_3) |
||||||
|
self.reset_button.setObjectName("reset_button") |
||||||
|
self.formLayout.setWidget(6, QtWidgets.QFormLayout.LabelRole, self.reset_button) |
||||||
|
self.verticalLayout.addWidget(self.groupBox_3) |
||||||
|
spacerItem = QtWidgets.QSpacerItem(20, 20, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Fixed) |
||||||
|
self.verticalLayout.addItem(spacerItem) |
||||||
|
self.groupBox_2 = QtWidgets.QGroupBox(self.centralwidget) |
||||||
|
self.groupBox_2.setObjectName("groupBox_2") |
||||||
|
self.gridLayout_2 = QtWidgets.QGridLayout(self.groupBox_2) |
||||||
|
self.gridLayout_2.setObjectName("gridLayout_2") |
||||||
|
self.mouse_position_label = QtWidgets.QLabel(self.groupBox_2) |
||||||
|
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Preferred) |
||||||
|
sizePolicy.setHorizontalStretch(0) |
||||||
|
sizePolicy.setVerticalStretch(0) |
||||||
|
sizePolicy.setHeightForWidth(self.mouse_position_label.sizePolicy().hasHeightForWidth()) |
||||||
|
self.mouse_position_label.setSizePolicy(sizePolicy) |
||||||
|
self.mouse_position_label.setMinimumSize(QtCore.QSize(100, 0)) |
||||||
|
self.mouse_position_label.setText("") |
||||||
|
self.mouse_position_label.setObjectName("mouse_position_label") |
||||||
|
self.gridLayout_2.addWidget(self.mouse_position_label, 0, 3, 1, 1) |
||||||
|
spacerItem1 = QtWidgets.QSpacerItem(20, 20, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding) |
||||||
|
self.gridLayout_2.addItem(spacerItem1, 1, 0, 1, 1) |
||||||
|
self.label = QtWidgets.QLabel(self.groupBox_2) |
||||||
|
self.label.setObjectName("label") |
||||||
|
self.gridLayout_2.addWidget(self.label, 0, 0, 1, 1) |
||||||
|
spacerItem2 = QtWidgets.QSpacerItem(20, 20, QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Minimum) |
||||||
|
self.gridLayout_2.addItem(spacerItem2, 0, 2, 1, 1) |
||||||
|
self.verticalLayout.addWidget(self.groupBox_2) |
||||||
|
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.setNativeMenuBar(True) |
||||||
|
self.menubar.setObjectName("menubar") |
||||||
|
self.menu_file = QtWidgets.QMenu(self.menubar) |
||||||
|
self.menu_file.setObjectName("menu_file") |
||||||
|
self.menu_help = QtWidgets.QMenu(self.menubar) |
||||||
|
self.menu_help.setObjectName("menu_help") |
||||||
|
MainWindow.setMenuBar(self.menubar) |
||||||
|
self.status_bar = QtWidgets.QStatusBar(MainWindow) |
||||||
|
self.status_bar.setObjectName("status_bar") |
||||||
|
MainWindow.setStatusBar(self.status_bar) |
||||||
|
self.tool_bar = QtWidgets.QToolBar(MainWindow) |
||||||
|
self.tool_bar.setMovable(False) |
||||||
|
self.tool_bar.setObjectName("tool_bar") |
||||||
|
MainWindow.addToolBar(QtCore.Qt.LeftToolBarArea, self.tool_bar) |
||||||
|
self.action_add_points = QtWidgets.QAction(MainWindow) |
||||||
|
self.action_add_points.setObjectName("action_add_points") |
||||||
|
self.action_edit_points = QtWidgets.QAction(MainWindow) |
||||||
|
self.action_edit_points.setObjectName("action_edit_points") |
||||||
|
self.action_delete_points = QtWidgets.QAction(MainWindow) |
||||||
|
self.action_delete_points.setObjectName("action_delete_points") |
||||||
|
self.action_solve = QtWidgets.QAction(MainWindow) |
||||||
|
self.action_solve.setObjectName("action_solve") |
||||||
|
self.action_move_points = QtWidgets.QAction(MainWindow) |
||||||
|
self.action_move_points.setObjectName("action_move_points") |
||||||
|
self.action_save_point_configuration = QtWidgets.QAction(MainWindow) |
||||||
|
self.action_save_point_configuration.setObjectName("action_save_point_configuration") |
||||||
|
self.action_load_point_configuration = QtWidgets.QAction(MainWindow) |
||||||
|
self.action_load_point_configuration.setObjectName("action_load_point_configuration") |
||||||
|
self.action_exit = QtWidgets.QAction(MainWindow) |
||||||
|
self.action_exit.setObjectName("action_exit") |
||||||
|
self.action_generate_random_points = QtWidgets.QAction(MainWindow) |
||||||
|
self.action_generate_random_points.setObjectName("action_generate_random_points") |
||||||
|
self.menu_file.addAction(self.action_load_point_configuration) |
||||||
|
self.menu_file.addAction(self.action_save_point_configuration) |
||||||
|
self.menu_file.addSeparator() |
||||||
|
self.menu_file.addAction(self.action_exit) |
||||||
|
self.menubar.addAction(self.menu_file.menuAction()) |
||||||
|
self.menubar.addAction(self.menu_help.menuAction()) |
||||||
|
self.tool_bar.addAction(self.action_generate_random_points) |
||||||
|
self.tool_bar.addAction(self.action_add_points) |
||||||
|
self.tool_bar.addAction(self.action_move_points) |
||||||
|
self.tool_bar.addAction(self.action_edit_points) |
||||||
|
self.tool_bar.addAction(self.action_delete_points) |
||||||
|
|
||||||
|
self.retranslateUi(MainWindow) |
||||||
|
QtCore.QMetaObject.connectSlotsByName(MainWindow) |
||||||
|
|
||||||
|
def retranslateUi(self, MainWindow): |
||||||
|
_translate = QtCore.QCoreApplication.translate |
||||||
|
MainWindow.setWindowTitle(_translate("MainWindow", "ClusterView")) |
||||||
|
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.reset_button.setText(_translate("MainWindow", "Reset")) |
||||||
|
self.groupBox_2.setTitle(_translate("MainWindow", "Canvas Information")) |
||||||
|
self.label.setText(_translate("MainWindow", "Mouse Position:")) |
||||||
|
self.menu_file.setTitle(_translate("MainWindow", "File")) |
||||||
|
self.menu_help.setTitle(_translate("MainWindow", "Help")) |
||||||
|
self.tool_bar.setWindowTitle(_translate("MainWindow", "toolBar")) |
||||||
|
self.action_add_points.setText(_translate("MainWindow", "Add Points")) |
||||||
|
self.action_add_points.setToolTip(_translate("MainWindow", "Enables point adding mode.")) |
||||||
|
self.action_add_points.setShortcut(_translate("MainWindow", "Ctrl+A")) |
||||||
|
self.action_edit_points.setText(_translate("MainWindow", "Edit Points")) |
||||||
|
self.action_edit_points.setToolTip(_translate("MainWindow", "Enables point editing mode.")) |
||||||
|
self.action_edit_points.setShortcut(_translate("MainWindow", "Ctrl+E")) |
||||||
|
self.action_delete_points.setText(_translate("MainWindow", "Delete Points")) |
||||||
|
self.action_delete_points.setToolTip(_translate("MainWindow", "Enables point deletion mode.")) |
||||||
|
self.action_delete_points.setShortcut(_translate("MainWindow", "Ctrl+D")) |
||||||
|
self.action_solve.setText(_translate("MainWindow", "Solve")) |
||||||
|
self.action_solve.setToolTip(_translate("MainWindow", "Opens the solve dialog to choose a solving solution.")) |
||||||
|
self.action_solve.setShortcut(_translate("MainWindow", "Ctrl+S")) |
||||||
|
self.action_move_points.setText(_translate("MainWindow", "Move Points")) |
||||||
|
self.action_move_points.setToolTip(_translate("MainWindow", "Enables the movement of a selection of points.")) |
||||||
|
self.action_save_point_configuration.setText(_translate("MainWindow", "Save Point Configuration")) |
||||||
|
self.action_load_point_configuration.setText(_translate("MainWindow", "Load Point Configuration")) |
||||||
|
self.action_exit.setText(_translate("MainWindow", "Exit")) |
||||||
|
self.action_generate_random_points.setText(_translate("MainWindow", "Generate Random Points")) |
@ -0,0 +1,259 @@ |
|||||||
|
from functools import partial |
||||||
|
|
||||||
|
from PyQt5.QtCore import Qt |
||||||
|
from PyQt5.QtGui import QCursor |
||||||
|
from PyQt5.QtWidgets import QFileDialog, QInputDialog, QMainWindow |
||||||
|
|
||||||
|
from clusterview2.exceptions import handle_exceptions |
||||||
|
from clusterview2.colors import Color |
||||||
|
from clusterview2.mode import Mode |
||||||
|
from clusterview2.ui.mode_handlers import (MODE_HANDLER_MAP, |
||||||
|
ogl_keypress_handler, |
||||||
|
refresh_point_list, |
||||||
|
reset_centroid_count_and_colors, |
||||||
|
generate_random_points) |
||||||
|
from clusterview2.ui.opengl_widget import (clear_selection, initialize_gl, |
||||||
|
mouse_leave, paint_gl, resize_gl, |
||||||
|
set_drawing_context) |
||||||
|
from clusterview2.point_manager import PointManager |
||||||
|
from clusterview2.ui.point_list_widget import item_click_handler |
||||||
|
from clusterview2_ui import Ui_MainWindow |
||||||
|
|
||||||
|
|
||||||
|
class MainWindow(QMainWindow, Ui_MainWindow): |
||||||
|
""" |
||||||
|
A wrapper class for handling creating a window based |
||||||
|
on the `clusterview_ui.py` code generated from |
||||||
|
`clusterview.ui`. |
||||||
|
""" |
||||||
|
|
||||||
|
# This is a static mode variable since there will only ever |
||||||
|
# be one MainWindow. |
||||||
|
__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 |
||||||
|
|
||||||
|
# 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 |
||||||
|
|
||||||
|
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 |
||||||
|
# the color for selection (Color.BLUE) and the default color for points |
||||||
|
# (Color.GREY). |
||||||
|
self.number_of_centroids.setMinimum(0) |
||||||
|
self.number_of_centroids.setMaximum(Color.count() - 2) |
||||||
|
|
||||||
|
# We only need to set the context in our OpenGL state machine |
||||||
|
# wrapper once here since the window is fixed size. |
||||||
|
# If we allow resizing of the window, the context must be updated |
||||||
|
# each resize so that coordinates are converted from screen (x, y) |
||||||
|
# to OpenGL coordinates properly. |
||||||
|
set_drawing_context(self) |
||||||
|
|
||||||
|
# Enables mouse tracking on the viewport so mouseMoveEvents are |
||||||
|
# tracked and fired properly. |
||||||
|
self.opengl_widget.setMouseTracking(True) |
||||||
|
|
||||||
|
# Enable keyboard input capture on the OpenGL Widget |
||||||
|
self.opengl_widget.setFocusPolicy(Qt.StrongFocus) |
||||||
|
|
||||||
|
# Here we partially apply the key press handler with self to |
||||||
|
# create a new function that only expects the event `keyPressEvent` |
||||||
|
# expects. In this way, we've snuck the state of the opengl_widget |
||||||
|
# into the function so that we can modify it as we please. |
||||||
|
self.opengl_widget.keyPressEvent = partial(ogl_keypress_handler, self) |
||||||
|
|
||||||
|
# Same story here but this time with the itemClicked event |
||||||
|
# so that when an element is clicked on in the point list it will |
||||||
|
# highlight. |
||||||
|
self.point_list_widget.itemClicked.connect(partial(item_click_handler, |
||||||
|
self)) |
||||||
|
|
||||||
|
self.choose_centroids_button.clicked.connect(self.__choose_centroids) |
||||||
|
|
||||||
|
self.group_button.clicked.connect(self.__group) |
||||||
|
|
||||||
|
self.reset_button.clicked.connect(self.__reset_grouping) |
||||||
|
|
||||||
|
# ----------------------------------------------- |
||||||
|
# OpenGL Graphics Handlers are set |
||||||
|
# here and defined in clusterview.opengl_widget. |
||||||
|
# ----------------------------------------------- |
||||||
|
self.opengl_widget.initializeGL = initialize_gl |
||||||
|
self.opengl_widget.paintGL = paint_gl |
||||||
|
self.opengl_widget.resizeGL = resize_gl |
||||||
|
self.opengl_widget.leaveEvent = partial(mouse_leave, self) |
||||||
|
|
||||||
|
# ------------------------------------- |
||||||
|
# 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_generate_random_points |
||||||
|
.triggered.connect(self.__generate_random_points)) |
||||||
|
|
||||||
|
self.action_save_point_configuration.triggered.connect( |
||||||
|
self.__save_points_file) |
||||||
|
|
||||||
|
self.action_load_point_configuration.triggered.connect( |
||||||
|
self.__open_points_file) |
||||||
|
|
||||||
|
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 |
||||||
|
|
||||||
|
# ----------------------------------------------------------------- |
||||||
|
# Mode changers - these will be used to signal the action in the |
||||||
|
# OpenGL Widget. |
||||||
|
# ----------------------------------------------------------------- |
||||||
|
def __off_mode(self): |
||||||
|
self.__mode = Mode.OFF |
||||||
|
self.opengl_widget.setCursor(QCursor(Qt.CursorShape.ArrowCursor)) |
||||||
|
self.status_bar.showMessage("") |
||||||
|
clear_selection() |
||||||
|
self.opengl_widget.update() |
||||||
|
|
||||||
|
def __add_points(self): |
||||||
|
self.__mode = Mode.ADD |
||||||
|
self.opengl_widget.setCursor(QCursor(Qt.CursorShape.CrossCursor)) |
||||||
|
self.status_bar.showMessage("ADD MODE") |
||||||
|
clear_selection() |
||||||
|
self.opengl_widget.update() |
||||||
|
|
||||||
|
def __edit_points(self): |
||||||
|
self.__mode = Mode.EDIT |
||||||
|
self.opengl_widget.setCursor(QCursor(Qt.CursorShape.CrossCursor)) |
||||||
|
self.status_bar.showMessage("EDIT MODE") |
||||||
|
clear_selection() |
||||||
|
self.opengl_widget.update() |
||||||
|
|
||||||
|
def __delete_points(self): |
||||||
|
self.__mode = Mode.DELETE |
||||||
|
self.opengl_widget.setCursor(QCursor( |
||||||
|
Qt.CursorShape.PointingHandCursor)) |
||||||
|
self.status_bar.showMessage("DELETE MODE") |
||||||
|
clear_selection() |
||||||
|
self.opengl_widget.update() |
||||||
|
|
||||||
|
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") |
||||||
|
clear_selection() |
||||||
|
self.opengl_widget.update() |
||||||
|
|
||||||
|
def __choose_centroids(self): |
||||||
|
self.__mode = Mode.CHOOSE_CENTROIDS |
||||||
|
self.opengl_widget.setCursor(QCursor(Qt.CursorShape.CrossCursor)) |
||||||
|
self.status_bar.showMessage("CHOOSE CENTROIDS") |
||||||
|
|
||||||
|
clear_selection() |
||||||
|
self.opengl_widget.update() |
||||||
|
|
||||||
|
def __group(self): |
||||||
|
self.__mode = Mode.GROUP |
||||||
|
|
||||||
|
self.opengl_widget.setCursor(QCursor(Qt.CursorShape.ArrowCursor)) |
||||||
|
self.status_bar.showMessage("GROUPING") |
||||||
|
clear_selection() |
||||||
|
group(self) |
||||||
|
self.__off_mode() |
||||||
|
self.opengl_widget.update() |
||||||
|
|
||||||
|
def __reset_grouping(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) |
||||||
|
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) |
||||||
|
|
||||||
|
if ok: |
||||||
|
self.__mode = Mode.ADD |
||||||
|
generate_random_points(value, |
||||||
|
(self.__viewport_width - self.__point_size), |
||||||
|
(self.__viewport_height - self.__point_size) |
||||||
|
) |
||||||
|
self.__mode = Mode.OFF |
||||||
|
|
||||||
|
self.opengl_widget.update() |
||||||
|
|
||||||
|
refresh_point_list(self) |
||||||
|
|
||||||
|
@property |
||||||
|
def mode(self): |
||||||
|
""" |
||||||
|
Function designed to be used from a context |
||||||
|
to get the current mode. |
||||||
|
""" |
||||||
|
return self.__mode |
||||||
|
|
||||||
|
@mode.setter |
||||||
|
def mode(self, mode): |
||||||
|
self.__mode = mode |
||||||
|
|
||||||
|
def __close_event(self, event): |
||||||
|
import sys |
||||||
|
sys.exit(0) |
||||||
|
|
||||||
|
def __open_points_file(self): |
||||||
|
ofile, _ = QFileDialog.getOpenFileName(self, |
||||||
|
"Open Point Configuration", |
||||||
|
"", |
||||||
|
"JSON files (*.json)") |
||||||
|
if ofile: |
||||||
|
self.__mode = Mode.LOADED |
||||||
|
|
||||||
|
PointManager.load(ofile) |
||||||
|
|
||||||
|
self.opengl_widget.update() |
||||||
|
|
||||||
|
refresh_point_list(self) |
||||||
|
|
||||||
|
def __save_points_file(self): |
||||||
|
file_name, _ = (QFileDialog. |
||||||
|
getSaveFileName(self, |
||||||
|
"Save Point Configuration", |
||||||
|
"", |
||||||
|
"JSON Files (*.json)")) |
||||||
|
if file_name: |
||||||
|
PointManager.save(file_name) |
||||||
|
|
||||||
|
@handle_exceptions |
||||||
|
def __ogl_click_dispatcher(self, event): |
||||||
|
""" |
||||||
|
Mode dispatcher for click actions on the OpenGL widget. |
||||||
|
""" |
||||||
|
# Map from Mode -> function |
||||||
|
# where the function is a handler for the |
||||||
|
# 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) |
@ -0,0 +1,8 @@ |
|||||||
|
-r requirements.txt |
||||||
|
|
||||||
|
flake8==3.7.8 |
||||||
|
mypy==0.730 |
||||||
|
coverage==4.5.4 |
||||||
|
pytest==5.0.1 |
||||||
|
pytest-cov==2.7.1 |
||||||
|
ipython==7.7.0 |
Loading…
Reference in new issue