ST_HexagonGrid — Returns a set of hexagons and cell indices that completely cover the bounds of the geometry argument.
setof record ST_HexagonGrid(float8 size, geometry bounds);
Starts with the concept of a hexagon tiling of the plane. (Not a hexagon tiling of the globe; for H3 use the h3-pg extension.) For a given planar SRS, and a given edge size, starting at the origin of the SRS, there is one unique hexagonal tiling of the plane, Tiling(SRS, Size). This function answers the question: what hexagons in a given Tiling(SRS, Size) overlap with a given bounds.
The SRS for the output hexagons is the SRS provided by the bounds geometry.
Doubling or tripling the edge size of the hexagon generates a new parent tiling that fits with the origin tiling. Unfortunately, it is not possible to generate parent hexagon tilings that the child tiles perfectly fit inside.
Availability: 3.1.0
Unit hexagons covering rectangular bounds.
WITH bounds AS (
SELECT ST_MakeEnvelope(2, 1, 4, 3, 3857) AS geom
), grid AS (
SELECT hex.*
FROM bounds
CROSS JOIN LATERAL ST_HexagonGrid(1, geom) AS hex
)
SELECT (SELECT geom FROM bounds) AS input_bounds,
ST_Collect(geom ORDER BY i, j) AS grid
FROM grid;
POLYGON((2 1,2 3,4 3,4 1,2 1)) | MULTIPOLYGON(((0.5 0.866,1 0,2 0,2.5 0.866,2 1.732,1 1.732,0.5 0.866)),((0.5 2.598,1 1.732,2 1.732,2.5 2.598,2 3.464,1 3.464,0.5 2.598)),((2 1.732,2.5 0.866,3.5 0.866,4 1.732,3.5 2.598,2.5 2.598,2 1.732)),((2 3.464,2.5 2.598,3.5 2.598,4 3.464,3.5 4.33,2.5 4.33,2 3.464)),((3.5 0.866,4 0,5 0,5.5 0.866,5 1.732,4 1.732,3.5 0.866)),((3.5 2.598,4 1.732,5 1.732,5.5 2.598,5 3.464,4 3.464,3.5 2.598)))
Child tiles that intersect a doubled-size parent hexagon share the tiling origin, but do not nest perfectly inside the parent.
WITH parent AS (
SELECT ST_Hexagon(2, 0, 0) AS geom
), children AS (
SELECT child.*
FROM parent
CROSS JOIN LATERAL ST_HexagonGrid(1, geom) AS child
WHERE ST_Area(ST_Intersection(child.geom, parent.geom)) > 0
)
SELECT ST_Collect(geom ORDER BY i, j) AS children,
(SELECT geom FROM parent) AS parent
FROM children;
MULTIPOLYGON(((-2.5 -0.866,-2 -1.732,-1 -1.732,-0.5 -0.866,-1 0,-2 0,-2.5 -0.866)),((-2.5 0.866,-2 0,-1 0,-0.5 0.866,-1 1.732,-2 1.732,-2.5 0.866)),((-1 -1.732,-0.5 -2.598,0.5 -2.598,1 -1.732,0.5 -0.866,-0.5 -0.866,-1 -1.732)),((-1 0,-0.5 -0.866,0.5 -0.866,1 0,0.5 0.866,-0.5 0.866,-1 0)),((-1 1.732,-0.5 0.866,0.5 0.866,1 1.732,0.5 2.598,-0.5 2.598,-1 1.732)),((0.5 -0.866,1 -1.732,2 -1.732,2.5 -0.866,2 0,1 0,0.5 -0.866)),((0.5 0.866,1 0,2 0,2.5 0.866,2 1.732,1 1.732,0.5 0.866))) | POLYGON((-2 0,-1 -1.732,1 -1.732,2 0,1 1.732,-1 1.732,-2 0))
Counting points in hexagons.
To do a point summary against a hexagonal tiling, generate a hexagon grid using the extent of the points as the bounds, then spatially join to that grid.
SELECT COUNT(*), hexes.geom
FROM ST_HexagonGrid(
10000,
ST_SetSRID(ST_EstimatedExtent('pointtable', 'geom'), 3857)
) AS hexes
INNER JOIN pointtable AS pts
ON ST_Intersects(pts.geom, hexes.geom)
GROUP BY hexes.geom;
Generating hex coverage of polygons.
If a grid is generated for each polygon boundary and filtered to intersecting cells, adjacent regions receive overlapping hexagons along their shared border. The build-time figure below uses two simple regions in place of an external administrative-boundary table, so the example is self-contained.
WITH admin1(gid, geom) AS (
VALUES
(1, ST_MakeEnvelope(0, 0, 3, 3, 3857)),
(2, ST_MakeEnvelope(3, 0, 6, 3, 3857))
), coverage AS (
SELECT hex.i, hex.j, hex.geom
FROM admin1
CROSS JOIN LATERAL ST_HexagonGrid(1.5, admin1.geom) AS hex
WHERE ST_Intersects(admin1.geom, hex.geom)
GROUP BY hex.i, hex.j, hex.geom
), layers AS (
SELECT 'region' AS kind, gid AS position, geom
FROM admin1
UNION ALL
SELECT
'hexagon',
1000 + row_number() OVER (ORDER BY i, j),
geom
FROM coverage
)
SELECT
CASE kind WHEN 'region' THEN geom END AS input_region,
CASE kind WHEN 'hexagon' THEN geom END AS hexagon
FROM layers
ORDER BY position;
POLYGON((0 0,0 3,3 3,3 0,0 0)) | null POLYGON((3 0,3 3,6 3,6 0,3 0)) | null null | POLYGON((-1.5 0,-0.75 -1.3,0.75 -1.3,1.5 0,0.75 1.3,-0.75 1.3,-1.5 0)) null | POLYGON((-1.5 2.6,-0.75 1.3,0.75 1.3,1.5 2.6,0.75 3.9,-0.75 3.9,-1.5 2.6)) null | POLYGON((0.75 1.3,1.5 0,3 0,3.75 1.3,3 2.6,1.5 2.6,0.75 1.3)) null | POLYGON((0.75 3.9,1.5 2.6,3 2.6,3.75 3.9,3 5.2,1.5 5.2,0.75 3.9)) null | POLYGON((3 0,3.75 -1.3,5.25 -1.3,6 0,5.25 1.3,3.75 1.3,3 0)) null | POLYGON((3 2.6,3.75 1.3,5.25 1.3,6 2.6,5.25 3.9,3.75 3.9,3 2.6)) null | POLYGON((5.25 1.3,6 0,7.5 0,8.25 1.3,7.5 2.6,6 2.6,5.25 1.3)) null | POLYGON((5.25 3.9,6 2.6,7.5 2.6,8.25 3.9,7.5 5.2,6 5.2,5.25 3.9))
The same pattern applies to a real administrative-boundary table:
|
|
|
The LATERAL keyword is implied for set-returning functions when referring to a prior table in the FROM list. So CROSS JOIN LATERAL, CROSS JOIN, or just plain , are equivalent constructs for this example. |
SELECT admin1.gid, hex.geom
FROM
admin1
CROSS JOIN
ST_HexagonGrid(100000, admin1.geom) AS hex
WHERE
adm0_a3 = 'USA'
AND
ST_Intersects(admin1.geom, hex.geom)
ST_EstimatedExtent, ST_SetSRID, ST_SquareGrid, ST_TileEnvelope