MultiVectorLayer in single SQL query with PostGIS
nitrag opened this issue · 1 comments
nitrag commented
I wrote this up for our purposes but perhaps you can incorporate it in a cleaner way than I could:
@dataclass
class VectorLayerQueryset:
layer_name: str
queryset: QuerySet
queryset_limit = None
fields: tuple = None
geom_name: str = 'geom'
class MultiLayerVectorTile:
"""
Base Mixin to handle vector tile generation
"""
vector_tile_content_type = "application/x-protobuf"
vector_layers: list[VectorLayerQueryset] = None
vector_tile_extent = 4096 # define tile extent
vector_tile_buffer = (
256 # define buffer around tiles (intersected polygon display without borders)
)
vector_tile_clip_geom = (
True # define if feature geometries should be clipped in tile
)
@classmethod
def get_bounds(cls, x, y, z):
return mercantile.xy_bounds(x, y, z)
def get_tile(self, x, y, z):
# get tile coordinates from x, y and z
xmin, ymin, xmax, ymax = self.get_bounds(x, y, z)
queries = []
params = []
for layer in self.vector_layers:
queryset = layer.queryset
# keep features intersecting tile
filters = {
# GeoFuncMixin implicitly transforms to SRID of geom
f"{layer.geom_name}__intersects": MakeEnvelope(
xmin, ymin, xmax, ymax, 3857
)
}
queryset = queryset.filter(**filters)
# annotate prepared geometry for MVT
queryset = queryset.annotate(
geom_prepared=AsMVTGeom(
Transform(layer.geom_name, 3857),
MakeEnvelope(xmin, ymin, xmax, ymax, 3857),
self.vector_tile_extent,
self.vector_tile_buffer,
self.vector_tile_clip_geom,
)
)
fields = (
layer.fields + ("geom_prepared",)
if layer.fields
else ("geom_prepared",)
)
# limit feature number if limit provided
limit = layer.queryset_limit
if limit:
queryset = queryset[:limit]
# keep values to include in tile (extra included_fields + geometry)
queryset = queryset.values(*fields)
# generate SQL/Params for layer
sql, p = queryset.query.sql_with_params()
queries.append(
f"SELECT ST_ASMVT(subquery.*, '{layer.layer_name}', {self.vector_tile_extent}, 'geom_prepared') as mvt "
f"FROM ({sql}) as subquery"
)
params = params + list(p)
with connection.cursor() as cursor:
cursor.execute(
"SELECT string_agg(mvt, '') as mvt FROM ({}) sub".format(' UNION '.join(queries)),
params=[*params]
)
row = cursor.fetchone()[0]
# psycopg2 returns memoryview, psycopg returns bytes
return row.tobytes() if type(row) == memoryview else row or b""
submarcos commented
Ok I undestand that you want to handle multiple layers in single SQL query instead of layer per layer. I will think about this as helper for this. Actually I prefer to make layer per layer because I manage my cache policy per layer. Thank you
Edit: I take this in consideration, maybe we could choose in future, in a view, if we want to make a single query or not