13

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?

Germán Carrillo
  • 36,307
  • 5
  • 123
  • 178
Pavel Pereverzev
  • 1,754
  • 12
  • 26

1 Answers1

20

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

Germán Carrillo
  • 36,307
  • 5
  • 123
  • 178
  • 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. – Pavel Pereverzev Sep 06 '21 at 19:56
  • 1
    Thank you very much for this detailed and very informative answer. – Kadir Şahbaz Sep 25 '21 at 21:37
  • 1
    You're welcome. I enjoy writing these kinds of posts. – Germán Carrillo Sep 26 '21 at 00:12
  • 1
    @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 :) – Pavel Pereverzev Sep 27 '21 at 14:48
  • We could simply add it to the existing answer ;). – Germán Carrillo Sep 27 '21 at 14:57
  • BTW @PavelPereverzev, if the answer I posted worked for you, don't forget to mark it as accepted and vote for it. That'll let other users know that it solved your question. – Germán Carrillo Sep 27 '21 at 14:58
  • @GermánCarrillo sorry, forgot about that. Marked just now – Pavel Pereverzev Sep 27 '21 at 15:18