Browse Source

Update point set to function off of bounding boxes and add delete feature

tb-init-ui-render
Taylor Bockman 5 years ago
parent
commit
b73a88d63f
  1. 1
      clusterview.py
  2. 38
      clusterview/mode_handlers.py
  3. 30
      clusterview/opengl_widget.py
  4. 174
      clusterview/points.py
  5. 59
      tests/test_point_set.py

1
clusterview.py

@ -9,7 +9,6 @@ def main():
app = QApplication(sys.argv) app = QApplication(sys.argv)
window = MainWindow() window = MainWindow()
window.show() window.show()
sys.exit(app.exec_()) sys.exit(app.exec_())

38
clusterview/mode_handlers.py

@ -2,9 +2,26 @@ from .mode import Mode
from .opengl_widget import set_current_points, set_drawing_event from .opengl_widget import set_current_points, set_drawing_event
from .points import PointSet from .points import PointSet
# Size of point for drawing
__POINT_SIZE = 8
# There are a lot of module-global variables being used because of the # There are a lot of module-global variables being used because of the
# nature of state management in OpenGL. # nature of state management in OpenGL.
__point_set = PointSet() __point_set = PointSet(__POINT_SIZE)
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 __point_set.points:
ctx.point_list_widget.addItem("({}, {})".format(p.x, p.y))
def __handle_add_point(ctx, event): def __handle_add_point(ctx, event):
@ -23,17 +40,11 @@ def __handle_add_point(ctx, event):
# No attribute at the moment. # No attribute at the moment.
__point_set.add_point(event.x(), event.y()) __point_set.add_point(event.x(), event.y())
__refresh_point_list(ctx)
set_drawing_event(event) set_drawing_event(event)
set_current_points(__point_set) set_current_points(__point_set)
# 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 __point_set.points:
ctx.point_list_widget.addItem("({}, {})".format(p[0], p[1]))
ctx.opengl_widget.update() ctx.opengl_widget.update()
ctx.point_list_widget.update() ctx.point_list_widget.update()
@ -66,8 +77,13 @@ def __handle_move_points(ctx, event):
def __handle_delete_point(ctx, event): def __handle_delete_point(ctx, event):
set_drawing_event(event) set_drawing_event(event)
ctx.update()
# Find the point from event and remove it from the list __point_set.remove_point(event.x(), event.y())
__refresh_point_list(ctx)
ctx.opengl_widget.update()
ctx.point_list_widget.update()
# Simple dispatcher to make it easy to dispatch the right mode # Simple dispatcher to make it easy to dispatch the right mode
# function when the OpenGL window is clicked. # function when the OpenGL window is clicked.

30
clusterview/opengl_widget.py

