12

I made a tool which runs a QgsMapToolEmitPoint object. Also set its canvasMoveEvent to create a polygon rubberband that takes a geometry from a feature that intersected with current tool's cursor position.

I.e., I move a pointer and each movement loops the whole layer until intersecting polygon is found. Then a rubberband with this polygon appears.

Here how it looks like:

enter image description here

But still I'm not satisfied with a speed of rubberband appearing. There is a small delay between pointer stop and polygon selection. A layer has about 160 features but to my opinion there should be a way to speed up getting an intersecting polygon.

I did a simple loop like this

for f in self.in_layer.getFeatures():
    if f.geometry().intersects(start_geom_point):
        f_selected = f 
        f_geometry = f.geometry()
        break

Also tried a processing.run("qgis:selectbylocation") but result was also not too good. As far as I see it is something about basic methods of selecting features from multiple layers with a geometry relations. Is there any way to increase the speed of finding a single intersecting feature here?

1
14

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):

enter image description here

8
  • 4
    Oh, that's the most impressive answer I ever got. Many thanks, I'll definitely test all thing you recommended! Especially for spatial index, just heard about them but didn't know how and where to use it. Sep 6 '21 at 19:56
  • Thank you very much for this detailed and very informative answer. Sep 25 '21 at 21:37
  • 1
    You're welcome. I enjoy writing these kinds of posts. Sep 26 '21 at 0:12
  • @Germán excellent explanation!! I follow carefully your publications, I would only like to complement the use of GEOS with: engine = QgsGeometry.createGeometryEngine(i.geometry().constGet()) engine.prepareGeometry() after filtering with the spatial index the geometries that meet the condition are then evaluated with engine.contains(c.geometry().constGet())
    – Luis Perez
    Sep 26 '21 at 21:26
  • @LuisPerez if you write a separate answer with a use of geometry engine, that would be very helpful and we make a final tip to make spatial selection as fast as possible :) Sep 27 '21 at 14:48

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.