diff --git a/README.md b/README.md index 63d77a4..eb5e58c 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/h3.go b/h3.go index 781762c..aaddb8b 100644 --- a/h3.go +++ b/h3.go @@ -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. +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. @@ -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. @@ -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. diff --git a/h3_test.go b/h3_test.go index b7ee559..6fec0fe 100644 --- a/h3_test.go +++ b/h3_test.go @@ -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() @@ -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) { @@ -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") }) }) @@ -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) }) } @@ -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 } @@ -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 @@ -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)) +}