@ -30,9 +30,6 @@ COLOR_TO_RGBA = {
Color.BLUE: (0, 0.5, 1.0, 0.0) Color.BLUE: (0, 0.5, 1.0, 0.0)
} }
# Size of point for drawing
__POINT_SIZE = 8
# Constants set based on the size of the window. # Constants set based on the size of the window.
__BOTTOM_LEFT = (0, 0) __BOTTOM_LEFT = (0, 0)
__WIDTH = None __WIDTH = None
@ -163,7 +160,12 @@ def paint_gl():
raise InvalidStateError("Points must exist for ADD, EDIT, MOVE, " + raise InvalidStateError("Points must exist for ADD, EDIT, MOVE, " +
"and DELETE") "and DELETE")
if __current_mode is Mode.ADD: if __current_mode is Mode.ADD or __current_mode is Mode.DELETE:
# Note that drawing the points doesn't require a bounding box or
# any special context, so delete just removes the element from
# the point set, which will be redrawn here. This action
# is the same as adding a point since we just draw what is in
# the point set.
draw_points(__current_points, Color.BLUE) draw_points(__current_points, Color.BLUE)
elif __current_mode is Mode.EDIT: elif __current_mode is Mode.EDIT:
raise NotImplementedError("Drawing for EDIT not implemented.") raise NotImplementedError("Drawing for EDIT not implemented.")
@ -220,25 +222,15 @@ def draw_points(point_set, color):
ct = COLOR_TO_RGBA[color] ct = COLOR_TO_RGBA[color]
glViewport(0, 0, __WIDTH, __HEIGHT) glViewport(0, 0, __WIDTH, __HEIGHT)
glPointSize(__POINT_SIZE) glPointSize(__current_points.point_size)
glColor4f(ct[0], ct[1], ct[2], ct[3])
glBegin(GL_POINTS) glBegin(GL_POINTS)
glColor4f(ct[0], ct[1], ct[2], ct[3])
for point in point_set.points: for point in point_set.points:
glVertex3f(__clamp_x(point[0]), glVertex3f(__clamp_x(point.x),
__clamp_y(point[1]), __clamp_y(point.y),
0.0) # Z is currently fixed to 0 0.0) # Z is currently fixed to 0
glEnd() glEnd()
def delete_point(x, y):
"""
Deletes a point.
The list deletion happens in the clusterview.mode module. This
function just overwrites the point color with the background.
@param x The x-coordinate.
@param y The y-coordinate.
"""
raise NotImplementedError("delete_point not implemented.")

174
clusterview/points.py

@ -1,6 +1,101 @@
from math import floor
class Point:
"""
A class representing a point. A point
has a point_size bounding box around
it.
"""
def __init__(self, x, y, point_size):
"""
Initializes a new point with a point_size bounding box.
@param point_size The size of the point in pixels.
@param x The x-coordinate.
@param y The y-coordinate.
"""
self.__point_size = point_size
self.__x = x
self.__y = y
half_point = floor(point_size / 2)
self.__top_left_corner = (self.__x - half_point,
self.__y + half_point)
self.__bottom_right_corner = (self.__x + half_point,
self.__y - half_point)
@property
def x(self):
return self.__x
@property
def y(self):
return self.__y
@property
def point_size(self):
return self.__point_size
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.__point_size == other.point_size)
def __hash__(self):
"""
Overridden hashing function so it can be used as a dictionary key
for attributes.
"""
return hash((self.__x, self.__y, self.__point_size))
def __repr__(self):
return "POINT<X :{} Y: {} SIZE: {}>".format(self.__x,
self.__y,
self.__point_size)
def hit(self, x, y):
"""
Determines if the point was hit inside of it's bounding box.
The condition for hit is simple - consider the following
bounding box:
-------------
| |
| (x,y) |
| |
-------------
Where the clicked location is in the center. Then the top
left corner is defined as (x - half_point_size, y + half_point_size)
and the bottom corner is (x + half_point_size, y - half_point_size)
So long as x and y are greater than the top left and less than the
top right it is considered a hit.
This function is necessary for properly deleting and selecting points.
"""
return (x >= self.__top_left_corner[0] and
x <= self.__bottom_right_corner[0] and
y <= self.__top_left_corner[1] and
y >= self.__bottom_right_corner[1])
class Attribute: class Attribute:
__name = None
__value = None
def __init__(self, name, value): def __init__(self, name, value):
""" """
@ -12,11 +107,35 @@ class Attribute:
class PointSet: class PointSet:
""" """
Useful point set for storing coordinates and attributes. It is Useful container for points backed by a set to insure point
backed by a set to provide nice convenience functions. uniqueness.
"""
def __init__(self, point_size):
"""
Initializes a point container with points of size point_size.
@param point_size The size of the points.
"""
self.__points = set()
self.__attributes = {}
self.__point_size = point_size
@property
def points(self):
"""
Getter for points. Returns a generator for
looping.
""" """
__points = set() for point in self.__points:
__attributes = {} yield point
@property
def point_size(self):
return self.__point_size
def add_point(self, x, y, attrs=[]): def add_point(self, x, y, attrs=[]):
""" """
@ -32,34 +151,43 @@ class PointSet:
raise ValueError("Attributes in add_point must be an " + raise ValueError("Attributes in add_point must be an " +
"attribute array.") "attribute array.")
point = (x, y) point = Point(x, y, self.__point_size)
self.__points.add(point) self.__points.add(point)
self.__attributes[point] = attrs self.__attributes[point] = attrs
def remove_point(self, x, y): def remove_point(self, x, y):
""" """
Removes a point and it's attributes from the point set. Removes a point and it's attributes 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.
""" """
point = (x, y)
self.__points.discard(point) # Find points that match
matched = set(p for p in self.__points if p.hit(x, y))
# In place set difference
self.__points = self.__points - matched
# Remove associated attributes
for point in matched:
self.__attributes.pop(point) self.__attributes.pop(point)
def attributes(self, x, y): def attributes(self, point):
""" """
Returns the attribute array for a given point. Returns the attribute array for a given point.
@param x The x-coordinate of the point. @param point The point to get attributes for..
@param y The y-coordinate of the point.
""" """
return self.__attributes[(x, y)] return self.__attributes[point]
@property
def points(self):
"""
Getter for points. Returns a generator for
looping.
"""
for point in self.__points:
yield point

