3

How to divide the world into cells of almost equal size, such that each lat,lon can be mapped to a different cell?

I am pretty sure I've seen a library do this, labelling cells as S1, S2, etc..

Say we have 62.356279,-99.422395 , how to map it to a 2km*2km cell named "FR,23" ?

Thank you!

Jim Jones
  • 18,404
  • 3
  • 35
  • 44
Slackware
  • 960
  • 1
  • 13
  • 29

2 Answers2

3

PostGIS 3.1+

PostGIS 3.1 introduces very easy to use grid generators, namely ST_SquareGrid and ST_HexagonGrid. An easy way to use these functions with data from a table is to use LATERAL to execute it, e.g. cells with 0.1° in size::

Sample Data

Consider the following polygon:

enter image description here

Creating a squared grid with cell size of 0.1° over a given geometry

SELECT grid.* FROM isle_of_man imn, 
LATERAL ST_SquareGrid(0.1,imn.geom) grid;

enter image description here

If you only want the cells that interersect with the geometry, just call the function ST_Intersects in the WHERE clause:

SELECT grid.* FROM isle_of_man imn, 
LATERAL ST_SquareGrid(0.1,imn.geom) grid
WHERE ST_Intersects(imn.geom,grid.geom);

enter image description here

The same principle applies to ST_HexagonGrid:

SELECT grid.* FROM isle_of_man imn, 
LATERAL ST_HexagonGrid(0.1,imn.geom) grid;

enter image description here

SELECT grid.* FROM isle_of_man imn, 
LATERAL ST_HexagonGrid(0.1,imn.geom) grid
WHERE ST_Intersects(imn.geom,grid.geom);

enter image description here

Older PostGIS versions

Inspired by this post I started writing a function to do just that - it still needs some tweaking but it'll certainly give you a direction look at. The following function creates a grid with cells of a given size covering the area of a given geometry:

CREATE OR REPLACE FUNCTION public.generate_grid(_size numeric,_geom geometry)
RETURNS TABLE(gid bigint, cell geometry) LANGUAGE 'plpgsql'
AS $BODY$
DECLARE
  _bbox box2d := ST_Extent(_geom);
  _ncol int := ceil(abs(ST_Xmax(_bbox)-ST_Xmin(_bbox))/_size);
  _nrow int := ceil(abs(ST_Ymax(_bbox)-ST_Ymin(_bbox))/_size);
  _srid int DEFAULT 4326;
BEGIN  
  IF ST_SRID(_geom) <> 0 THEN
    IF EXISTS (SELECT 1 FROM spatial_ref_sys crs
               WHERE crs.srid = ST_SRID(_geom) AND NOT crs.proj4text LIKE '+proj=longlat%') THEN
      RAISE EXCEPTION 'Only lon/lat spatial reference systems are supported in this function.';
    ELSE
      _srid := ST_SRID($2); 
    END IF; 
  END IF;
  
  RETURN QUERY 
   SELECT ROW_NUMBER() OVER (), geom FROM (
    SELECT 
     ST_SetSRID(
      (ST_PixelAsPolygons(
        ST_AddBand(
          ST_MakeEmptyRaster(_ncol, _nrow, ST_XMin(_bbox), ST_YMax(_bbox),_size), 
          '1BB'::text, 1, 0), 
         1, false)).geom,_srid)) j(geom);
END;
$BODY$;

Note: This function relies on the extension PostGIS Raster.

SELECT cell FROM isle_of_man, 
LATERAL generate_grid(0.1,geom);

enter image description here

... if you're only interested in cells that overlap your polygon, add a ST_Intersects to the query:

SELECT cell FROM isle_of_man, 
LATERAL generate_grid(0.1,geom)
WHERE ST_Intersects(geom,cell)

enter image description here


Other alternatives

Mike's fishnet function does basically the same, but you'd need to manually provide the number of rows and columns, and the coordinate pair of the lower left corner:

SELECT ST_SetSRID(cells.geom,4326)
FROM ST_CreateFishnet(4, 6, 0.1, 0.1,-4.8411, 54.0364) AS cells;

enter image description here

You could use this makegrid_2d function to create a grid over an area using a polygon, e.g. a grid with cells of 5000 meters in size:

