Skip to contents

Core H3 Themes

There’s lots of really good content and documentation on the fundamentals of H3. This post touches on some of the core concepts, and shows you how you can use them in R through h3r

library(mapdeck)     ## Plots / Maps
library(sfheaders)   ## building sf objects
library(secret)      ## accesing Mapbox Token

mapdeck::set_token(secret::get_secret("MAPBOX"))

H3 is a geospatial indexing system that partitions the world into hexagonal cells

The H3 Core Library implements the H3 grid system. It includes functions for converting from latitude and longitude coordinates to the containing H3 cell, finding the center of H3 cells, finding the boundary geometry of H3 cells, finding neighbors of H3 cells, and more.

Cells

Resolution 0 cells are the largest (i.e, they cover the largest area) and resolution 15 are the smallest. The whole globe is covered with cells at each resolution.

We’re using the term ‘cells’ because each resolution is made up of some number of hexagons, and exactly 12 pentagons.

If we plot all the Resolution 0 cells on a ‘flat’ map we get distortions as the cells get further from the equator. This is purely a result of plotting using the Mercator projection. However, all hexagons at each resolution are roughly the same size.

cells <- h3r::getRes0Cells()
mapdeck(
  libraries = "h3"
  , style = mapdeck_style("light")
  , repeat_view = TRUE
  )  %>%
  add_h3(
    data = data.frame(x = cells)
    , hexagon = "x"
    , fill_colour = "x"
    , fill_opacity = 0.4
    , tooltip = "x"
    , stroke_colour = "#000000"
    , stroke_width = 25000
  )
resolution0
resolution0

Plotting (interlude)

We’re using the mapdeck library for the maps because it uses deck.gl as the underlying plotting engine. And given it is also developed by Uber, it can natively plot the H3 Cells, so there’s no need to convert to lat/lon coordinates.

However, note that you have to tell mapdeck() you want to plot the cells by adding the libraries = "h3" parameter:

mapdeck(libraries = "h3") %>%
  add_h3()

Pentagons

The reason for using pentagons is summarised by Uber

The H3 grid is constructed on the icosahedron by recursively creating increasingly higher precision hexagon grids until the desired resolution is achieved. Note that it is impossible to tile the sphere/icosahedron completely with hexagons; each resolution of an icosahedral hexagon grid must contain exactly 12 pentagons at every resolution, with one pentagon centered on each of the icosahedron vertices.

The nice feature about the pentagons is that they are all centered in the ocean.

cells <- h3r::getRes0Cells()
is_pentagon = h3r::isPentagon(cells)
mapdeck(
  libraries = "h3"
  , style = mapdeck_style("dark")
  , repeat_view = TRUE
  )  %>%
  add_h3(
    data = data.frame(x = cells, is_pentagon = is_pentagon)
    , hexagon = "x"
    , fill_colour = "is_pentagon"
    , fill_opacity = 0.5
    , tooltip = "x"
    , stroke_colour = "#000000"
    , stroke_width = 25000
  )
pentagons
pentagons


Icosahedron faces

And the pentagons are the corners of each of the 20 icosahedron faces (i.e, each triangle).

cells <- h3r::getRes0Cells()
is_pentagon = h3r::isPentagon(cells)

pentagonCells <- cells[as.logical(is_pentagon)]
pentagonFaces <- h3r::getIcosahedronFaces(pentagonCells) 

## Make triangles out of the pentagons
df_pentagonFaces <- data.frame(
  cell = rep(names(pentagonFaces), lengths(pentagonFaces))
  , face = unlist(unname(pentagonFaces))
  )

df_pentagonFaces <- df_pentagonFaces[ order(df_pentagonFaces$face), ]

df_pentagonFaces <- cbind(
  df_pentagonFaces
  , h3r::cellToLatLng(df_pentagonFaces$cell)
)


sf_faces <- sfheaders::sf_polygon(
  obj = df_pentagonFaces
  , polygon_id = "face"
  , x = "lng"
  , y = "lat"
)
## For plotting reasons I'm removing the faces that cross the 180 & Poles
mapdeck(
  libraries = "h3"
  , style = mapdeck_style("light")
  , repeat_view = FALSE
  )  %>% 
  add_polygon(
    data = sf_faces[ !sf_faces$face %in% c(1, 6, 11, 15, 16, 19), ]
    , fill_colour = "face"
    , fill_opacity = 0.3
    , tooltip = "face"
    , stroke_width = 50000
  ) %>%
  add_h3(
    data = data.frame(cell = pentagonCells)
    , hexagon = "cell"
    , fill_opacity = 0.6
  )
Icosahedrons
Icosahedrons


Changing Resolution

If we plot all the Resolution 0 cells at the center of each icosahedron face, plus the children of each of these (by going up to resolution 1) we see there are (approximately) 7 children per cell.

cells <- c(
  '8021fffffffffff', '8005fffffffffff', '800ffffffffffff', '8035fffffffffff',
  '803ffffffffffff', '8065fffffffffff', '8033fffffffffff', '8049fffffffffff', '8081fffffffffff', '8097fffffffffff', '8073fffffffffff', '805dfffffffffff',
  '808ffffffffffff', '80c1fffffffffff', '80abfffffffffff', '80bffffffffffff',
  '80b5fffffffffff', '80d3fffffffffff', '80effffffffffff', '80e5fffffffffff'
  )

