For something a little different, here is a PostGIS recursive SQL quadgrid function which has been in my toolbox for some time now. The inspiration came in 2010 when reading “Open Source GIS: A Grass GIS Approach” by Markus Neteler and Helena Mitasova (3rd edition, p.244). The quadgrid function works by recursively subdividing tiles into smaller tiles (quadcells) up to a maximum depth if say the number of intersecting points (or some other feature criteria) exceeds a certain threshold.
Quadgrids have many applications, but I’ve found them useful for mapping urban phenomena (such as population density), both in 2D and 3D. Since large cities tend to be packed with more people, the result is more quadcells packed into a given land area. Quadgrids are also computationally efficient, as a finer grid cell resolution is only used in the more populous areas.
Below is an example of how I’ve used a population density quadgrid to create a 3D city skyline of Sydney. The taller the quadcells, the greater the number of people per square meter – no different to the actual built form of our cities where, if land is scarce, we build skyward.
The recursive quadgrid function is available on github. It requires PostgreSQL9.3 or above.
https://github.com/dimensionaledge/cf_public/blob/master/lattices/DE_RegularQuadGrid.sql
The function is called in the same way as any other pl/pgsql function. For instance:
CREATE TABLE quadgrid AS
SELECT depth::integer, the_geom::geometry(Polygon, 3577) as wkb_geometry, cell_value::float8
FROM DE_RegularQuadGrid((SELECT wkb_geometry FROM abs_aus11_tiles_32k WHERE tid IN (17864)), ‘tutorials.abs_mb11_points’,’wkb_geometry’, 10, 1000);
The arguments of the quadgrid function are: the parent tile geometry, the name of the points feature table along with its geometry column name, the maximum depth of recursion, and the threshold number of points per cell above which a cell will sub-divide.
The function uses two helper functions, namely DE_MakeRegularQuadCells() and DE_MakeSquare(). They work in tandem by taking a square geometry and subdividing it into four child geometries.
https://github.com/dimensionaledge/cf_public/tree/master/shapes
I’ve also just refactored the quadgrid function to incorporate a new, highly efficient LATERAL ‘Point in Polygon’ code pattern which avoids the use of GROUP BY when counting points in polygons. The result is a 75% reduction in query times compared to the conventional CROSS JOIN and GROUP BY approach.
SELECT r.the_geom, r.pcount FROM
(SELECT DE_MakeRegularQuadCells(wkb_geometry) as the_geom FROM abs_aus11_tiles_32k WHERE tid IN (17864)) l,
LATERAL
(SELECT l.the_geom, count(*) as pcount FROM tutorials.abs_mb11_points WHERE ST_Intersects(l.the_geom, wkb_geometry) AND l.the_geom && wkb_geometry) r;
Quadgrid run times depend on the number of underlying point features, and the depth of recursion. In the above GIF animation, there are 1.86 million points alone that intersect with the parent tile, making it one of the most populous areas in Australia. For this exercise, the time to ‘quadgrid’ this one tile took 19.9 seconds to a depth of 10. Less populated tiles tend to take only fractions of a second.
DE_MakeRegularQuadGrid.sql
--------------------------------------------------------------------------- -- Code Desciption: --------------------------------------------------------------------------- -- PostgreSQL/PostGIS custom function for generating quadcells recursively from a given starting geometry and intersecting reference table, to a maximum number of iteration levels or threshold value per cell -- Dependencies: DE_MakeSquare(), DE_MakeRegularQuadCells() -- Developed by: mark[a]dimensionaledge[dot]com -- Licence: GNU GPL version 3.0 --------------------------------------------------------------------------- DROP FUNCTION IF EXISTS DE_RegularQuadGrid(geometry, text, text, integer, double precision); CREATE OR REPLACE FUNCTION DE_RegularQuadGrid(parent_geom geometry, reference_table text, reference_geom_col text, max_depth integer, threshold_value double precision) RETURNS TABLE (depth integer, the_geom GEOMETRY, cell_value double precision) AS $$ DECLARE reference_geom_type text; BEGIN EXECUTE 'SELECT GeometryType('|| reference_geom_col ||') FROM '|| reference_table ||' LIMIT 1' INTO reference_geom_type ; IF reference_geom_type NOT IN ('POINT') THEN RAISE EXCEPTION 'Reference table is not a valid geometry type'; ELSE END IF; RETURN QUERY EXECUTE 'WITH RECURSIVE quadcells (depth, the_geom, cell_value) AS ( --SEED THE PARENT GEOMETRY AND CELL VALUE SELECT 1, l.the_geom, r.pcount FROM (SELECT ST_GeomFromEWKT(ST_AsEWKT('|| quote_literal(CAST(parent_geom as text)) ||')) as the_geom) l, LATERAL (SELECT count(*) as pcount, l.the_geom FROM '|| reference_table ||' WHERE ST_Intersects(l.the_geom, '|| reference_geom_col ||') AND l.the_geom && '|| reference_geom_col ||') r --RECURSIVE PART UNION ALL SELECT t.depth, t.the_geom, t.pcount FROM --TERMINAL CONDITION SUBQUERY LOOPS UNTIL THE CONDITIONS ARE NO LONGER MET - NOTE THE RECURSIVE ELEMENT CAN ONLY BE EXPLICITYLY REFERRED TO ONCE, HENCE THE USE OF CTE ( WITH a AS (SELECT * FROM quadcells WHERE the_geom IS NOT NULL AND depth < '|| max_depth ||' AND cell_value > '|| threshold_value ||'), b AS (SELECT max(depth) as previous FROM a), c AS (SELECT a.* FROM a,b WHERE a.depth = b.previous), d AS (SELECT r.the_geom, r.pcount FROM (SELECT DE_MakeRegularQuadCells(the_geom) as the_geom FROM c) l, LATERAL (SELECT count(*) as pcount, l.the_geom FROM '|| reference_table ||' WHERE ST_Intersects(l.the_geom, '|| reference_geom_col ||') AND l.the_geom && '|| reference_geom_col ||') r) SELECT b.previous+1 as depth, d.the_geom, d.pcount FROM b, d ) t ) SELECT depth, the_geom, cell_value::float8 FROM quadcells WHERE ST_IsEmpty(the_geom)=false AND (cell_value <= '|| threshold_value ||' OR (cell_value > '|| threshold_value ||' AND depth = '|| max_depth||'))' ; END; $$ LANGUAGE 'plpgsql' VOLATILE;
DE_MakeRegularQuadCells.sql
--------------------------------------------------------------------------- -- Code Desciption: --------------------------------------------------------------------------- -- PostgreSQL/PostGIS custom function for subdividing a square polygon into four child polygons -- Dependencies: DE_MakeSquare() -- Developed by: mark[at]dimensionaledge[dot]com -- Licence: GNU GPL version 3.0 --------------------------------------------------------------------------- -- Usage Example: --------------------------------------------------------------------------- -- SELECT DE_MakeRegularQuadCells(DE_MakeSquare(ST_MakePoint(0,0),1)); --------------------------------------------------------------------------- CREATE OR REPLACE FUNCTION DE_MakeRegularQuadCells(parent GEOMETRY) RETURNS SETOF GEOMETRY AS $$ DECLARE halfside float8; i INTEGER DEFAULT 1; srid INTEGER; centerpoint GEOMETRY; centersquare GEOMETRY; quadcell GEOMETRY; BEGIN srid := ST_SRID(parent); centerpoint := ST_Centroid(parent); halfside := abs(ST_Xmax(parent) - ST_Xmin(parent))/2; centersquare := ST_ExteriorRing(DE_MakeSquare(centerpoint, halfside)); WHILE i < 5 LOOP quadcell := DE_MakeSquare(ST_PointN(centersquare, i), halfside); RETURN NEXT quadcell; i := i + 1; END LOOP; RETURN; END $$ LANGUAGE 'plpgsql' IMMUTABLE;
DE_MakeSquare.sql
--------------------------------------------------------------------------- -- Code Desciption: --------------------------------------------------------------------------- -- PostgreSQL/PostGIS custom function for generating a square polygon of a specified size -- Dependencies: nil -- Developed by: mark[at]dimensionaledge[dot]com -- Licence: GNU GPL version 3.0 --------------------------------------------------------------------------- -- Usage Example: --------------------------------------------------------------------------- -- SELECT DE_MakeSquare(ST_MakePoint(0,0),1); --------------------------------------------------------------------------- CREATE OR REPLACE FUNCTION DE_MakeSquare(centerpoint GEOMETRY, side FLOAT8) RETURNS GEOMETRY AS $$ SELECT ST_SetSRID(ST_MakePolygon(ST_MakeLine( ARRAY[ st_makepoint(ST_X(centerpoint)-0.5*side, ST_Y(centerpoint)+0.5*side), st_makepoint(ST_X(centerpoint)+0.5*side, ST_Y(centerpoint)+0.5*side), st_makepoint(ST_X(centerpoint)+0.5*side, ST_Y(centerpoint)-0.5*side), st_makepoint(ST_X(centerpoint)-0.5*side, ST_Y(centerpoint)-0.5*side), st_makepoint(ST_X(centerpoint)-0.5*side, ST_Y(centerpoint)+0.5*side) ] )),ST_SRID(centerpoint)); $$ LANGUAGE 'sql' IMMUTABLE STRICT;