Add type information to Quadtree

And some left over typing fixes. Use latest version of Mypy.
This commit is contained in:
Arjan Molenaar 2020-10-21 10:04:39 +02:00
parent 6d530df8ef
commit e0d742ba86
8 changed files with 59 additions and 55 deletions

View File

@ -5,7 +5,7 @@ repos:
- id: black
language_version: python3
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v0.782
rev: v0.790
hooks:
- id: mypy
- repo: https://gitlab.com/pycqa/flake8

View File

@ -1,4 +1,5 @@
"""Basic connectors such as Ports and Handles."""
from typing import Tuple, Union
from gaphas.constraint import LineConstraint, PositionConstraint
from gaphas.geometry import distance_line_point, distance_point_point
@ -28,13 +29,11 @@ class Handle:
self._movable = movable
self._visible = True
def _set_pos(self, pos: Position):
def _set_pos(self, pos: Union[Position, Tuple[float, float]]):
"""
Shortcut for ``handle.pos.pos = pos``
>>> h = Handle((10, 10))
>>> h.pos
<Position object on (10, 10)>
>>> h.pos = (20, 15)
>>> h.pos
<Position object on (20, 15)>

View File

@ -64,11 +64,12 @@ class PortoBox(Box):
# handle for movable port
self._hm = Handle(strength=WEAK)
self._hm.pos = width, height / 2.0
self._hm.pos.x = width
self._hm.pos.y = height / 2.0
self._handles.append(self._hm)
# movable port
self._ports.append(PointPort(self._hm.pos)) # type: ignore[arg-type]
self._ports.append(PointPort(self._hm.pos))
# keep movable port at right edge
self.constraint(vertical=(self._hm.pos, ne.pos), delta=10)

View File

@ -6,6 +6,7 @@ intersections).
A point is represented as a tuple `(x, y)`.
"""
from math import sqrt
from typing import Tuple
class Rectangle:
@ -80,6 +81,10 @@ class Rectangle:
self.width += delta * 2
self.height += delta * 2
def tuple(self) -> Tuple[float, float, float, float]:
"""A type safe version of `tuple(rectangle)`."""
return (self.x, self.y, self.width, self.height)
def __repr__(self):
"""
>>> Rectangle(5,7,20,25)

View File

@ -90,15 +90,15 @@ class Matrix:
def to_cairo(self):
return self._matrix
def __eq__(self, other) -> bool:
def __eq__(self, other):
if isinstance(other, Matrix):
return self._matrix == other._matrix # type: ignore[no-any-return]
return self._matrix == other._matrix
else:
return False
def __ne__(self, other) -> bool:
def __ne__(self, other):
if isinstance(other, Matrix):
return self._matrix != other._matrix # type: ignore[no-any-return]
return self._matrix != other._matrix
else:
return False

View File

@ -17,12 +17,20 @@ as a Q-tree. All forms of Quadtrees share some common features:
(From Wikipedia, the free encyclopedia)
"""
from __future__ import annotations
import operator
from typing import Callable, Dict, Generic, List, Optional, Tuple, TypeVar
from gaphas.geometry import rectangle_clip, rectangle_contains, rectangle_intersects
Bounds = Tuple[float, float, float, float]
class Quadtree:
T = TypeVar("T")
D = TypeVar("D")
class Quadtree(Generic[T, D]):
"""The Quad-tree.
Rectangles use the same scheme throughout Gaphas: (x, y, width, height).
@ -78,7 +86,7 @@ class Quadtree:
>>> qtree.rebuild()
"""
def __init__(self, bounds=(0, 0, 0, 0), capacity=10):
def __init__(self, bounds: Bounds = (0, 0, 0, 0), capacity=10):
"""Create a new Quadtree instance.
Bounds is the boundaries of the quadtree. this is fixed and do not
@ -87,14 +95,14 @@ class Quadtree:
Capacity defines the number of elements in one tree bucket (default: 10)
"""
self._capacity = capacity
self._bucket = QuadtreeBucket(bounds, capacity)
self._bucket: QuadtreeBucket[T] = QuadtreeBucket(bounds, capacity)
# Easy lookup item->(bounds, data, clipped bounds) mapping
self._ids = {}
self._ids: Dict[T, Tuple[Bounds, Optional[D], Bounds]] = {}
bounds = property(lambda s: s._bucket.bounds)
def resize(self, bounds):
def resize(self, bounds: Bounds) -> None:
"""Resize the tree.
The tree structure is rebuild.
@ -102,7 +110,8 @@ class Quadtree:
self._bucket = QuadtreeBucket(bounds, self._capacity)
self.rebuild()
def get_soft_bounds(self):
@property
def soft_bounds(self) -> Bounds:
"""Calculate the size of all items in the tree. This size may be beyond
the limits of the tree itself.
@ -121,29 +130,17 @@ class Quadtree:
>>> qtree.bounds
(0, 0, 0, 0)
"""
x_y_w_h = list(
zip( # type: ignore[call-overload]
*list(
map(
operator.getitem,
iter(list(self._ids.values())),
[0] * len(self._ids),
)
)
)
)
x_y_w_h = list(zip(*[d[0] for d in self._ids.values()]))
if not x_y_w_h:
return 0, 0, 0, 0
x0 = min(x_y_w_h[0])
y0 = min(x_y_w_h[1])
add = operator.add
x1 = max(list(map(add, x_y_w_h[0], x_y_w_h[2])))
y1 = max(list(map(add, x_y_w_h[1], x_y_w_h[3])))
return (x0, y0, x1 - x0, y1 - y0)
x1 = max(map(add, x_y_w_h[0], x_y_w_h[2]))
y1 = max(map(add, x_y_w_h[1], x_y_w_h[3]))
return x0, y0, x1 - x0, y1 - y0
soft_bounds = property(get_soft_bounds)
def add(self, item, bounds, data=None):
def add(self, item: T, bounds: Bounds, data: D = None):
"""Add an item to the tree.
If an item already exists, its bounds are updated and the item
@ -176,7 +173,7 @@ class Quadtree:
self._bucket.find_bucket(clipped_bounds).add(item, clipped_bounds)
self._ids[item] = (bounds, data, clipped_bounds)
def remove(self, item):
def remove(self, item: T):
"""Remove an item from the tree."""
bounds, data, clipped_bounds = self._ids[item]
del self._ids[item]
@ -199,15 +196,15 @@ class Quadtree:
self._bucket.find_bucket(clipped_bounds).add(item, clipped_bounds)
self._ids[item] = (bounds, data, clipped_bounds)
def get_bounds(self, item):
def get_bounds(self, item: T):
"""Return the bounding box for the given item."""
return self._ids[item][0]
def get_data(self, item):
def get_data(self, item: T):
"""Return the data for the given item, None if no data was provided."""
return self._ids[item][1]
def get_clipped_bounds(self, item):
def get_clipped_bounds(self, item: T):
"""Return the bounding box for the given item.
The bounding box is clipped on the boundaries of the tree
@ -215,14 +212,14 @@ class Quadtree:
"""
return self._ids[item][2]
def find_inside(self, rect):
def find_inside(self, rect: Bounds):
"""Find all items in the given rectangle (x, y, with, height).
Returns a set.
"""
return set(self._bucket.find(rect, method=rectangle_contains))
def find_intersect(self, rect):
def find_intersect(self, rect: Bounds):
"""Find all items that intersect with the given rectangle (x, y, width,
height).
@ -234,27 +231,27 @@ class Quadtree:
"""Return number of items in tree."""
return len(self._ids)
def __contains__(self, item):
def __contains__(self, item: T):
"""Check if an item is in tree."""
return item in self._ids
def dump(self):
def dump(self) -> None:
"""Print structure to stdout."""
self._bucket.dump()
class QuadtreeBucket:
class QuadtreeBucket(Generic[T]):
"""A node in a Quadtree structure."""
def __init__(self, bounds, capacity):
def __init__(self, bounds: Bounds, capacity: int):
"""Set bounding box for the node as (x, y, width, height)."""
self.bounds = bounds
self.capacity = capacity
self.items = {}
self._buckets = []
self.items: Dict[T, Bounds] = {}
self._buckets: List[QuadtreeBucket[T]] = []
def add(self, item, bounds):
def add(self, item: T, bounds: Bounds):
"""Add an item to the quadtree.
The bucket is split when necessary. Items are otherwise added to
@ -281,7 +278,7 @@ class QuadtreeBucket:
else:
self.items[item] = bounds
def remove(self, item):
def remove(self, item: T) -> None:
"""Remove an item from the quadtree bucket.
The item should be contained by *this* bucket (not a sub-
@ -289,7 +286,7 @@ class QuadtreeBucket:
"""
del self.items[item]
def update(self, item, new_bounds):
def update(self, item: T, new_bounds: Bounds) -> None:
"""Update the position of an item within the current bucket.
The item should live in the current bucket, but may be placed in
@ -299,7 +296,7 @@ class QuadtreeBucket:
self.remove(item)
self.find_bucket(new_bounds).add(item, new_bounds)
def find_bucket(self, bounds):
def find_bucket(self, bounds: Bounds):
"""Find the bucket that holds a bounding box.
This method should be used to find a bucket that fits, before
@ -322,7 +319,7 @@ class QuadtreeBucket:
return self
return self._buckets[index].find_bucket(bounds)
def find(self, rect, method):
def find(self, rect: Bounds, method: Callable[[Bounds, Bounds], bool]):
"""Find all items in the given rectangle (x, y, with, height). Method
can be either the contains or intersects function.

View File

@ -1,10 +1,13 @@
"""This module contains everything to display a Canvas on a screen."""
from typing import Tuple
import cairo
from gi.repository import Gdk, GLib, GObject, Gtk
from gaphas.canvas import Context, instant_cairo_context
from gaphas.decorators import AsyncIO
from gaphas.geometry import Rectangle, distance_point_point_fast
from gaphas.item import Item
from gaphas.matrix import Matrix
from gaphas.painter import BoundingBoxPainter, DefaultPainter, ItemPainter
from gaphas.quadtree import Quadtree
@ -33,7 +36,7 @@ class View:
self._hovered_item = None
self._dropzone_item = None
self._qtree = Quadtree()
self._qtree: Quadtree[Item, Tuple[float, float, float, float]] = Quadtree()
self._bounds = Rectangle(0, 0, 0, 0)
self._canvas = None
@ -667,7 +670,7 @@ class GtkView(Gtk.DrawingArea, Gtk.Scrollable, View):
x0, y0 = i2v(bounds[0], bounds[1])
x1, y1 = i2v(bounds[2], bounds[3])
vbounds = Rectangle(x0, y0, x1=x1, y1=y1)
self._qtree.add(i, vbounds, bounds)
self._qtree.add(i, vbounds.tuple(), bounds)
self.update_bounding_box(set(dirty_items))

View File

@ -1,15 +1,14 @@
import pytest
from gaphas.geometry import Rectangle
from gaphas.quadtree import Quadtree
@pytest.fixture()
def qtree():
qtree = Quadtree((0, 0, 100, 100))
qtree: Quadtree[str, None] = Quadtree((0, 0, 100, 100))
for i in range(0, 100, 10):
for j in range(0, 100, 10):
qtree.add(item=f"{i:d}x{j:d}", bounds=Rectangle(i, j, 10, 10))
qtree.add(item=f"{i:d}x{j:d}", bounds=(i, j, 10, 10))
return qtree