children_cells <- unlist(h3r::cellToChildren(cells, 1L))
mapdeck(
  libraries = "h3"
  , style = mapdeck_style("dark")
  )  %>%
  add_h3(
    data = data.frame(x = c(cells, children_cells))
    , hexagon = "x"
    , fill_colour = "x"
    , fill_opacity = 0.5
    , tooltip = "x"
    , stroke_colour = "#000000"
    , stroke_width = 50000
  )
Resolution
Resolution


And if we continue to plot finer and finer resolution cells (all of which are children of the parent cell) we can start to see the rotation effect when changing resolution.

As you go from an odd-to-even resolution, the hexagons are rotated by ~19.1 degrees. Then as you go from even-to-odd, they are rotated back again.

So all the even-resolution cells are at the same orientation as each other, and similarly all the odd-resolution cells are at the same orientations as each other.

cells <- c('8081fffffffffff')

## Children and resolutions 1:4
children_cells <- unlist(h3r::cellToChildren(cells, 1L))
children_cells_res2 <- unlist(h3r::cellToChildren(children_cells[1], 2L))
children_cells_res3 <- unlist(h3r::cellToChildren(children_cells_res2[1], 3L))
children_cells_res4 <- unlist(h3r::cellToChildren(children_cells_res3[1], 4L))
mapdeck(
  libraries = "h3"
  , location = rev(as.numeric(cellToLatLng(cells)))
  , zoom = 3
  , style = mapdeck_style("dark")
  )  %>%
  add_h3(
    data = data.frame(
      x = c(cells, children_cells, children_cells_res2, children_cells_res3, children_cells_res4)
      )
    , hexagon = "x"
    , fill_colour = "x"
    , fill_opacity = 0.4
    , tooltip = "x"
    , stroke_colour = "#000000"
    , stroke_width = 5000
    , update_view = TRUE
  )
Alternating Resolution
Alternating Resolution


Cells and Coordinates

For this section we’ll be using the stations data from the h3r package

df_stations <- h3r::stations
head(df_stations)
##   stop_id    stop_name       lat      lon
## 1   15351      Sunbury -37.57909 144.7273
## 2   15353 Diggers Rest -37.62702 144.7199
## 3   19827  Stony Point -38.37423 145.2218
## 4   19828   Crib Point -38.36612 145.2040
## 5   19829     Morradoo -38.35403 145.1896
## 6   19830      Bittern -38.33739 145.1780

You can convert these coordinates to their containing cells…

df_stations$cell <- h3r::latLngToCell(
  lat = df_stations$lat
  , lng = df_stations$lon
  , resolution = 8
  )
head(df_stations)
##   stop_id    stop_name       lat      lon            cell
## 1   15351      Sunbury -37.57909 144.7273 88be637a09fffff
## 2   15353 Diggers Rest -37.62702 144.7199 88be634497fffff
## 3   19827  Stony Point -38.37423 145.2218 88be606e81fffff
## 4   19828   Crib Point -38.36612 145.2040 88be606e95fffff
## 5   19829     Morradoo -38.35403 145.1896 88be606eb3fffff
## 6   19830      Bittern -38.33739 145.1780 88be606cdbfffff

… but you can’t get the coordinates back again. You can only get the center coordinates for each cell.

Notice here how the returned coords are different to the original coordinates

coords <- h3r::cellToLatLng(df_stations$cell)

cbind( head(coords), head(df_stations) )
##         lat      lng stop_id    stop_name       lat      lon            cell
## 1 -37.58134 144.7259   15351      Sunbury -37.57909 144.7273 88be637a09fffff
## 2 -37.62727 144.7150   15353 Diggers Rest -37.62702 144.7199 88be634497fffff
## 3 -38.37178 145.2217   19827  Stony Point -38.37423 145.2218 88be606e81fffff
## 4 -38.36837 145.1992   19828   Crib Point -38.36612 145.2040 88be606e95fffff
## 5 -38.35136 145.1916   19829     Morradoo -38.35403 145.1896 88be606eb3fffff
## 6 -38.34114 145.1766   19830      Bittern -38.33739 145.1780 88be606cdbfffff
# Notice how they are different to head(stations)

Boundaries

If you want the cell as a spatial polygon you can get their boundaries as coordinates, and convert to an sf object, as in this example

boundaries <- h3r::cellToBoundary(df_stations$cell)

## The `cellToBoundary` function returns a named list. Each element is named 
## with the cell name, and the coordinatres are the vertieces of the hexagons. 
##
## However, some stations are in the same cell
## so Replace the cell name with the stop name

names(boundaries) <- df_stations$stop_name

boundaries <- do.call(rbind, boundaries)
boundaries$id <- gsub("\\.[0-9]","",rownames(boundaries))

## Make a polygon
sf_boundaries <- sfheaders::sf_polygon(
  boundaries
  , x = "lng"
  , y = "lat"
  , polygon_id = "id"
  )

Notice in the map how some stations share the same cell (at resolution 8)

mapdeck(
  location = c(144.967590, -37.810935)
  , zoom = 11
  ) %>% 
  add_scatterplot(
    data = df_stations
    , update_view = FALSE
    , radius = 50
  ) %>%
  add_polygon(
    sf_boundaries
    , fill_colour = '#9f19ff88'
    , stroke_width = 25
    , update_view = FALSE
  )
Stations
Stations



References

  1. H3: Tiling the Earth with Hexagons Youtube - Uber Engineering
  2. Tables of Cell Statistics Across Resolutions h3Geo.org