CREATE TABLE grid_isle_of_man AS
SELECT 'S'||ROW_NUMBER() OVER () AS grid_id, (g).geom 
FROM (
  SELECT ST_Dump(makegrid_2d(geom,5000))
  FROM isle_of_man) j(g)
JOIN isle_of_man ON ST_Intersects((g).geom,geom);

enter image description here

The same logic applies for this hexagrid function. It creates a hexagon grid with fixed sized cells over a given BBOX. You can either manually provide the BBOX (function's second parameter) or extract it from a given polygon. For instance, to create a hexagrid that matches the polygon's extent and store it in a new table with the label you want - with cells of 0.1° in size:

CREATE TABLE hexgrid_isle_of_man AS 
WITH j (hex_rec) AS (
  SELECT generate_hexagons(0.1,ST_Extent(geom)) 
  FROM isle_of_man
)
SELECT 'S'||ROW_NUMBER() OVER () AS grid_id,(hex_rec).hexagon FROM j
JOIN isle_of_man t ON ST_Intersects(t.geom,(hex_rec).hexagon);

enter image description here

Further reading:

Jim Jones
  • 18,404
  • 3
  • 35
  • 44
2

Jim's answer is excellent. There are use cases though, where you don't need the actual geometries. Where, as you've mentioned, all you need is coordinates in the same cell mapping to the same code. So instead of a costly point-in-polygon operation that takes O(n) for n polygons - without index, that is ;) - you call a function that simply evaluates a formula transforming a coordinate into a code in O(1). Very handy to aggregate data spatially fast.

Personally, I love Uber's H3 library for that sort of thing, but I'm sure S2 does something similar and does it well. There is a well-maintained PostgreSQL binding for H3 and a simple aggregation example would look something like this:

SELECT h3_geo_to_h3(geom_4326, 9) AS h3res09, SUM(pop_19) AS pop_19
FROM uk_postcode_population
GROUP BY 1;

Read: Sum up the postcode-level population of the UK for each resolution 9 hexagon.

You can still create the actual hexagon geometries when you need them (whole hex grids even). But in my experience, once you commit to the grid approach, you will only need polygons for visualisation in the very end.

I should note that you can't divide the world yourself using this library - Uber has already divided it for you. So if 2km squares are a hard requirement, this is not for you.

Installing the H3 extension is not as straightforward as CREATE EXTENSION postgis, but you don't need to be a command line warrior either. You will at the least have to install PGXN, most likely PostgreSQL's extension build library and the extension itself.


Someone asked for a point-in-polygon example in the comments. This is not exactly related to the question, but highlights how one would use H3.

Polygon layer prep:

CREATE TABLE poly_h3 AS (
    SELECT id, h3_polyfill(geom_4326, 13) AS h3res13
    FROM poly
);
CREATE INDEX ON poly_h3 (h3res13);

Point layer prep:

ALTER TABLE points ADD COLUMN h3res13 h3index;

UPDATE points
SET h3res13 = h3_geo_to_h3(geom_p_4326, 13);

CREATE INDEX ON points (h3res13);

Count points per polygon:

SELECT poly.id, COUNT(*) AS n
FROM poly_h3 AS poly
    INNER JOIN points AS x
        ON poly.h3res13 = x.h3res13
GROUP BY poly.id;
chris
  • 423
  • 2
  • 9
  • H3 looks quite nice... how come I never heard about it till now! Thanks for sharing +1 – Jim Jones Jul 13 '21 at 12:32
  • 1
    Ah Jim, small world - we must have missed each other by a year. Sorry, I don't usually stalk people on Stackoverflow, but I just had such a great time at IfGI! – chris Jul 13 '21 at 13:34
  • That's indeed a nice coincidence. I "left" ifgi some years ago but I still work in the university. I also had the best time ever over there.. I miss it very much. Schöne Grüße nach London :) – Jim Jones Jul 13 '21 at 13:54
  • Hey Chris, could you give an example for the function you described in your first paragraph? This is exactly what I am looking for, but I would need a starting point :) – four-eyes Jul 29 '22 at 15:56
  • @Stophface I've added an example at the end – chris Jul 31 '22 at 12:05
  • Thanks Redu, thanks. That clarifies! – four-eyes Aug 01 '22 at 18:18