Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
101 changes: 52 additions & 49 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,55 +82,58 @@ func ExampleLatLngToCell() {

## Bindings

| C API | Go API |
| ---------------------------- | -------------------------------------------------- |
| `latLngToCell` | `LatLngToCell`, `LatLng#Cell` |
| `cellToLatLng` | `CellToLatLng`, `Cell#LatLng` |
| `cellToBoundary` | `CellToBoundary`, `Cell#Boundary` |
| `gridDisk` | `GridDisk`, `Cell#GridDisk` |
| `gridDiskDistances` | `GridDiskDistances`, `Cell#GridDiskDistances` |
| `gridRingUnsafe` | N/A |
| `polygonToCells` | `PolygonToCells`, `GeoPolygon#Cells` |
| `cellsToMultiPolygon` | `CellsToMultiPolygon`
| `degsToRads` | `DegsToRads` |
| `radsToDegs` | `RadsToDegs` |
| `greatCircleDistance` | `GreatCircleDistance* (3/3)` |
| `getHexagonAreaAvg` | `HexagonAreaAvg* (3/3)` |
| `cellArea` | `CellArea* (3/3)` |
| `getHexagonEdgeLengthAvg` | `HexagonEdgeLengthAvg* (2/2)` |
| `exactEdgeLength` | `EdgeLength* (3/3)` |
| `getNumCells` | `NumCells` |
| `getRes0Cells` | `Res0Cells` |
| `getPentagons` | `Pentagons` |
| `getResolution` | `Resolution` |
| `getBaseCellNumber` | `BaseCellNumber`, `Cell#BaseCellNumber` |
| `stringToH3` | `IndexFromString`, `Cell#UnmarshalText` |
| `h3ToString` | `IndexToString`, `Cell#String`, `Cell#MarshalText` |
| `isValidCell` | `Cell#IsValid` |
| `cellToParent` | `Cell#Parent`, `Cell#ImmediateParent` |
| `cellToChildren` | `Cell#Children` `Cell#ImmediateChildren` |
| `cellToCenterChild` | `Cell#CenterChild` |
| `compactCells` | `CompactCells` |
| `uncompactCells` | `UncompactCells` |
| `isResClassIII` | `Cell#IsResClassIII` |
| `isPentagon` | `Cell#IsPentagon` |
| `getIcosahedronFaces` | `Cell#IcosahedronFaces` |
| `areNeighborCells` | `Cell#IsNeighbor` |
| `cellsToDirectedEdge` | `Cell#DirectedEdge` |
| `isValidDirectedEdge` | `DirectedEdge#IsValid` |
| `getDirectedEdgeOrigin` | `DirectedEdge#Origin` |
| `getDirectedEdgeDestination` | `DirectedEdge#Destination` |
| `directedEdgeToCells` | `DirectedEdge#Cells` |
| `originToDirectedEdges` | `Cell#DirectedEdges` |
| `directedEdgeToBoundary` | `DirectedEdge#Boundary` |
| `cellToVertex` | `CellToVertex` |
| `cellToVertexes` | `CellToVertexes` |
| `vertexToLatLng` | `VertexToLatLng` |
| `isValidVertex` | `IsValidVertex` |
| `gridDistance` | `GridDistance`, `Cell#GridDistance` |
| `gridPathCells` | `GridPath`, `Cell#GridPath` |
| `cellToLocalIj` | `CellToLocalIJ` |
| `localIjToCell` | `LocalIJToCell` |
| C API | Go API |
|------------------------------|-----------------------------------------------------------|
| `latLngToCell` | `LatLngToCell`, `LatLng#Cell` |
| `cellToLatLng` | `CellToLatLng`, `Cell#LatLng` |
| `cellToBoundary` | `CellToBoundary`, `Cell#Boundary` |
| `gridDisk` | `GridDisk`, `Cell#GridDisk` |
| `gridDisksUnsafe` | `GridDisksUnsafe` |
| `gridDiskDistances` | `GridDiskDistances`, `Cell#GridDiskDistances` |
| `gridDiskDistancesSafe` | `GridDiskDistancesSafe`, `Cell#GridDiskDistancesSafe` |
| `gridDiskDistancesUnsafe` | `GridDiskDistancesUnsafe`, `Cell#GridDiskDistancesUnsafe` |
| `gridRingUnsafe` | `GridRingUnsafe`, `Cell#GridRingUnsafe` |
| `polygonToCells` | `PolygonToCells`, `GeoPolygon#Cells` |
| `cellsToMultiPolygon` | `CellsToMultiPolygon` |
| `degsToRads` | `DegsToRads` |
| `radsToDegs` | `RadsToDegs` |
| `greatCircleDistance` | `GreatCircleDistance* (3/3)` |
| `getHexagonAreaAvg` | `HexagonAreaAvg* (3/3)` |
| `cellArea` | `CellArea* (3/3)` |
| `getHexagonEdgeLengthAvg` | `HexagonEdgeLengthAvg* (2/2)` |
| `exactEdgeLength` | `EdgeLength* (3/3)` |
| `getNumCells` | `NumCells` |
| `getRes0Cells` | `Res0Cells` |
| `getPentagons` | `Pentagons` |
| `getResolution` | `Resolution` |
| `getBaseCellNumber` | `BaseCellNumber`, `Cell#BaseCellNumber` |
| `stringToH3` | `IndexFromString`, `Cell#UnmarshalText` |
| `h3ToString` | `IndexToString`, `Cell#String`, `Cell#MarshalText` |
| `isValidCell` | `Cell#IsValid` |
| `cellToParent` | `Cell#Parent`, `Cell#ImmediateParent` |
| `cellToChildren` | `Cell#Children` `Cell#ImmediateChildren` |
| `cellToCenterChild` | `Cell#CenterChild` |
| `compactCells` | `CompactCells` |
| `uncompactCells` | `UncompactCells` |
| `isResClassIII` | `Cell#IsResClassIII` |
| `isPentagon` | `Cell#IsPentagon` |
| `getIcosahedronFaces` | `Cell#IcosahedronFaces` |
| `areNeighborCells` | `Cell#IsNeighbor` |
| `cellsToDirectedEdge` | `Cell#DirectedEdge` |
| `isValidDirectedEdge` | `DirectedEdge#IsValid` |
| `getDirectedEdgeOrigin` | `DirectedEdge#Origin` |
| `getDirectedEdgeDestination` | `DirectedEdge#Destination` |
| `directedEdgeToCells` | `DirectedEdge#Cells` |
| `originToDirectedEdges` | `Cell#DirectedEdges` |
| `directedEdgeToBoundary` | `DirectedEdge#Boundary` |
| `cellToVertex` | `CellToVertex` |
| `cellToVertexes` | `CellToVertexes` |
| `vertexToLatLng` | `VertexToLatLng` |
| `isValidVertex` | `IsValidVertex` |
| `gridDistance` | `GridDistance`, `Cell#GridDistance` |
| `gridPathCells` | `GridPath`, `Cell#GridPath` |
| `cellToLocalIj` | `CellToLocalIJ` |
| `localIjToCell` | `LocalIJToCell` |

## CGO

Expand Down
119 changes: 119 additions & 0 deletions h3.go
Original file line number Diff line number Diff line change
Expand Up @@ -230,7 +230,35 @@ func (c Cell) GridDisk(k int) ([]Cell, error) {
return GridDisk(c, k)
}

// GridDisksUnsafe produces cells within grid distance k of all provided origin
// cells.
//
// k-ring 0 is defined as the origin cell, k-ring 1 is defined as k-ring 0 and
// all neighboring cells, and so on.
//
// Outer slice is ordered in the same order origins were passed in. Inner slices
// are in no particular order.
//
// This does not call through to the underlying C.gridDisksUnsafe implementation
// as it is slightly easier to do so to avoid unnecessary type conversions.
Comment thread
jogly marked this conversation as resolved.
func GridDisksUnsafe(origins []Cell, k int) ([][]Cell, error) {
out := make([][]Cell, len(origins))
gridDiskSize := maxGridDiskSize(k)
for i := range origins {
inner := make([]C.H3Index, gridDiskSize)
errC := C.gridDiskUnsafe(C.H3Index(origins[i]), C.int(k), &inner[0])
if err := toErr(errC); err != nil {
return nil, err
}
out[i] = cellsFromC(inner, true, false)
}
return out, nil
}

// GridDiskDistances produces cells within grid distance k of the origin cell.
// This method optimistically tries the faster GridDiskDistancesUnsafe first.
// If a cell was a pentagon or was in the pentagon distortion area, it falls
// back to GridDiskDistancesSafe.
//
// k-ring 0 is defined as the origin cell, k-ring 1 is defined as k-ring 0 and
// all neighboring cells, and so on.
Expand Down Expand Up @@ -260,6 +288,9 @@ func GridDiskDistances(origin Cell, k int) ([][]Cell, error) {
}

// GridDiskDistances produces cells within grid distance k of the origin cell.
// This method optimistically tries the faster GridDiskDistancesUnsafe first.
// If a cell was a pentagon or was in the pentagon distortion area, it falls
// back to GridDiskDistancesSafe.
//
// k-ring 0 is defined as the origin cell, k-ring 1 is defined as k-ring 0 and
// all neighboring cells, and so on.
Expand All @@ -271,6 +302,94 @@ func (c Cell) GridDiskDistances(k int) ([][]Cell, error) {
return GridDiskDistances(c, k)
}

// GridDiskDistancesUnsafe produces cells within grid distance k of the origin cell.
// Output behavior is undefined when one of the cells returned by this
// function is a pentagon or is in the pentagon distortion area.
//
// k-ring 0 is defined as the origin cell, k-ring 1 is defined as k-ring 0 and
// all neighboring cells, and so on.
//
// Outer slice is ordered from origin outwards. Inner slices are in no
// particular order. Elements of the output array may be left zero, as can
// happen when crossing a pentagon.
func GridDiskDistancesUnsafe(origin Cell, k int) ([][]Cell, error) {
rsz := maxGridDiskSize(k)
outHexes := make([]C.H3Index, rsz)
outDists := make([]C.int, rsz)

if err := toErr(C.gridDiskDistancesUnsafe(C.H3Index(origin), C.int(k), &outHexes[0], &outDists[0])); err != nil {
return nil, err
}

ret := make([][]Cell, k+1)
for i := 0; i <= k; i++ {
ret[i] = make([]Cell, 0, ringSize(i))
}

for i, d := range outDists {
ret[d] = append(ret[d], Cell(outHexes[i]))
}

return ret, nil
}

// GridDiskDistancesUnsafe produces cells within grid distance k of the origin cell.
// Output behavior is undefined when one of the cells returned by this
// function is a pentagon or is in the pentagon distortion area.
//
// k-ring 0 is defined as the origin cell, k-ring 1 is defined as k-ring 0 and
// all neighboring cells, and so on.
//
// Outer slice is ordered from origin outwards. Inner slices are in no
// particular order. Elements of the output array may be left zero, as can
// happen when crossing a pentagon.
func (c Cell) GridDiskDistancesUnsafe(k int) ([][]Cell, error) {
return GridDiskDistancesUnsafe(c, k)
}

// GridDiskDistancesSafe produces cells within grid distance k of the origin cell.
// This is the safe, but slow version of GridDiskDistances.
//
// k-ring 0 is defined as the origin cell, k-ring 1 is defined as k-ring 0 and
// all neighboring cells, and so on.
//
// Outer slice is ordered from origin outwards. Inner slices are in no
// particular order. Elements of the output array may be left zero, as can
// happen when crossing a pentagon.
func GridDiskDistancesSafe(origin Cell, k int) ([][]Cell, error) {
rsz := maxGridDiskSize(k)
outHexes := make([]C.H3Index, rsz)
outDists := make([]C.int, rsz)

if err := toErr(C.gridDiskDistancesSafe(C.H3Index(origin), C.int(k), &outHexes[0], &outDists[0])); err != nil {
return nil, err
}

ret := make([][]Cell, k+1)
for i := 0; i <= k; i++ {
ret[i] = make([]Cell, 0, ringSize(i))
}

for i, d := range outDists {
ret[d] = append(ret[d], Cell(outHexes[i]))
}

return ret, nil
}

// GridDiskDistancesSafe produces cells within grid distance k of the origin cell.
// This is the safe, but slow version of GridDiskDistances.
//
// k-ring 0 is defined as the origin cell, k-ring 1 is defined as k-ring 0 and
// all neighboring cells, and so on.
//
// Outer slice is ordered from origin outwards. Inner slices are in no
// particular order. Elements of the output array may be left zero, as can
// happen when crossing a pentagon.
func (c Cell) GridDiskDistancesSafe(k int) ([][]Cell, error) {
return GridDiskDistancesSafe(c, k)
}

// GridRing produces the "hollow" ring of cells at exactly grid distance k from the origin cell.
//
// k-ring 0 returns just the origin hexagon.
Expand Down
98 changes: 96 additions & 2 deletions h3_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,53 @@ func TestGridDisk(t *testing.T) {
})
}

