Nice question!
You can apply several optimizations, like these 5:
1. Remove any print()
statement.
Print statements are nice for debugging (although there are better options, see this answer). However, in production you shouldn't have any print statements because they could degrade performance when placed inside certain methods or loops.
2. Optimize getFeatures()
call.
Since you are not interested in attributes, you should discard them. That is, use a request like this when getting all features from your layer (more details in this answer):
layer.getFeatures(QgsFeatureRequest().setNoAttributes())
3. Cache the getFeatures()
response.
Chances are you don't need to call getFeatures()
more than once in your code. Therefore, just call it once and traverse a member list of features or a member dict
of (id, geometry) pairs against which you can perform the intersects analysis. For example:
def __init__(self):
request = QgsFeatureRequest().setNoAttributes()
self.polygons = list(layer.getFeatures(request))
def canvasMoveEvent(self):
for polygon in self.polygons:
# Now test intersections
4. Use a Spatial Index.
Testing intersections against polygons with a large number of vertices could be improved by first testing against their bounding boxes, which will help you discard a lot polygons that are simply too far away.
What is left is a set of "candidate" polygons (or ids), against which you can now test real intersections, that is, intersections against the real (possibly complex) geometries.
For that you need to:
a) Build the spatial index for your polygon layer.
b) Build a dict
of (id, geometry) pairs.
c) Get your cursor position in map coordinates.
d) Use the bounding box of c) to perform a quick check against the spatial index and get candidate ids.
e) Iterate candidate ids to test intersections (this time, real polygon geometry from b) against c)). Stop when you get your first positive result.
f) Pass the resulting geometry from e) to your rubber band.
Like this:
def __init__(self, e):
# ...
request = QgsFeatureRequest().setNoAttributes()
self._feature_map = {f.id():f.geometry() for f in self.layer.getFeatures(request)}
self._index = QgsSpatialIndex(self.layer)
# ...
def canvasMoveEvent(self, e):
# ...
pos = self.toMapCoordinates(e.pos())
point = QgsGeometry(QgsPoint(pos.x(), pos.y()))
pointbb = point.boundingBox()
candidates = self._index.intersects(pointbb)
for candidate in candidates:
if self._feature_map[candidate].intersects(point):
self._rb.reset()
self._rb.addGeometry(self._feature_map[candidate], self.layer)
# ...
5. Avoid unnecessary updates to the rubber band's geometry.
When you're moving the mouse, chances are you're resetting the rubber band's geometry to the same polygon a large number of times. You can easily avoid it by caching the latest id set and comparing to it just before setting a new geometry to the rubber band. Like this:
def __init__(self, iface):
# ...
self._current_id = None
def canvasMoveEvent(self, e):
# ...
for candidate in candidates:
if self._feature_map[candidate].intersects(point):
if candidate != self._current_id:
self._current_id = candidate
self._rb.reset()
self._rb.addGeometry(self._feature_map[candidate], self.layer)
return
self._rb.reset()
self._current_id = None
Applying all these recommendations, this is what you'll get using the World map (240 polygons):