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())