func TestGridDisksUnsafe(t *testing.T) {
t.Parallel()

t.Run("two cells", func(t *testing.T) {
t.Parallel()

gds, err := GridDisksUnsafe([]Cell{validCell, validCell}, len(validDiskDist3_1)-1)
assertNoErr(t, err)
assertEqual(t, 2, len(gds), "expected grid disks to have two arrays returned")
assertEqualDisks(t,
flattenDisks(validDiskDist3_1),
gds[0],
"expected grid disks[0] to be the same",
)
assertEqualDisks(t,
flattenDisks(validDiskDist3_1),
gds[1],
"expected grid disks[1] to be the same",
)
})

t.Run("pentagon", func(t *testing.T) {
t.Parallel()

_, err := GridDisksUnsafe([]Cell{pentagonCell}, 1)
assertErr(t, err)
assertErrIs(t, err, ErrPentagon)
})

t.Run("invalid cell", func(t *testing.T) {
t.Parallel()

c := Cell(-1)
_, err := GridDisksUnsafe([]Cell{c}, 1)
assertErr(t, err)
assertErrIs(t, err, ErrCellInvalid)
})

t.Run("invalid k", func(t *testing.T) {
t.Parallel()

_, err := GridDisksUnsafe([]Cell{validCell}, -1)
assertErr(t, err)
assertErrIs(t, err, ErrDomain)
})
}