59
tests/test_point_set.py

@ -1,38 +1,43 @@
import pytest import pytest
from clusterview.points import Attribute, PointSet from clusterview.points import Attribute, Point, PointSet
def test_add_to_point_set(): def test_add_to_point_set():
l = PointSet() l = PointSet(3)
l.add_point(1, 2) l.add_point(1, 2)
points = list(l.points) points = list(l.points)
p = Point(1, 2, 3)
assert len(points) == 1 assert len(points) == 1
assert points[0] == (1, 2) assert points[0] == p
assert len(l.attributes(1, 2)) == 0 assert len(l.attributes(p)) == 0
def test_add_to_point_set_with_attributes(): def test_add_to_point_set_with_attributes():
attribute = Attribute("thing", 1) attribute = Attribute("thing", 1)
l = PointSet() l = PointSet(3)
l.add_point(1, 2, attrs=[attribute]) l.add_point(2, 3, attrs=[attribute])
points = list(l.points) points = list(l.points)
attrs = l.attributes(1, 2) point = Point(2, 3, 3)
attrs = l.attributes(point)
assert len(points) == 1 assert len(points) == 1
assert points[0] == (1, 2) assert points[0] == point
assert len(l.attributes(1, 2)) == 1 assert len(l.attributes(point)) == 1
def test_remove_point(): def test_remove_point_exact_click():
attribute = Attribute("thing", 1) attribute = Attribute("thing", 1)
l = PointSet() l = PointSet(8)
l.add_point(1, 2, attrs=[attribute]) l.add_point(1, 2, attrs=[attribute])
p = Point(1, 2, 8)
l.remove_point(1, 2) l.remove_point(1, 2)
points = list(l.points) points = list(l.points)
@ -44,9 +49,37 @@ def test_remove_point():
# point to raise a KeyError because it no # point to raise a KeyError because it no
# longer exists in the point -> attribute_list # longer exists in the point -> attribute_list
# dictionary. # dictionary.
l.attributes(1, 2) l.attributes(p)
def test_remove_point_bounding_box():
"""
This test checks the bounding box hit heuristic.
"""
attribute = Attribute("thing", 1)
l = PointSet(8)
l.add_point(1, 2, attrs=[attribute])
p = Point(1, 2, 8)
# The click-point (2, 1) will be inside of our point size 8
# bounding box.
l.remove_point(2, 1)
points = list(l.points)
assert len(points) == 0
with pytest.raises(KeyError):
# We expect a call to attributes on a removed
# point to raise a KeyError because it no
# longer exists in the point -> attribute_list
# dictionary.
l.attributes(p)
def test_attributes_must_be_array_of_attributes(): def test_attributes_must_be_array_of_attributes():
with pytest.raises(ValueError): with pytest.raises(ValueError):
l = PointSet() l = PointSet(8)
l.add_point(1, 2, attrs=[1,2,3,4,5]) l.add_point(1, 2, attrs=[1,2,3,4,5])

Loading…
Cancel
Save