Blog


Quadgrid Generation Using Recursive SQL Function

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.

quadgrid

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.
sydney_skyline

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
  1. ---------------------------------------------------------------------------
  2. -- Code Desciption:
  3. ---------------------------------------------------------------------------
  4. -- 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
  5. -- Dependencies: DE_MakeSquare(), DE_MakeRegularQuadCells()
  6. -- Developed by: mark[a]dimensionaledge[dot]com
  7. -- Licence: GNU GPL version 3.0
  8. ---------------------------------------------------------------------------
  9.  
  10. DROP FUNCTION IF EXISTS DE_RegularQuadGrid(geometry, text, text, integer, double precision);
  11. CREATE OR REPLACE FUNCTION DE_RegularQuadGrid(parent_geom geometry, reference_table text, reference_geom_col text, max_depth integer, threshold_value double precision)
  12. RETURNS TABLE (depth integer, the_geom GEOMETRY, cell_value double precision)
  13. AS $$
  14. DECLARE
  15. reference_geom_type text;
  16.  
  17. BEGIN
  18. EXECUTE 'SELECT GeometryType('|| reference_geom_col ||') FROM '|| reference_table ||' LIMIT 1' INTO reference_geom_type ;
  19.  
  20. IF reference_geom_type NOT IN ('POINT')
  21. THEN
  22. RAISE EXCEPTION 'Reference table is not a valid geometry type';
  23. ELSE
  24. END IF;
  25.  
  26. RETURN QUERY EXECUTE
  27. 'WITH RECURSIVE quadcells (depth, the_geom, cell_value) AS (
  28. --SEED THE PARENT GEOMETRY AND CELL VALUE
  29. SELECT 1, l.the_geom, r.pcount
  30. FROM (SELECT ST_GeomFromEWKT(ST_AsEWKT('|| quote_literal(CAST(parent_geom as text)) ||')) as the_geom) l,
  31. LATERAL
  32. (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
  33. --RECURSIVE PART
  34. UNION ALL
  35. SELECT t.depth, t.the_geom, t.pcount
  36. FROM
  37. --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
  38. (
  39. WITH a AS (SELECT * FROM quadcells WHERE the_geom IS NOT NULL AND depth < '|| max_depth ||' AND cell_value > '|| threshold_value ||'),
  40. b AS (SELECT max(depth) as previous FROM a),
  41. c AS (SELECT a.* FROM a,b WHERE a.depth = b.previous),
  42. 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)
  43. SELECT b.previous+1 as depth, d.the_geom, d.pcount FROM b, d
  44. ) t
  45. )
  46. 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||'))' ;
  47.  
  48. END;
  49. $$ LANGUAGE 'plpgsql' VOLATILE;
  50.  
DE_MakeRegularQuadCells.sql
  1. ---------------------------------------------------------------------------
  2. -- Code Desciption:
  3. ---------------------------------------------------------------------------
  4. -- PostgreSQL/PostGIS custom function for subdividing a square polygon into four child polygons
  5. -- Dependencies: DE_MakeSquare()
  6. -- Developed by: mark[at]dimensionaledge[dot]com
  7. -- Licence: GNU GPL version 3.0
  8. ---------------------------------------------------------------------------
  9. -- Usage Example:
  10. ---------------------------------------------------------------------------
  11. -- SELECT DE_MakeRegularQuadCells(DE_MakeSquare(ST_MakePoint(0,0),1));
  12. ---------------------------------------------------------------------------
  13.  
  14. CREATE OR REPLACE FUNCTION DE_MakeRegularQuadCells(parent GEOMETRY)
  15. RETURNS SETOF GEOMETRY
  16. AS $$
  17. DECLARE
  18. halfside float8;
  19. i INTEGER DEFAULT 1;
  20. srid INTEGER;
  21. centerpoint GEOMETRY;
  22. centersquare GEOMETRY;
  23. quadcell GEOMETRY;
  24.  
  25. BEGIN
  26. srid := ST_SRID(parent);
  27. centerpoint := ST_Centroid(parent);
  28. halfside := abs(ST_Xmax(parent) - ST_Xmin(parent))/2;
  29. centersquare := ST_ExteriorRing(DE_MakeSquare(centerpoint, halfside));
  30.  
  31. WHILE i < 5 LOOP
  32. quadcell := DE_MakeSquare(ST_PointN(centersquare, i), halfside);
  33. RETURN NEXT quadcell;
  34. i := i + 1;
  35. END LOOP;
  36.  
  37. RETURN;
  38. END
  39. $$ LANGUAGE 'plpgsql' IMMUTABLE;
  40.  
DE_MakeSquare.sql
  1. ---------------------------------------------------------------------------
  2. -- Code Desciption:
  3. ---------------------------------------------------------------------------
  4. -- PostgreSQL/PostGIS custom function for generating a square polygon of a specified size
  5. -- Dependencies: nil
  6. -- Developed by: mark[at]dimensionaledge[dot]com
  7. -- Licence: GNU GPL version 3.0
  8. ---------------------------------------------------------------------------
  9. -- Usage Example:
  10. ---------------------------------------------------------------------------
  11. -- SELECT DE_MakeSquare(ST_MakePoint(0,0),1);
  12. ---------------------------------------------------------------------------
  13.  
  14. CREATE OR REPLACE FUNCTION DE_MakeSquare(centerpoint GEOMETRY, side FLOAT8)
  15. RETURNS GEOMETRY
  16. AS $$
  17. SELECT ST_SetSRID(ST_MakePolygon(ST_MakeLine(
  18. ARRAY[
  19. st_makepoint(ST_X(centerpoint)-0.5*side, ST_Y(centerpoint)+0.5*side),
  20. st_makepoint(ST_X(centerpoint)+0.5*side, ST_Y(centerpoint)+0.5*side),
  21. st_makepoint(ST_X(centerpoint)+0.5*side, ST_Y(centerpoint)-0.5*side),
  22. st_makepoint(ST_X(centerpoint)-0.5*side, ST_Y(centerpoint)-0.5*side),
  23. st_makepoint(ST_X(centerpoint)-0.5*side, ST_Y(centerpoint)+0.5*side)
  24. ]
  25. )),ST_SRID(centerpoint));
  26. $$ LANGUAGE 'sql' IMMUTABLE STRICT;
  27.