func TestGridDiskDistances(t *testing.T) {
t.Parallel()

Expand All @@ -211,6 +258,10 @@ func TestGridDiskDistances(t *testing.T) {
rings, err := validCell.GridDiskDistances(len(validDiskDist3_1) - 1)
assertNoErr(t, err)
assertEqualDiskDistances(t, validDiskDist3_1, rings)

rings, err = validCell.GridDiskDistancesSafe(len(validDiskDist3_1) - 1)
assertNoErr(t, err)
assertEqualDiskDistances(t, validDiskDist3_1, rings)
})

t.Run("pentagon centered", func(t *testing.T) {
Expand All @@ -220,6 +271,11 @@ func TestGridDiskDistances(t *testing.T) {
assertNoErr(t, err)
assertEqual(t, 2, len(rings), "expected 2 rings")
assertEqual(t, 5, len(rings[1]), "expected 5 cells in second ring")

rings, err = GridDiskDistancesSafe(pentagonCell, 1)
assertNoErr(t, err)
assertEqual(t, 2, len(rings), "expected 2 rings")
assertEqual(t, 5, len(rings[1]), "expected 5 cells in second ring")
})
})

Expand All @@ -228,6 +284,38 @@ func TestGridDiskDistances(t *testing.T) {
assertErr(t, err)
assertErrIs(t, err, ErrDomain)
assertNil(t, rings)

rings, err = GridDiskDistancesSafe(pentagonCell, -1)
assertErr(t, err)
assertErrIs(t, err, ErrDomain)
assertNil(t, rings)
})
}

