A grid prints as one line
(<HexGridInfo> aperture 3, resolution 10: 590,492 cells of 863.8 km^2)
and gridded data as two; summary() keeps the full listing,
which for gridded data now includes the grid’s orientation and ellipsoid
(#108). The list hexify_grid() builds and the
dggs list as_dggrid() returns print as one
line too.
Each class has one help page listing its methods:
?HexGridInfo-class and ?HexData-class carry
show(), summary(), plot(),
st_as_sf(), st_crs(), as_tibble()
and the accessors (#110). Help titles are in Title Case. The empty pages
hexify-conversions, hexify-grid and
hexify-stats are gone, and hexify_grid(),
dgverify(), hexify_compare_resolutions(),
hexify_roundtrip_test() and the projection-level helpers
(hexify_forward(), hexify_inverse(),
hexify_forward_to_face(), hexify_which_face(),
hexify_face_centers(), hexify_build_icosa())
are marked internal: hex_grid() is the grid
constructor.
Every exported user-facing topic has a runnable example; the
hex_globe_png() example runs in an interactive session with
hexglobe installed (#109).
hex_grid(area_km2 = , type = "h3") no longer warns
on every call: the note that H3 cells are not equal-area is one message
per session on both the area and the resolution path (#112).
grid_clip(), hexify_heatmap() and
plot() no longer print sf’s s2 switch and planar-assumption
messages, and dg_closest_res_to_area(show_info = TRUE)
reports in one message.
The README and vignettes read grids and data through
summary(), as.data.frame(),
n_cells() and $ columns.
ISEA cell IDs are 64-bit integers (bit64::integer64,
a new import), exact at every resolution (#99). They were doubles, which
hold whole numbers exactly only below 2^53: aperture 4 from resolution
25 and aperture 7 from resolution 18 returned IDs that decoded to other
cells, and aperture 7 from resolution 22 IDs that did not decode.
lonlat_to_cell(), get_parent(),
get_children(), get_neighbors(),
cell_to_sf(), hexify() and every other
function returning ISEA cell IDs return integer64; functions taking them
also accept whole numbers below 2^53 and digit strings, and refuse a
double of 2^53 or more, which cannot name a cell exactly. H3 cell IDs
stay character. n_cells() stays a double: it is a count,
not an ID. unlist(), sapply(),
ifelse() and for loops drop the integer64
class, leaving raw bits that read as numbers between 0 and 1: combine
the lists get_neighbors() and get_children()
return with do.call(c, x). A value between 0 and 1 passed
as a cell ID is refused with that hint.
A grid’s resolution is capped where its cell count passes 2^63 -
1, the largest integer64: on the icosahedron aperture 3 keeps
resolutions 0-30, aperture 4 stops at 29 and aperture 7 at 21 (on the
octahedron, aperture 4 reaches 30). hex_grid() names the
finest resolution when asked for a finer one, and an
area_km2 finer than that clamps to it. Aperture 7’s Z7
index strings, previously limited to resolution 20, follow the grid to
21.
plot_globe() is removed.
plot(<grid>) draws a grid in 3D, on the sphere
(surface = "sphere") or on the flat faces of the solid the
grid is built on (surface = "solid"), with land fill and
country outlines from hexify_world or any sf polygons
(land =), a center to look down on, and the
solid’s face edges. It takes the grid object, so any aperture, mixed
sequence, resolution, solid or body reaches it; an H3 grid is drawn on
the sphere. globe_centers names the views as
before.
A grid built with one aperture per level stores it
comma-separated:
hex_grid(resolution = 4, aperture = c(4, 4, 7, 3))@aperture
is "4,4,7,3", no longer "4/4/7/3";
aperture = "4,4,7,3" is accepted and “/” is kept for the
two-aperture families such as "4/3". A two-level sequence
c(4, 7) was stored as "4/7", the family name,
so its parents were cells of the family’s resolution-1 grid (aperture 7)
rather than of its own first level (aperture 4). Cell IDs are unchanged.
get_children() on a per-level grid now says that the
spelling names no finer level.
The aperture-7 Z7 index on the icosahedron is now IGEO7’s, string
for string the one DGGRID writes: every cell at resolutions 0 to 7
agrees with DGGRID, on ISEA and FULLER and under a given orientation
(paper/bench/bench_dggrid_z7.R). Previously about two cells
in three differed: a cell whose ancestry leaves its quad was written as
quad + 12 seed instead of the base cell it descends from, and the
digits were not turned into a polar base cell’s frame or past the
direction each pentagon lacks. DGGRID’s encoder is bijective: the two
cells #53 reported as sharing a string get distinct strings from it, so
that report came from hexify’s own earlier port, not from DGGRID. The
axis names of digits 3 to 6 were also swapped in hexify, which turned
digit rotations the wrong way. A pentagon has six children, so
hexify_get_children() returns six strings under one, and
hexify_z7_canonical() sends a string in the deleted
subsequence to the cell’s own index. Cell IDs are unchanged. Octahedral
grids keep the quad + 6 seed leading field, since IGEO7 is defined
on the icosahedron (#86).
hexify_cell_id_to_quad_ij() is removed.
hexify_cell_to_quad_ij() takes the same arguments and
returns the same quad, i, j data
frame.
The inverse of Snyder’s projection is now in closed form (Recht
2021), so hexify_set_precision(),
hexify_get_precision(), hexify_set_verbose()
and hexify_projection_stats() are removed, and
hexify_inverse() no longer takes tol or
max_iters. The inverse agrees with the previous Newton
iteration to within 1e-13 radians on the icosahedron, octahedron and
tetrahedron, returns a forward-projected point to within a few times
1e-15 radians, and is about twice as fast (#84).
hex_grid(projection = "ivea") builds a grid on van
Leeuwen and Strebe’s
(2006) vertex-oriented great-circle projection, the third face
projection beside Snyder’s and Fuller’s: DGGAL’s IVEA3H and IVEA7H, and
any aperture, aperture sequence, orientation, the octahedron, and
hex_globe(). It is equal-area, so cell_area()
gives every hexagon of a resolution the same area, and its inverse is in
closed form. With DGGAL’s orientation
(orientation = c(11.20, 58.282525588538995, 0)) every zone
of IVEA3H at levels 1 to 8 and of IVEA7H at levels 1 to 5 is a hexify
cell, centres and corners within a micrometre
(paper/bench/bench_dggal_agreement.R).
hexify_forward() and hexify_inverse() take it
on the tetrahedron too. DGGRID has no IVEA, so as_dggrid()
refuses such a grid (#92).
projection_distortion() gives Tissot’s indicatrix of
a grid’s face projection at any point: the scale factors, the maximum
angular deformation, the areal scale and the direction of the longer
axis. The derivative is exact, from forward-mode automatic
differentiation of the projection code. Snyder’s projection reproduces
his Table 1 (17.27 degrees, scale factors 1.163 and 0.860) and keeps
areas to 1e-14; Fuller’s areal scale averages 1.
plot(<grid>, distortion = "angular" / "areal")
colours the sphere, the solid or a net by it, and tissot =
draws the ellipses on the solid and the net (#88).
Densified cell boundaries, the cell paths plot()
draws and the globe’s face and land meshes put a vertex where an edge
crosses an arc from a face centre to a corner, where Snyder’s projection
bends, and densified boundaries also where they cross a face edge
(#88).
plot(<grid>, parents = ) outlines the cells
one or more resolutions up over the grid’s cells; on a net,
area_legend = TRUE draws one cell of each resolution at the
map’s scale with its area, and tabs = TRUE adds glue tabs
along the seams so a printed net folds into the solid (#90).
hex_rays() draws Carr’s ray glyphs, whose angle
shows a value, with confidence arcs and a second ray for a second
variable, and hex_triangles() cuts cells into one triangle
per wall carrying the change towards that neighbour (#90).
net_cells() cuts cells into their parts on the
pieces of a net layout, with each part’s map area and which parts the
map shows joined, so the cells a seam cuts can be counted; on Snyder’s
projection the parts of a cell add up to its area (#90).
hex_grid(polyhedron = "octahedron") builds an
ISEA-family grid on the octahedron: Snyder’s equal-area projection with
g = 54.73561032 deg, G = 45 deg (Snyder 1992), four diamonds, and six
square vertex cells, each two thirds of a hexagon’s area. Apertures 3,
4, 7 and mixed sequences, the hierarchy, indices, neighbours,
cell_area(), plot() and
hex_globe() work as on the icosahedron. The solid is a
descriptor (vertices, faces, Snyder’s constants) from which the face
pairing, the quad adjacency and the vertex cells are derived, so the
icosahedron’s grids are unchanged bit for bit. Fuller’s projection stays
icosahedron-only (#94).
hexify_forward(), hexify_inverse(),
hexify_which_face(), hexify_face_centers() and
hexify_lonlat_to_plane() take
polyhedron = "octahedron" or "tetrahedron".
The tetrahedron carries the projection only, and
hex_grid(polyhedron = "tetrahedron") says why: the cell
numbering pairs faces into diamonds that leave exactly two vertices as
single-cell poles, a vertex where three faces meet has to be one of
them, and all four vertices of the tetrahedron are such
vertices.
cell_to_sf(shape = , depth = ) draws ISEA cells as
Gosper islands ("gosper") or as the outline of their
descendants ("descendants"); plot(),
hexify_heatmap() and st_as_sf() on gridded
data take the same arguments. A Gosper island replaces each cell edge
depth times by three segments 1/sqrt(7) as
long, turned by atan(sqrt(3)/5), on the face plane of the
projection, so on an equal-area grid each island has its cell’s area and
the islands of a grid tile the sphere. The descendant outline of an
aperture-7 cell is the boundary of its descendants depth
resolutions down, which nests across resolutions; the child lattice of
ISEA7H turns one way at one resolution and back at the next, so this
outline stays close to the hexagon (#82).
get_children() on ISEA grids looks children up by
match() rather than by name, so its time grows linearly
with the number of cells: all 492 cells of an aperture-7 resolution-2
grid, three levels down, go from 123 s to 0.5 s.
hex_grid(projection = "fuller") builds an
ISEA-family grid on Fuller’s projection instead of Snyder’s equal-area
one: DGGRID’s FULLER3H, FULLER4H, FULLER7H and FULLER43H, and any
aperture sequence or orientation. The forward projection follows Gray
(1995) and the inverse solves Gray’s equation (39) in Crider’s (2008)
form in closed form, as a cubic solved by Viete’s trigonometric method
(#96). Cell IDs, the hierarchy and neighbours are those of the ISEA
grid; centres and corners agree with DGGRID’s FULLER output. Fuller
cells are not equal-area, so area_km2 is the mean cell area
and cell_area() returns each cell’s own, from its solid
angle. hexify_forward(),
hexify_forward_to_face() and hexify_inverse()
take projection, as_dggrid() and
from_dggrid() carry FULLER,
dggrid_is_compatible() accepts it, and
hex_globe() finds Fuller cells per pixel as it does ISEA
cells.
hex_globe(<grid>) draws a grid on an
interactive globe, rendered on the graphics card through WebGPU (an
htmlwidget; needs ‘htmlwidgets’). Drag turns the globe, Shift-drag tilts
and turns the view, the wheel zooms, and a slider folds the icosahedron
into the sphere. values fills the cells through a colour
ramp (palette, limits, na_fill).
It takes the camera and style arguments of
plot(<grid>). The cells of an ISEA grid are found per
pixel on the graphics card, with lonlat_to_cell()’s
projection and numbering, so the page carries the grid’s description and
one value per cell rather than cell outlines: at aperture 3, resolution
10 (590,492 cells), a globe with a value on every cell builds in 0.4 s,
its page is 7 MB and a frame takes about 3 ms at 800 and at 1600 pixels.
The shader’s cells agree with lonlat_to_cell() to within
1e-5 radians. Borders keep their width at every zoom and fade out where
cells shrink to a few pixels. The pointer shows the ID and value of the
cell under it. H3 cells are drawn from their outlines. Without WebGPU
the widget shows a notice.
hex_globe_png() saves a globe as a PNG, drawn with
the widget’s own WebGPU shader and pipelines and read back from the
graphics card, so a machine without a display saves the globe rather
than a blank page; pixels off the globe are transparent.
renderer = "wgpu" (the default) draws through the companion
package ‘hexglobe’ (‘wgpu’, the Rust WebGPU implementation) with no
browser; renderer = "chrome" draws the widget in headless
Chrome (needs ‘chromote’). The two agree to within edge anti-aliasing
(#107).
The article “Interactive Globe” on the package website shows
hex_globe() live and lists the browsers that draw it. The
globe keeps its colours in the site’s dark mode.
Land is triangulated on the faces of the icosahedron in C++, so
filled land folds with the grid; the land area of the mesh matches
sf::st_area() of hexify_world.
Both surfaces read their cell boundaries from the same points: each cell edge is walked in the face plane, where it is straight, and cut where it crosses a face edge, so a cell on the icosahedron is the cell on the sphere folded flat.
plot(<grid>) takes a viewpoint.
projection = "perspective" places a camera
distance sphere radii from the centre, tilt
swings it about the point at center, which it keeps looking
at, until the horizon shows, and fov sets its field of
view; by default the frame holds the whole visible sphere.
rotation turns either projection about the line of sight.
Filled shapes are clipped at a plane in front of the camera, so any tilt
below 90 degrees draws.
hex_grid(orientation = ) places an ISEA grid’s
icosahedron anywhere on the sphere (#80):
c(vert0_lon, vert0_lat, azimuth) as DGGRID’s
dggs_vert0_lon, dggs_vert0_lat and
dggs_vert0_azimuth take it, "random"
(repeatable with set.seed()), or "region" with
region = a point or an sf object, which centres the grid as
DGGRID’s REGION_CENTER does (on the midpoint of an
icosahedron edge), or "face" with region =,
which puts the region’s centre on the centre of a face. The grid stores
it in a new orientation slot, and every function taking the
grid reads it, the hex_globe() shader included; grids saved
before the slot existed read the standard orientation. Cell IDs, the
hierarchy and neighbours are the same under every orientation. Under
three given orientations and three region placements, 50,000 random
points per grid fall in the same cells as in DGGRID for apertures 3, 4,
7, ISEA43H and a mixed sequence, with cell centres within 2 mm. H3 grids
keep H3’s own orientation.
as_dggrid(), from_dggrid() and
dggrid_is_compatible() carry the orientation
(pole_lon_deg, pole_lat_deg,
azimuth_deg) instead of warning about or rejecting a
non-standard one.
hexify_build_icosa() sets the orientation of the
functions that take no grid, and no longer reaches grids;
hex_grid() and hexify_grid() no longer reset
it.
hex_grid(orientation = "dymaxion") places the
icosahedron in Fuller’s Dymaxion orientation (#89; Sahr et al. 2003):
vertex 0 at 5.2454W, 2.3009N, azimuth 7.46658, with all twelve pentagons
in the ocean. With projection = "fuller" the grid lies on
Fuller’s Dymaxion map.
New net_layout() lays out the faces of a grid’s
solid as a flat map, and
plot(<grid>, surface = "net", layout = ) draws on it
(#83). A layout cuts faces into pieces and places each by a rotation and
a translation, so a piece can appear more than once.
"plane" is DGGRID’s PLANE layout, as before;
"gosper" is Van de Sande’s Gosper World on the octahedron,
four regular hexagons each of one face and a third of its three
neighbours, equal-area on Snyder’s projection;
"gosper_flower" surrounds one hexagon by the six that
border it; "gosper_land" joins the four hexagons along the
sides that cross the most land, as in Van de Sande’s Fig. 1;
"rhombic" is DGGAL’s 5 x 6 rhombic space on the
icosahedron, each face sheared onto half a unit square;
"land" cuts the solid along the edges that cross the least
land, a Dymaxion-style net with
orientation = "dymaxion", projection = "fuller".
net_project() places points on a layout.
seams = TRUE draws the cuts of the net.
hex_grid(polyhedron = "octahedron", orientation = "gosper")
puts the poles at midpoints of octahedron edges, as the Gosper World
does, with all six vertices at least 619 km offshore.
plot(<grid>, graticule = ) draws meridians and
parallels on the sphere, the solid and the net.
New cell_metrics() and wall_metrics()
measure a grid’s cells: area, perimeter and compactness (White et
al. 1998) per cell, and per wall between adjacent cells its length, the
distance between the two centres and the cell wall midpoint ratio
(Gregory et al. 2008). Without cell_id they measure every
cell of the grid. ISEA walls are followed on the sphere as the
projection curves them, for Snyder’s and Fuller’s projections alike,
each piece halved until its length and area agree measured whole and as
two halves: perimeters come out within about 1e-10 of their converged
values and areas within about 1e-9, at a tenth of a millisecond to a
millisecond per cell. H3 walls are read from H3’s directed
edges.
New hex_smooth() smooths cell values over
neighbouring cells (#90): each step replaces a value by the weighted
mean of the cell and its listed neighbours, the neighbour-matrix
smoother of Carr et al. (1997). group keeps groups such as
land and ocean apart; NA values and unlisted cells do not
contribute.
Zone queries of OGC Topic 21 (#105).
get_parent(overlapping = TRUE) returns every coarser cell a
cell reaches into (Topic 21’s parent() with
inheritID false): three for an aperture-3 cell on a parent
corner, two for an aperture-4 cell on a parent edge or an aperture-7
cell off the parent’s centre. get_siblings() returns the
other children of a cell’s parent, and
get_neighbors(ring = TRUE) the hollow ring of cells exactly
k hops away.
hex_grid() reads an aperture list shorter than the
resolution as a recurring sequence, as Topic 21 reads its list of
refinement ratios: aperture = c(4, 3) at resolution 5
refines by 4, 3, 4, 3, 4 (#105).
dggrs_definition() describes a grid in the DGGRS
definition schema of OGC API - DGGS: solid, refinement ratios and
strategy, zone shapes, orientation, zone identifiers and sub-zone order
(#105).
cell_metrics() adds normalized_area, a
cell’s area over the grid’s mean, and ipq, the
isoperimetric quotient 4 pi A / p^2, the two measures of Kmoch et
al. (2022) (#106).
The article “OGC Topic 21 Conformance” sets hexify against each requirement of Topic 21, with the Earth model and its area error on WGS84, the distance between cell centre and area centroid, and how often binning and taking the parent disagree with binning at the parent’s resolution (#105, #106).
grid_global() returns every cell of an ISEA grid. It
sampled points and kept the cells they hit, which missed small polar
cells from about 40,000 cells on: 2 of 40,962 cells at aperture 4
resolution 6, 54 of 168,072 at aperture 7 resolution 5. It now lists the
cell IDs 1 to n.
get_parent() on a mixed aperture grid finds the
parent in exact lattice coordinates (#85). After an aperture-3 step a
child centre sits on a corner of three parents, and after an aperture-4
step on an edge shared by two; the parent of such a child was decided by
floating-point rounding of the projected centre, so a hexagonal parent
received anywhere from 1 to 7 children. A fixed tie rule now gives every
hexagonal parent inside a quad exactly as many children as the step’s
aperture. Children whose centre lies strictly inside a parent keep their
parent. get_children() and cell_to_index()
follow get_parent(), so index strings of mixed grids change
for the tied children; regenerate stored ones.
levels > 1 steps up one level at a time.
Cells in the last column of a quad at aperture 3, resolutions 25-30, were assigned to another quad: the quad edge dimension was computed in floating point and came out one too small. It is now exact.
cell_area() on an ISEA grid returns each cell’s
area: every hexagon of a resolution has S / (N - 2) of a
body of area S split into N cells, and each of
the 12 pentagons 5/6 of that. It returned the mean S / N
for every cell. The cell_area_km2 column of a HexData
object and of as.data.frame() reads
cell_area(), so it carries the same areas, on the Fuller
projection each cell’s own.
hexify_world is simplified as a coverage, so
neighbouring countries share their simplified borders exactly. It was
simplified country by country, which left about 480 slivers of gap
between neighbours; they showed as thin lines wherever land was filled
without country outlines. Attributes are unchanged.
A cell at an icosahedral vertex now drops the corner that lies in
the icosahedron’s angular deficit, found from the faces rather than a
fixed corner number. Under the lattice turn of a mixed sequence such as
c(4, 4, 7, 3) the fixed number named the wrong corner, and
those twelve pentagons were drawn with a corner read off the wrong
face.
On a mixed aperture sequence with an odd number of aperture-7
levels ("4/7" at odd resolutions,
c(4, 4, 7, 3), c(7, 3), …), the cell lattice
is turned by atan(sqrt(3)/5) and its cells straddle the quad edges, so a
point near an edge can have its nearest centre in the next quad.
lonlat_to_cell() clamped that centre back onto the quad and
read it as an unrelated cell: 2,500 to 4,200 boundary points per grid
went to the wrong cell, and grid_global() missed a cell of
c(4, 4, 7, 3) and of c(4, 3, 7). The centre
now moves to the quad that owns it through DGGRID’s edge table, as pure
aperture 7 already did.
At odd aperture-3 resolutions, a point on or just across an
icosahedron face edge could quantize to a centre two rows outside its
quad, which lonlat_to_cell() read as an unrelated cell
(often a pentagon). Such centres now go through the same edge table.
Points away from face edges keep their cells.
The Snyder projection constants are computed from their closed forms at full double precision (tan θ = 3 - sqrt(5), a face edge of sqrt(4π / (5 sqrt(3))), R’ = edge / (sqrt(3) tan θ), vertex latitude atan(1/2)) instead of being carried to 8 to 10 digits, and the icosahedron’s vertices are rotated into place on unit vectors, which places them to 4e-16 where they were off by 1e-8. Cell assignments move only for points within about 1e-8 rad (6 cm) of a cell boundary: 2 of 3.8 million random points across 19 grids.
Forward and inverse projection put an azimuth that lies exactly on a 120° sector boundary, a line from a face centre through a vertex, in different sectors, so the inverse returned such a point on the mirrored side of the face. Both now read one half-open sector rule. A point on a face edge now gets one of the two cells beside it at every resolution.
cell_to_sf(), grid_rect(),
grid_global(), hexify_cell_to_sf() and the
plot methods now split each edge until every piece is a straight lon/lat
chord to within 0.1% of its length, as DGGRID’s densification does.
Drawn with their six (or five) corners alone, cells were off by up to 4%
of their area at aperture 3 resolution 8 and up to 19% at coarse
resolutions; now within about 0.1%. A polygon now has six corners and
the points between them, so a cell’s vertex count is no longer six; the
corners themselves are unchanged.At odd aperture-7 resolutions, lonlat_to_cell(),
hexify() and the index functions gave a point near a cell
edge to a neighbouring cell: 7 to 11% of random points over central
Europe at resolutions 2 to 7. A point was snapped to the finer substrate
lattice and then to that point’s parent, so a cell was the union of
seven substrate hexagons rather than a hexagon. Points now go to the
cell whose centre is nearest, the hexagon its polygon draws, which
changes the cell ID of those points.
A cell straddling a quad edge was drawn with the corners beyond
the edge read on its centre’s face stretched across it, and an
icosahedral-vertex cell with its folded edge drawn straight across the
missing sector. Both are now drawn where lonlat_to_cell()
places their boundary.
grid_rect() on ISEA grids missed cells that
intersect the bounding box, mostly along its edges and occasionally
inside it. It now seeds from a point sampling and grows through
neighbouring cells that meet the box until none is added, so the result
is every cell meeting the box, independent of the sampling density
(#77). hexify_grid_rect() had its own copy of the sampling
and now calls grid_rect().
as_sf() is now sf::st_as_sf(). sf owns
the verb, so hexify registers methods on it for HexData and
HexGridInfo and re-exports it, and an sf pipeline reaches a
hexify object without knowing hexify’s own name for the conversion.
st_as_sf() on a grid specification returns its global cell
set. The geometry argument follows ..., as the
generic’s signature requires, so name it:
st_as_sf(x, geometry = "polygon").
The index entry points that returned one value now return one per
index: hexify_index_to_cell() and
hexify_index_to_lonlat() a data frame with a row per index,
hexify_get_children() a list with an element per index.
Reading a single index out of them takes [[1]] or
[1, ].
summary() on a grid or on gridded data returns what
printing it reports, as a list: grid type, aperture, resolution, area,
diagonal, CRS, radius and cell count for a grid; row and column counts,
column names, cell count, storage type and the grid’s own summary for
data. Printing an object prints this summary, so what it shows and what
it returns are the same thing.get_children() returned cell IDs past the end of the
child grid on apertures 3 and 4, and NAs on aperture 7, at
the twelve icosahedron vertices, so get_parent() stopped on
its own output there. A vertex sits at the corner of several quads and
has an index spelling in each, and appending a digit to one spelling
names cells the other quads hold: some digits named nothing, and the
children spelled under another quad were never reached. Children are now
read back from get_parent() over candidates drawn from both
the index expansion and the ring of child cells around the parent, so
they invert get_parent() exactly. Between them the parents
of a resolution now claim every cell of the child grid once, and an
aperture-7 pentagon has six children rather than five and two
NAs.
hex_compact() and hex_uncompact()
refused every ISEA grid but aperture 7, and did their arithmetic on the
index strings. Both read cells now, so all three pure apertures compact:
cells are grouped by get_parent(), and a group holding as
many distinct cells as the parent has children replaces them, which
reads the six children of a cell at an icosahedron vertex rather than
assuming seven. Uncompaction expands through get_children()
for the same reason, so the children spelled under a neighbouring quad
are reached. An index string that names no cell of the grid, which
appending a digit to a vertex cell writes, is refused by name rather
than reported as a cell number the caller never wrote. A full grid
compacts to the twelve resolution-0 cells at every aperture.
get_parent() on an aperture-7 grid at resolution 1
returned cell IDs past the end of the resolution-0 grid – up to 83,
where resolution 0 holds 12 – and only 42 distinct ones, so
get_children() at resolution 0 came back in threes rather
than sixes. A Z7 index spells its leading field as
quad + 12 * seed, the seed naming the point the hierarchy
walk arrives at, and a two-character index was read as a bare quad
number. It is decoded now: the seed says which of the base cells meeting
at the quad’s corner the index reaches, through the same adjacency the
encoder walks. Every resolution-0 cell sits on an icosahedron vertex and
now has the six children that make up resolution 1’s 72, at every
aperture the parents of a resolution claim each cell of the finer one
exactly once, and a full grid compacts to the twelve resolution-0 cells
at apertures 3, 4 and 7 rather than stopping at 52.
hexify_lonlat_to_index() documented itself as
vectorised over lon and lat and took one
point, and nothing that consumes an index string accepted the vector
cell_to_index() returns:
hexify_index_to_cell(),
hexify_index_to_lonlat(), hexify_get_parent(),
hexify_get_children(),
hexify_get_resolution(),
hexify_compare_indices() and
hexify_z7_canonical() each stopped on Rcpp’s own
scalar-coercion message, which named no hexify function and no argument.
Every index entry point takes vectors now and loops in C++, so a caller
pays one call rather than one per element, and a missing coordinate or
index carries through as a missing result. Arguments read in step whose
lengths differ are named, with the function they were passed
to.
hex_distance() over-reported the hop count between
cells of one quad, by up to three times, and at aperture 7 also
under-reported it by half. It read the difference of the two cells
straight off the substrate the cell IDs are packed on, which is the cell
lattice only some of the time: at aperture 3’s odd resolutions one
substrate point in three is a cell and the six neighbours stand
sqrt(3) substrate units apart, so a step was counted as
three. The handedness of the axial formula was wrong as well. The
difference is now divided by the lattice generator first, which reads it
in cell steps, and the six neighbours are the six units there. Every
adjacent pair of cells is one hop apart at apertures 3, 4 and 7, and the
distances agree with breadth-first search over
get_neighbors().
get_neighbors() and hex_distance() read
a grid’s aperture as a single number, which is NA for a
sequence spelling, so every grid built from one – "4/3",
"4/7" or a per-level vector – stopped on a coerced-aperture
message while every other operation on it worked. One walk now serves
both: it takes what a quad edge measures, which substrate lattice the
cells sit on, and how a quad coordinate maps to and from the plane, and
a pure aperture and a sequence differ only in those. A mixed grid has
the twelve cells with five neighbours and six everywhere else that a
pure one has, at every sequence and resolution tested, and its distances
agree with breadth-first search.
grid_clip(), hex_zonal() and
hex_extract() stopped on a grid built for another body,
with sf’s own st_crs(x) == st_crs(y) is not TRUE, which
names neither the function nor the boundary nor the radius that
separates them. A boundary and a grid on two bodies are now reconciled
in one place: two CRSs on the same body reproject as before, and where
the bodies differ, longitude and latitude name the same region on either
sphere, so a lon/lat boundary is read in the grid’s own CRS and the
caller is told which two CRSs met. A projected CRS carries lengths that
do not carry over, and is refused with both named. hexify()
read sf coordinates the same way, transforming them to WGS84 whatever
body the grid was on, and now reads them on the grid’s own.
The grid_clip() and plot_grid()
examples asked for France and got a third of the globe: the world map’s
France carries the overseas departements, and both functions cover the
bounding box of what they are given, which for all of France runs from
-62 to 56 degrees of longitude. That built 36707 cells to draw
metropolitan France. The examples crop to the metropolitan extent, which
is what they meant, and together they now take under a second rather
than fifty.
as_dggrid() on a grid built for another body
returned a dggs that read as an Earth grid, and said nothing. A dggs has
no field for the radius, so the conversion warns and names the radius
being dropped. from_dggrid() is the same boundary in
reverse and takes a radius_km argument, defaulting to
Earth, so a grid on another body round-trips.
hexify_heatmap() handled an RColorBrewer palette
only between three levels and the palette’s own length. A
breaks argument with ten or more bins passed fewer colours
than the scale needed and stopped in ggplot2, naming ggplot2 rather than
the palette; fewer than three bins let RColorBrewer’s own warning
through. The palette is now taken at exactly the number of levels asked
for, by interpolating over its full range past its length, and it
matches RColorBrewer::brewer.pal() colour for colour within
it.
A colors name in neither the viridis nor the
RColorBrewer family was passed to viridisLite, which drew the default
palette and warned about its own argument. It is an error now, naming
both families.
n_cells() had no HexGridInfo method, so
asking a grid how many cells it holds stopped on dispatch while printing
the grid reported exactly that number. Both read one function now. On a
HexData the count is still the number of distinct cells its
rows fall in.
tibble::as_tibble() on a HexData
reached tibble’s default method and stopped there, unable to coerce an
S4 object. as_tibble.HexData() was exported rather than
registered, so dispatch never found it. It is registered on tibble’s
generic now, and tibble stays in Suggests.
cell_to_sf() returned an empty polygon for every
cell whose boundary lay within two degrees of a pole, which from
resolution 4 at aperture 7 and resolution 5 at aperture 4 is each cell
nearest the poles. Corners inside that band were snapped onto the pole,
so once a cell was narrower than the band all of its corners landed on
one point. A pole falls inside a cell or on a cell edge, never on a
corner, and corners now keep the position the inverse projection gives
them.
grid_global() emitted GDAL errors and returned an
invalid geometry for the cells that cover a pole. The ring of such a
cell winds once around the globe, which no lon/lat ring closes, and
sf::st_wrap_dateline() joined the two halves it cut along a
line of constant latitude, across the cell’s own edges. The ring is now
cut at the antimeridian and carried up to the pole, the way a polar cap
is drawn in lon/lat.
A cell split at the antimeridian keeps its shape. The cut ends were joined along a line of constant latitude, which is not where the cell’s edge runs and moved as much as a tenth of the area of a cell at the seam. The ring now carries a corner on its own edge where it meets +/-180, and a cell covers the same area whether or not it is split.
Cell rings cross the antimeridian by carrying their longitudes on
continuously rather than by shifting the negative ones, which placed a
ring spanning exactly half the globe on both sides of the map at once.
Only cells reaching past +/-180 go to
sf::st_wrap_dateline(), which cuts any segment half a turn
wide and so also split the edge a cell runs over a pole.
lonlat_to_cell() on a mixed-aperture grid names a
cell for a point that sits on a quad corner, which is where the twelve
icosahedron vertices put the poles. The rotate/requantize chain can
leave the quad’s coordinate range by a tie-breaking unit there, and the
cell index reads the pair as an unsigned offset from the quad origin, so
such a point came back out of range and cell_to_index()
stopped on it.
The default ISEA orientation carries atan(phi) at
full double precision, with phi = (1 + sqrt(5)) / 2,
placing vertex 0 at latitude 58.282525588538995 degrees. The constant
was stored rounded at the eighth decimal, 1.5e-09 degrees from the
analytic value, which is 0.16 mm on Earth and reaches the assignment of
a point that close to a cell boundary.
cell_to_sf() on a mixed-aperture grid reads its
cells with the aperture sequence the grid was built from. The polygon
path took a single aperture, which a mixed sequence does not have, and
decoded every mixed grid as aperture 3: cell IDs past that grid’s range
stopped the call and the rest named different cells.
is_pentagon() read the same collapsed aperture and is on
the sequence path as well.
Aperture-7 hexagons carry the orientation of their own lattice, unrotated at even resolutions and turned 19.1 degrees at odd ones. The corners were placed 30 degrees off the lattice at every resolution, which aimed them at the neighbouring cell centres rather than the points between them. Corner positions now come from the grid form, so every aperture reads its rotation and scale from one place.
A cell at an icosahedral vertex drops the corner that falls in the icosahedron’s angular deficit on a rotated lattice too. The dropped corner was fixed to the unrotated layout, so aperture-7 pentagons and aperture-3 pentagons at odd resolutions kept a corner no neighbour reaches and dropped one they share.
hexify_cell_to_sf() returns the same boundary
whichever way return_sf is set. The data frame path was a
second implementation, without the pentagon handling, the fallback for a
corner outside its quad, or the extension of polar cells to the pole, so
it stopped on every pentagon and drew edge cells differently. Both paths
read one boundary routine now, and the data frame gives a pentagon six
rows rather than seven.
get_parent() and get_children() honour
levels on pure ISEA grids, where they moved one level
whatever was asked for.
get_children(get_parent(x)) contains x.
get_parent_index() stripped two levels off a Z3 or
aperture-3 z-order index whenever its digit count was even, and one
instead of two off an aperture-7 z-order index;
get_children_indices() rebuilt aperture-3 and aperture-4
children from a coordinate box rather than appending the child digit,
which is not the inverse of stripping one. Over every cell ID the round
trip goes from 4.3 per cent to 100 per cent for aperture 3 at resolution
2, and from an error to 98.2 per cent for aperture 7, where the cells
below a pentagon remain: a pentagon has six aperture-7 children, so a Z7
index does not name seven distinct descendants there.
quad_ij_to_cell() re-expresses a coordinate that has
stepped outside its quad in the quad that owns it, and returns
NA where the icosahedron folds at a vertex and no quad
does. Such a pair was packed as it stood, giving cell IDs past the end
of the grid.
The compiled entry points all carry the cpp_ prefix.
Five unprefixed duplicates at the foot of rcpp_index.cpp
had no callers, and compileAttributes() wrote each one into
R/RcppExports.R, where cell_to_index()
collided with the exported cell_to_index(cell_id, grid),
and which definition survived came down to the order R sources
R/.
crs takes any coordinate reference system sf reads:
an EPSG code as before, or a ‘PROJ’ or ‘WKT’ string. A grid left to its
default is read on its own body, so a grid built with
radius_km carries a longlat CRS on the sphere of that
radius and an Earth grid keeps EPSG:4326. EPSG codes name Earth
reference systems, so a grid on another body had no code to name it and
was reported in ‘WGS84’.
Every function that hands out coordinates reads the grid’s CRS
through grid_crs(), the way kilometres go through
grid_radius_km(). cell_to_sf(),
as_sf(), hex_summarize(geometry = TRUE) and
hex_extract() all return a grid’s own coordinates rather
than assuming Earth.
plot_globe() returns in about a second on the grids its
own examples use, where it took eleven minutes. Cells on the hemisphere
edge come out of the orthographic transform as rings of two points,
which GEOS rejects for the whole set, so every call fell through to a
per-cell repair loop that rewrote the whole table once per cell. Those
cells are now dropped before the repair, which lets the batch call
through, and the loop that remains as a fallback assembles its
geometries once. The cells the function returns are unchanged.hexify_heatmap() documents the basemap formats it
takes, including the rasters it already accepted, and names
sf::st_read() and terra::rast() as the way a
file becomes one.
The quickstart says what an sf object is where it first uses one,
and the bodies section of vignette("workflows") covers
coordinates on another body.
Reported by Christian Carey.
Aperture-7 cell IDs and Z7 index strings change. The IDs now span
[0, 7^res) within a quad and the index carries the
hierarchy seed in its leading field, both of which the Bug fixes below
explain. Aperture-7 IDs and indices stored from an earlier version do
not name the same cells and need regenerating from the coordinates.
Aperture-3 and aperture-4 cell IDs are unchanged.
hexify_assign() assigns about a third of points to a
different cell, at every aperture and effective resolution, because the
quantizer built its cube triple from the wrong pair of axes. The cell it
used to name sits 0.68 to 0.87 cell spacings from the point, further
than the circumradius, so the point fell outside it. Its id
is now the Z3 index string and its face the quad, and the
match_dggrid_parity argument is gone; it was never wired to
an effect.
cell_to_lonlat() returns the centres of the two
vertex-quad pentagons at (11.25, 58.28) and (-168.75, -58.28), where
those cells are under the ISEA default orientation, rather than at the
geographic poles.
Grids cover any body, not just Earth (#56, requested by Christian
Carey). hex_grid(radius_km = ) takes a radius in kilometres
or a body name – "mars", "moon",
"titan", "europa" and thirteen others, at the
IAU mean radii (Archinal et al. 2018) tabulated by JPL Solar System
Dynamics. The grid object carries the radius, so everything reporting
kilometres follows it: dgearthstat(),
cell_area(), hexify_compare_resolutions(), the
sf exports, and the resolution a target area_km2 picks.
Cell geometry is angular and unchanged – a coordinate lands in the same
cell on every body – and Earth remains the default, sized against the
WGS84 ellipsoid area as before.
Both backends take a radius. H3 reports a cell’s area as its
solid angle times Earth’s radius squared, so another radius scales those
areas by the square of the radius ratio, exactly;
cell_area(), dgearthstat(),
hexify_compare_resolutions(type = "h3"), the resolution an
area_km2 picks and h3_crosswalk() all follow.
An H3 cell ID names a position in H3’s topology, which Uber’s H3 reads
on Earth, so the IDs of a grid on another body are that topology on that
body and are not interchangeable with Earth H3 data;
hex_grid() says so once per session and
h3_crosswalk() needs both grids on the same body.
hex_grid() takes any aperture sequence, so mixed
grids beyond ISEA43H are reachable from R (#57). A family name splits
the levels in two, as "4/3" already did –
"4/7", "7/4", "3/7" – and a
vector names one aperture per resolution level,
aperture = c(4, 4, 7, 3). Cell IDs, centres, neighbours,
the geometric hierarchy and the resolution-for-area inversion all follow
the sequence. The cell count of a sequence is
10 * prod(apertures) + 2.
Cell IDs pack onto the substrate sublattice of any grid form. A
sequence with an odd number of aperture-7 levels leaves the cells on a
lattice of norm 7 or 21 rather than the norm-1 or norm-3 lattices pure
apertures give; all four are now the single congruence
j = c * i (mod N), which the aligned and Class II packings
turn out to be the N = 1 and N = 3 cases
of.
Mixed aperture sequences accept aperture 7 alongside 3 and 4, in
any order (#55, requested by Christian Carey). Scale and lattice
orientation now come from one model shared by the pure-aperture and
mixed-sequence code: refining by aperture a multiplies the
lattice generator by an Eisenstein integer of norm a
(1 + w for 3, 2 for 4, 2 + w for
7), so orientation is the product of the steps taken rather than a
per-level Class I/II flag. The C++ entry points are now
hex_quantize_mixed(), hex_center_mixed() and
hex_corners_mixed(), replacing the _ap34
names, which no longer describe what they accept.
Aperture-7 cells now round-trip through their Z7 index (#53). A
cell was encoded by walking it up the aperture-7 hierarchy to resolution
0, and decoded by walking the digits back down from the origin, but the
walk does not arrive at the origin for every cell: a quad is a rhombus
while the aperture-7 parents are hexagons, so the quad boundary cuts
through the parents of the cells along it, and those arrive at one of
the six neighbours of the origin instead. Two thirds of all cells do,
and the encoder discarded where it landed, so
cell_to_index() followed by cell_to_quad_ij()
returned the starting cell for only 33 to 54 per cent of them. The index
now carries the arrival point in its leading field as
quad + 12 * seed, with seed one of the seven
unit digits, and decoding seeds the walk from it. Every cell round-trips
at resolutions 1 to 9, distinct cells keep distinct indices, and the
index length is unchanged at 2 + resolution. A cell whose
ancestry stays inside its quad has seed = 0 and keeps the
plain two-digit quad, so the two cells DGGRID’s own encoder merges onto
0045310 now read 2752310 and
2545310. Read the quad from an index with
BB %% 12 or hexify_index_to_cell(). A
coordinate that lies outside the quad it is given is now rejected rather
than encoded to a string that names a different cell.
Aperture-7 cell IDs ran outside the documented
[1, 10 * 7^res + 2] range and
validate_cell_id() rejected them: a uniform sample at
resolution 2 reached 2647 against a nominal count of 492, and at
resolutions 1 and 2 the IDs also overran the padded space the encoder
itself allotted, so cell_to_quad_ij() errored on ids the
assignment had just produced. Aperture 7 stored each cell in a square
bounding box sized by a fitted constant, roughly three times the cell
count, because the surrogate lattice sits at 19.1 degrees to the quad
frame. Cells now index by walking the quad’s own substrate box, where
the centres are the sublattice 2u + v = 0 (mod 7) and a row
holds one seventh of the box, so the IDs of a quad span
[0, 7^res) with no gaps and the cell count is
10 * 7^res + 2 for every aperture.
Aperture 7 gave the same cell two addresses at odd resolutions,
once from each quad an edge cell straddles: a uniform sample at
resolution 1 produced 122 distinct (quad, i, j) for a grid
of 72 cells. One cell there covers seven substrate points, and the quad
was decided from the sampled point rather than the cell, so a cell whose
points fall on both sides of an edge belonged to both. The cell centre
now picks the quad, which puts each cell in exactly one: every ID in
[1, 10 * 7^res + 2] names one distinct cell and round-trips
through cell_to_quad_ij().
ISEA neighbour offsets stepped along the wrong lattice
directions. The aligned lattice writes (i, j) as
x = i - j/2, so (-1, 1) and
(1, -1) stand sqrt(3) units away rather than
one, and aperture 3’s odd resolutions carry cells on the 30 degree
lattice, where neighbours are sqrt(3) substrate units
apart. Aperture 3 at resolution 5 returned as few as two neighbours for
a cell and one ID past the end of the grid; apertures 3 and 4 reported
second-ring cells, at 1.6 to 2.4 times the cell spacing, as neighbours
of the 12 pentagons. A neighbour that under-runs its quad’s frame has no
image in it and used to be dropped; it now steps to the owning quad
through DGGRID’s edge table. Both poles are read from an adjacent quad’s
corner, where the offsets reach the five surrounding cells, instead of
from their own single-cell frame, which the south pole overran into IDs
past the end of the grid and the north pole left empty. Resolution 0,
where all 12 cells are pentagons and each quad holds one cell, reads the
icosahedron’s vertex graph. Every aperture at resolutions 0-4 now
reports exactly 12 pentagons, hexagons elsewhere, a symmetric adjacency,
and no neighbour centre further than 1.12 cell spacings.
cell_to_lonlat() reported the two vertex-quad
pentagons (quads 0 and 11) at the geographic poles (#58). Those cells
sit at icosahedron vertex 0 and its antipode, which are the poles only
under a pole-aligned orientation; under the ISEA default they are at
(11.25, 58.28) and (-168.75, -58.28), about 3500 km away. Both now come
from folding the cell through the quad frame, as the polygon and
mixed-sequence paths already did, so a point assigned to one gets its
own cell’s centre back. index_to_cell()’s inverse carried
the same hardcoded pair and is fixed with it.
hexify_assign() returned centres thousands of
kilometres outside the assigned cell for a share of points – 113 of 400
uniformly sampled points further than two centre spacings at effective
resolution 2 (#58). It was the one path that quantized against raw
triangle-local coordinates instead of folding into the non-negative quad
frame, so it needed its own Z3 digit scheme (magnitude digits plus two
trailing sign digits) whose round trip through
z3::encode()/decode() did not preserve
(i, j). It now runs on the same pipeline as
hexify(): lonlat_to_cell() for the cell,
cell_to_lonlat() for the centre,
cell_to_index() for the ID. Every sampled point is now
within one circumradius of its cell centre at effective resolutions 1-8.
The reported id is the Z3 index string, face
is the quad (0-11), and the match_dggrid_parity argument is
gone – it was never wired to an effect, and the shared pipeline is the
DGGRID-verified one. cpp_hex_index_z3_*() are removed with
the scheme they served.
Quantization built the cube triple (i, j, -i-j) from
the grid coordinates. The i and j axes point
at 0 and 120 degrees, so they are two of the three cube axes rather than
the adjacent pair round-and-fix expects, and the step that corrects the
coordinate with the largest rounding error corrected the wrong one:
against brute-force nearest-centre over 3000 points, the quantizer
returned a cell other than the nearest for 1026 of them. The triple is
(i - j, j, -i), which now matches brute-force
nearest-centre on every sampled point. This reached
hex_quantize_ap3/4/7(), the mixed-sequence quantizer and
hexify_assign(), whose cell assignment changes for about a
third of points; the quantization is scale-free, so the share is the
same at every effective resolution. Cell IDs and hierarchical indices
are unaffected – they quantize through quad_xy_to_ij(),
which uses the exact region-classifying quantizer.
The aperture-7 grids returned by the hex_*_ap7()
helpers were not centre-nested: each level applied a fixed
kAp7RotDeg offset plus a 30-degree alternation, which is
not an aperture-7 refinement (the norm-7 Eisenstein generators sit at
+/- kAp7RotDeg, not 30 degrees), so a cell centre was about
0.85 of a child cell spacing away from the nearest centre one level
down. Levels now alternate the two norm-7 generators, putting even
resolutions at 0 degrees and odd resolutions at kAp7RotDeg
on a sqrt(7) substrate – the same convention as the
exact-integer aperture-7 route used for cell IDs, whose divisor is
7^ceil(res/2).
Mixed sequences computed orientation from the count of aperture-3
levels including the base level, and treated any sequence ending in
aperture 4 as unrotated. A sequence such as c(4, 4, 3) was
therefore 30 degrees away from the grid it describes, and
c(4, 3, 4) was not nested in c(4, 3).
The aperture-7 inverse in quad_ij_to_xy() scaled by
a sqrt(7)/sqrt(21) substrate while the forward
quantization used the exact 7^ceil(res/2) one. Callers that
reached it – hexify_index_to_lonlat(),
hexify_quad_ij_to_xy() and
hexify_quad_ij_to_icosa_tri() – placed aperture-7 cells in
the wrong location, or outside the quad entirely, where the conversion
then failed. DGGRID’s DgHexGrid2DS toggles Class III on
every aperture-7 level, so even resolutions are an unrotated Class I
grid and odd ones carry one aperture-7 level; the inverse now takes the
same exact-integer route as the forward. The float-rotation Class III
helpers that encoded the old substrate (quantize_class3i(),
quantize_class3ii(),
substrate_to_surrogate_ap7(),
surrogate_to_substrate_ap7()) were unused and have been
removed.
hexify_index_to_lonlat() returned the icosahedron
vertex instead of the pole for cells in the polar pentagon quads, at
every aperture. It now answers those directly, as
cell_to_lonlat() does.
kAp7RotDeg was mislabeled as
atan(sqrt(3/7)) (~33.2 deg) and its numeric literal drifted
from the true value starting at the 12th significant digit, present
since the initial release. The correct closed form is
atan(sqrt(3)/5), now cross-checked against DGGRID’s
M_AP7_ROT_DEGS and updated to full double precision. A
duplicate literal in coordinate_transforms.cpp was removed
in favor of deriving from the same constant (reported by Christian
Carey)."4/3" (ISEA43H) grids: get_parent(),
get_children(), and cell_to_index() no longer
error on these grids (#31). Mixed 4/3 grids have no DGGRID-standard
hierarchical index, so hexify defines the hierarchy geometrically – a
cell’s parent is the coarser cell whose lattice point contains the
cell’s centre. This is the correct relationship for how these grids
quantise (a single scaled quantisation per resolution, not a
step-composed subdivision), and it round-trips
cell -> cell_to_index() -> cell exactly, including
the seam and icosahedron-vertex cells that a purely local walk would
miss.Bug fixes, package hygiene, and test coverage
hexify_lonlat_to_index()/hexify_index_to_lonlat()
for aperture 3: quantization skipped the quad-frame fold that aperture
4/7 already had, producing indices inconsistent with
lonlat_to_cell()/cell_to_index() for
essentially all points.is_pentagon() for ISEA aperture 7, which
undercounted or threw at resolution >= 1; it now decodes each cell’s
own (i, j) instead of relying on an unreliable forward computation.hex_compact() emitting duplicate cell IDs when
the input already contained a parent cell alongside all 7 of its
children.hexify_assign()’s Z3 backend discarding the sign
of quantized (i, j), causing sign-flipped points to collide on the same
cell ID.snyder_forward()’s sort comparator and silently returning
garbage from hex quantization instead of erroring.cell_to_index()/get_parent()/get_children()
no longer produce a confusing internal bound error (or, with a naive
fix, silently wrong cell IDs) for mixed aperture "4/3"
grids. In 0.7.3 these raised a clear “not implemented” error; 0.7.4
implements the navigation geometrically (see above).hex_extract() now respects
cells=/boundary= when grid is a
HexData object (previously silently ignored).hex_browse()’s data.frame input mode no longer crashes
on duplicate cell_id rows.plot_globe()’s resolve_center() now
validates a named center vector’s names instead of silently
building an invalid PROJ string.hexify_cell_to_index()
and siblings) now validate resolution/aperture
like hexify_lonlat_to_cell() already did; the C++ layer now
enforces the max resolution for aperture 3/4 the same way it already did
for aperture 7.as_dggrid() now accepts a modern
HexGridInfo object, matching dgverify().HexData’s validity check no longer has a blind spot for
empty cell_id/cell_center paired with
non-empty data.hex_grid() and legacy hexify_grid() now
give clear errors for non-numeric/NA/non-positive
resolution, crs, or area, instead
of base-R errors or a silent resolution = NaN.plot_globe(exclude_antarctica = TRUE) now warns instead
of silently no-op’ing for custom land_data without a
recognized country-name column.import_h3() now drops NA cell IDs (with a warning) when
data is attached, matching hexify()’s existing
NA-coordinate handling.prepare_fill_column() now warns when
breaks= is supplied for a discrete value column instead of
silently discarding it.index_to_cell_internal()), an
orphaned test fixture, and a committed rendered vignette; fixed a
native-pipe usage that required a newer R than the package declares;
fixed stale roxygen text; documented HexData[’s
drop = FALSE default explicitly.malloc/free digit
buffer with std::vector; an out-of-range digit now throws
instead of silently clamping; added a resolution/string-length
consistency check to z3::decode();
get_children_indices() no longer swallows unrelated
exceptions behind a catch-all around the max-resolution boundary
check.hexify_heatmap() tests that previously
only checked the return type is a ggplot object to assert on the actual
plot data/mapping (fill column, scale type, colors, bins, limits,
labels).Raster*/SpatRaster
basemap path and for hex_browse()’s value-to-fill-color
mapping.cpp_hex_index_z3_quantize_digits(),
cpp_hex_index_z3_center(), and the Z3 corners helper, which
previously returned placeholder zeros instead of real quantized
digits/coordinates.hex_zonal() row misalignment: results are now
keyed off the deduplicated hex_sf$cell_id order instead of
the pre-dedup input, and cells input now drops
NA/duplicate values before lookup.grid_clip() argument order in
hex_zonal()’s boundary path.Code quality and documentation
rcpp_aperture.cpp: extracted parameterized
helpers for quantize/center/corners, eliminating copy-paste across 3
apertures.validate_aperture() / validate_resolution()
from constants.R.is_pentagon() for ISEA grids: computes
pentagon cell IDs directly from quad coordinates instead of looping
through the lon/lat pipeline.hexify(): rejects
unexpected values with an informative error instead of silently coercing
via as.integer().theme_minimal() calls in plot methods with
.theme_clean() helper.test-edge-cases.R with 32 tests covering poles,
antimeridian, dateline, equator, roundtrip stability, pentagon
invariants, neighbor symmetry, and resolution-0 cell counts.Spatial analysis primitives
max_cell_id() for aperture 7 bounding box.get_neighbors(): returns k-ring (disk) of
neighboring cells for both ISEA and H3 grids. Supports
k > 1 for multi-ring expansion,
distances = TRUE for ring distance output, and vectorized
cell input. ISEA backend uses axial coordinate offsets with lon/lat
fallback for cross-quad boundaries. H3 backend uses vendored
gridDisk / gridDiskDistances /
gridRingUnsafe.hex_summarize(): cell-level data aggregation with
tidyeval support. Groups by cell_id, applies user-defined summary
expressions, returns a data.frame with cell centers, areas, and point
counts. Supports geometry = TRUE for sf output.cpp_h3_gridDisk,
cpp_h3_gridDiskDistances,
cpp_h3_gridRingUnsafe, cpp_get_neighbors_isea,
cpp_get_neighbors_z7h3Assert.c (clang 21
-Wempty-translation-unit).o) from source tarball
that caused installation failure on Linux (Debian) and NOTE on all
platformsplot_globe() examples in
\donttest{} to reduce check time (was 608s on
win-builder)cell_to_sf() now applies
sf::st_wrap_dateline() automatically, fixing horizontal
streaks on flat map projections (Plate Carrée, Robinson, etc.) for
hexagons crossing the ±180° antimeridianas_sf(x, geometry = "polygon") now routes all grids
(ISEA and H3) through cell_to_sf(), ensuring consistent
antimeridian handlinghexify_cell_to_sf() gains antimeridian normalization
matching cell_to_sf()st_wrap_dateline()
callsNative H3 backend — zero external dependencies
h3o R
package dependencyhexify()h3_lat_lng_to_cell,
h3_cell_to_boundary, h3_cell_to_parent,
h3_cell_to_children, h3_polygon_to_cells,
h3_cell_area_km2, h3_cell_to_lat_lngaperture is passed with
type = "h3" (ignored parameter)hex_grid()
documentationh3_crosswalk() example to H3 vignetteISEA–H3 crosswalk and per-cell area
h3_crosswalk(): bidirectional mapping between ISEA
and H3 cell IDs, with automatic resolution matching and per-cell area
comparisoncell_area(): returns geodesic area (km²) for each
cell — constant for ISEA (equal-area), location-dependent for H3, with
session-scoped caching$cell_area_km2,
[["cell_area_km2"]], and as.data.frame() now
return per-cell areas for H3 grids instead of the grid-wide averageclosest_h3_resolution() helper
shared by hex_grid() and h3_crosswalk()H3 grid support
hex_grid(resolution = 8, type = "h3")hexify(),
cell_to_sf(), grid_rect(),
grid_global(), grid_clip(),
get_parent(), get_children()h3o package (Suggests, not
required for ISEA workflows)hexify_compare_resolutions(type = "h3") for H3
resolution tabledgearthstat() now accepts HexGridInfo objects
directlygrid_type slot on HexGridInfo: "isea"
(default) or "h3"cell_id slot supports character (H3) and
numeric (ISEA) cell IDsHotfix for geometry issues
st_wrap_dateline()grid_global() to include
cells above ±85° latitudeHotfix for CRAN UBSAN check failure
as_dggrid(),
from_dggrid())