func TestGridDiskDistancesUnsafe(t *testing.T) {
t.Parallel()

t.Run("no pentagon", func(t *testing.T) {
t.Parallel()
rings, err := validCell.GridDiskDistancesUnsafe(len(validDiskDist3_1) - 1)
assertNoErr(t, err)
assertEqualDiskDistances(t, validDiskDist3_1, rings)
})

t.Run("pentagon centered", func(t *testing.T) {
t.Parallel()
assertNoPanic(t, func() {
_, err := GridDiskDistancesUnsafe(pentagonCell, 1)
assertErr(t, err)
assertErrIs(t, err, ErrPentagon)
})
})

t.Run("invalid k-ring", func(t *testing.T) {
rings, err := GridDiskDistancesUnsafe(pentagonCell, -1)
assertErr(t, err)
assertErrIs(t, err, ErrDomain)
assertNil(t, rings)
})
}

Expand Down Expand Up @@ -1354,11 +1442,13 @@ func assertEqualDiskDistances(t *testing.T, expected, actual [][]Cell) {
}
}

func assertEqualDisks(t *testing.T, expected, actual []Cell) {
func assertEqualDisks(t *testing.T, expected, actual []Cell, msgAndArgs ...any) {
t.Helper()

if len(expected) != len(actual) {
t.Errorf("disk size mismatch: %v != %v", len(expected), len(actual))
logMsgAndArgs(t, msgAndArgs...)

return
}

Expand All @@ -1370,9 +1460,9 @@ func assertEqualDisks(t *testing.T, expected, actual []Cell) {
for i, cell := range expected {
if cell != actual[i] {
t.Errorf("cell[%d]: %v != %v", i, cell, actual[i])
logMsgAndArgs(t, msgAndArgs...)

count++

if count > 5 {
t.Logf("... and more")
break
Expand Down Expand Up @@ -1516,3 +1606,7 @@ func TestToErr(t *testing.T) {
assertErrIs(t, toErr(999), ErrUnknown)
})
}

func TestLatLngsToC_Nil(t *testing.T) {
assertEqual(t, nil, latLngsToC(nil))
}
Loading