Skip to content

Commit

Permalink
GH-44: some basic docs
Browse files Browse the repository at this point in the history
  • Loading branch information
pocketken committed Oct 24, 2021
1 parent 528606c commit 4a2b833
Show file tree
Hide file tree
Showing 12 changed files with 415 additions and 126 deletions.
131 changes: 8 additions & 123 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,19 +1,10 @@
<img align="right" src="https://uber.github.io/img/h3Logo-color.svg" alt="H3 Logo" width="200">

# H3.net: A port of Uber's Hexagonal Hierarchical Geospatial Indexing System to C#
This is a port of [Uber's H3 library](https://github.com/uber/h3) to C# (`net5.0` and `netstandard2.1`), with additional functionality to support [NetTopologySuite](http://nettopologysuite.github.io/NetTopologySuite/index.html) geometries. It is based on release 3.7.2 of the library.
This is a port of [Uber's H3 library](https://github.com/uber/h3) to C# with most of the polygon functionality based on [NetTopologySuite](http://nettopologysuite.github.io/NetTopologySuite/index.html) instead of in-library implementations. It supports `netstandard2.0`, `netstandard2.1` and `net5.0` and is based on release 3.7.2 of the C library.

H3 is a geospatial indexing system using a hexagonal grid that can be (approximately) subdivided into finer and finer hexagonal grids, combining the benefits of a hexagonal grid with [S2](https://code.google.com/archive/p/s2-geometry-library/)'s hierarchical subdivisions.

Upstream documentation is available at [https://h3geo.org/](https://h3geo.org/). Documentation for this port of the library is unfortunately currently limited to documentation in the source and examples via unit tests.

## Work in Progress!
This is a work in progress and likely contains the odd silly bug or poorly performing/confusing implementation choice (PR's are welcome!). I have tried to make the library work and feel more "C# like" using operators, extension methods, enumerables vs. arrays and so on, but some things may be implemented more or less as straight ports of the upstream library. Other than the obvious difference in some method names and whatnot, the biggest difference vs. the upstream library is the reliance on NTS for geometries (e.g. cell boundary polygons) and algorithms such as polyfill instead of in-library classes such as the `GeoBoundary` and vertex graph stuff.

While the majority of the core H3 API should be here in one form or another, there's still gaps particularly in terms of documentation and tests to validate behaviour vs. upstream. My focus so far has been on getting the algorithm side of things (such as k-rings, polyfill and so on) working and tested as I needed those features for the project(s) I am working on, however given that the majority of those methods depend on the basics of the library working, test coverage is "ok". I also make use of the [PostgreSQL bindings](https://github.com/bytesandbrains/h3-pg) in my work, so I tend to validate results with that as well.

PRs to improve code, tests and documentation are definitely welcome, although please keep in mind I am quite busy these days and may be a bit slow to respond. Sorry in advance!

## Installing
Available on [nuget.org](https://nuget.org) as [pocketken.H3](https://www.nuget.org/packages/pocketken.H3/).

Expand All @@ -23,120 +14,14 @@ PM> Install-Package pocketken.H3 -Version 3.7.2.1

See [CHANGES.md](CHANGES.md) for a list of changes between releases.

## Some Mostly-Pointless Benchmarks
There is an extremely basic set of benchmarks using [BenchmarkDotNet](https://benchmarkdotnet.org/index.html) that I have begun to use in order to track performance and perform optimizations as things progress. You can check the code out to run the benchmarks locally if you want, e.g.:

```sh
$ dotnet run --configuration Release --project .\test\H3.Benchmarks\H3.Benchmarks.csproj --filter *Uncompact* --join --framework net5.0
```
## Documentation
Upstream documentation is available at [https://h3geo.org/](https://h3geo.org/). Basic getting-started documentation for this port of the library is available [here](docs/basic-usage.md). I have tried to make the library work and feel more "C# like" using classes, operators, extension methods, enumerables vs. arrays and so on, but some things may be implemented more or less as straight ports of the upstream library.

All numbers here are from my primary Windows development VM:
While the majority of the core H3 API should be here in one form or another, there's still the odd gap in terms of documentation and tests to validate behaviour vs. upstream. PRs to improve code, tests and documentation are definitely welcome and appreciated, although please keep in mind I am quite busy these days and may be a bit slow to respond. Sorry in advance!

``` ini
BenchmarkDotNet=v0.12.1, OS=Windows 10.0.19042
AMD Ryzen 9 3900X, 1 CPU, 12 logical and 6 physical cores
.NET Core SDK=5.0.201
[Host] : .NET Core 5.0.4 (CoreCLR 5.0.421.11614, CoreFX 5.0.421.11614), X64 RyuJIT
.NET Core 5.0 : .NET Core 5.0.4 (CoreCLR 5.0.421.11614, CoreFX 5.0.421.11614), X64 RyuJIT
## Benchmarks
See these likely-out-of-date [benchmarks](docs/benchmarks.md), or you can run them yourself - e.g.:

Job=.NET Core 5.0 Runtime=.NET Core 5.0
```sh
dotnet run --configuration Release --project .\test\H3.Benchmarks\H3.Benchmarks.csproj --join --framework net5.0
```

While there are some comparisons here against [H3Lib](https://github.com/RichardVasquez/h3net), I still need to work on getting some benchmarks for the other H3 package which wraps the native library; if anyone is interested in assisting PRs are welcome!

### Hierarchy Ops

#### GetParentForResolution
Using `89283080dcbffff` (Uber's SF Test index @ resolution 9) to get parent at resolution 0 (a silly microbenchmark):

| Method | Mean | Error | StdDev | Gen 0 | Allocated |
|------------------------------------ |----------:|----------:|----------:|-------:|----------:|
| pocketken.H3.GetParentForResolution | 4.918 ns | 0.0838 ns | 0.0784 ns | 0.0029 | 24 B |
| H3Lib.ToParent | 21.087 ns | 0.1255 ns | 0.1174 ns | - | - |

#### GetChildrenForResolution
Using `89283080dcbffff` (Uber's SF Test index @ resolution 9) to get all children at resolution 15.

| Method | Mean | Error | StdDev | Gen 0 | Gen 1 | Gen 2 | Allocated |
|-------------------------------------- |----------:|----------:|----------:|----------:|----------:|---------:|----------:|
| pocketken.H3.GetChildrenForResolution | 9.639 ms | 0.1317 ms | 0.1099 ms | 796.8750 | 781.2500 | 484.3750 | 5 MB |
| H3Lib.ToChildren | 10.660 ms | 0.2096 ms | 0.3072 ms | 3453.1250 | 1671.8750 | 984.3750 | 24 MB |

#### GetDirectNeighbour
Using `89283080dcbffff` (Uber's SF Test index @ resolution 9) and `8e0800000000007` (first pentagon @ resolution 14) to get neighbours at `Direction.I` and `Direction.IJ`:

| Method | Mean | Error | StdDev | Gen 0 | Allocated |
|-------------------------------------------- |---------:|---------:|---------:|-------:|----------:|
| 'pocketken.H3.GetDirectNeighbour(hex, I)' | 16.68 ns | 0.140 ns | 0.124 ns | 0.0029 | 24 B |
| 'H3Lib.NeighborRotations(hex, I)' | 16.70 ns | 0.187 ns | 0.166 ns | - | - |
| 'pocketken.H3.GetDirectNeighbour(hex, IJ)' | 24.47 ns | 0.223 ns | 0.198 ns | 0.0029 | 24 B |
| 'H3Lib.NeighborRotations(hex, IJ)' | 27.64 ns | 0.226 ns | 0.189 ns | - | - |
| 'pocketken.H3.GetDirectNeighbour(pent, I)' | 27.50 ns | 0.307 ns | 0.287 ns | 0.0029 | 24 B |
| 'H3Lib.NeighborRotations(pent, I)' | 33.15 ns | 0.415 ns | 0.388 ns | - | - |
| 'pocketken.H3.GetDirectNeighbour(pent, IJ)' | 27.34 ns | 0.367 ns | 0.343 ns | 0.0029 | 24 B |
| 'H3Lib.NeighborRotations(pent, IJ)' | 32.86 ns | 0.250 ns | 0.234 ns | - | - |

### Algorithms

#### Fill (Polyfill)
Filling world and [Uber SF Test](https://github.com/uber/h3/blob/master/src/apps/testapps/testPolygonToCells.c#L27) polygons at varied resolutions:

* world, res 4: 288,122 cells
* world, res 5: 2,016,842 cells
* SF test, res 10: 8,794 cells
* SF test, res 11: 61,569 cells
* SF test, res 12: 430,832 cells
* SF test, res 13: 3,015,836 cells
* SF test, res 14: 21,111,191 cells
* SF test, res 15: 147,778,335 cells

| Method | Mean | Error | StdDev | Gen 0 | Gen 1 | Gen 2 | Allocated |
|------------------------------------- |---------------:|------------:|------------:|-------------:|------------:|-----------:|----------:|
| 'pocketken.H3.Fill(worldPolygon, 4)' | 196.743 ms | 1.8058 ms | 1.6008 ms | 10000.0000 | 2000.0000 | 1000.0000 | 90 MB |
| 'pocketken.H3.Fill(worldPolygon, 5)' | 1,413.311 ms | 11.3172 ms | 10.5861 ms | 71000.0000 | 13000.0000 | 4000.0000 | 648 MB |
| 'pocketken.H3.Fill(sfPolygon, 10)' | 7.319 ms | 0.0241 ms | 0.0202 ms | 367.1875 | 226.5625 | 117.1875 | 3 MB |
| 'H3Lib.Polyfill(sfPolygon, 10)' | 494.078 ms | 1.2105 ms | 0.9450 ms | 3000.0000 | - | - | 27 MB |
| 'pocketken.H3.Fill(sfPolygon, 11)' | 48.994 ms | 0.8197 ms | 0.7667 ms | 2727.2727 | 1090.9091 | 636.3636 | 20 MB |
| 'H3Lib.Polyfill(sfPolygon, 11)' | 3,319.726 ms | 3.1163 ms | 2.9150 ms | 21000.0000 | 3000.0000 | 1000.0000 | 168 MB |
| 'pocketken.H3.Fill(sfPolygon, 12)' | 361.273 ms | 6.7138 ms | 6.2801 ms | 16000.0000 | 3000.0000 | 2000.0000 | 145 MB |
| 'H3Lib.Polyfill(sfPolygon, 12)' | 20,111.706 ms | 269.9403 ms | 239.2950 ms | 137000.0000 | 19000.0000 | 2000.0000 | 1,119 MB |
| 'pocketken.H3.Fill(sfPolygon, 13)' | 2,692.485 ms | 14.1803 ms | 13.2643 ms | 109000.0000 | 30000.0000 | 9000.0000 | 1,046 MB |
| 'pocketken.H3.Fill(sfPolygon, 14)' | 18,216.525 ms | 22.5448 ms | 18.8259 ms | 719000.0000 | 119000.0000 | 10000.0000 | 6,702 MB |
| 'pocketken.H3.Fill(sfPolygon, 15)' | 128,363.156 ms | 501.3173 ms | 444.4047 ms | 4991000.0000 | 791000.0000 | 46000.0000 | 47,576 MB |

#### Lines
Line from `8e283080dc80007` to `8e48e1d7038d527` (`DistanceTo` of 554,625 cells).

| Method | Mean | Error | StdDev | Gen 0 | Gen 1 | Gen 2 | Allocated |
|-------------------- |-----------:|---------:|---------:|-------------:|-----------:|----------:|----------:|
| pocketken.H3.LineTo | 725.4 ms | 9.40 ms | 8.79 ms | 34000.0000 | 10000.0000 | 1000.0000 | 283 MB |
| H3Lib.LineTo | 4,683.3 ms | 14.94 ms | 13.25 ms | 1057000.0000 | 3000.0000 | 1000.0000 | 8,449 MB |

#### Rings
`hex` is a hexagon index (`8f48e1d7038d520`).

| Method | Mean | Error | StdDev | Gen 0 | Gen 1 | Gen 2 | Allocated |
|------------------------------------------ |----------------:|--------------:|--------------:|-------------:|-------------:|-------------:|--------------:|
| 'pocketken.H3.GetKRing(hex, k = 50)' | 419.5 us | 1.79 us | 1.59 us | 73.7305 | 36.6211 | - | 608 KB |
| 'pocketken.H3.GetKRingFast(hex, k = 50)' | 422.6 us | 1.40 us | 1.09 us | 66.4063 | 33.2031 | - | 548 KB |
| 'pocketken.H3.GetKRingSlow(hex, k = 50)' | 3,080.5 us | 7.55 us | 6.31 us | 269.5313 | 179.6875 | 89.8438 | 2,113 KB |
| 'H3Lib.KRingDistances(hex, k = 50)' | 463.7 us | 3.11 us | 2.76 us | 99.6094 | 99.6094 | 99.6094 | 487 KB |

`pent` is a pentagon index (`8e0800000000007`) which forces the use of the iterative (recursive in the case of H3Lib) method of generating the ring due to the fast method's inability to handle pentagons.

| Method | Mean | Error | StdDev | Gen 0 | Gen 1 | Gen 2 | Allocated |
|------------------------------------------ |----------------:|--------------:|--------------:|-------------:|-------------:|-------------:|--------------:|
| 'pocketken.H3.GetKRing(pent, k = 50)' | 3,295.9 us | 24.51 us | 21.73 us | 269.5313 | 179.6875 | 89.8438 | 2,113 KB |
| 'pocketken.H3.GetKRingSlow(pent, k = 50)' | 3,097.6 us | 23.57 us | 22.05 us | 269.5313 | 179.6875 | 89.8438 | 2,113 KB |
| 'H3Lib.KRingDistances(pent, k = 50)' | 79,416,403.4 us | 594,028.76 us | 555,654.87 us | 7644000.0000 | 6050000.0000 | 5015000.0000 | 73,068,645 KB |

### Sets
* Compact: Result of compacting all cells at resolution 5.
* Uncompact: Result of uncompacting all base cells to resolution of 5.

| Method | Mean | Error | StdDev | Gen 0 | Gen 1 | Gen 2 | Allocated |
|----------------------- |---------:|--------:|--------:|-----------:|----------:|----------:|----------:|
| pocketken.H3.Compact | 330.3 ms | 5.50 ms | 5.14 ms | 11000.0000 | 3000.0000 | - | 243.51 MB |
| H3Lib.Compact | 381.0 ms | 5.83 ms | 5.45 ms | 9000.0000 | 4000.0000 | 2000.0000 | 305.24 MB |
| pocketken.H3.Uncompact | 136.6 ms | 0.36 ms | 0.32 ms | 6500.0000 | 3500.0000 | 1000.0000 | 78.18 MB |
| H3Lib.Uncompact | 203.3 ms | 4.05 ms | 4.16 ms | 43000.0000 | 7333.3333 | 666.6667 | 493.02 MB |
50 changes: 50 additions & 0 deletions docs/api-hierarchy.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# Hierarchial grid functions

## `GetParentForResolution` (`h3ToParent`)
Gets the parent (coarser) index at the specified resolution.
```cs
var index = new H3Index(0x89283080dcbffff);
var parentIndex = index.GetParentForResolution(5);
```

Note that you'll get back `H3Index.Invalid` (aka upstream's `H3_NULL`) if you specify an invalid parent resolution (<= the index resolution).

## `GetChildCenterForResolution` (`h3ToCenterChild`)
Gets the center child (finer) index at the specified resolution.

```cs
var index = new H3Index(0x89283080dcbffff);
var childIndex = index.GetChildCenterForResolution(12);
```

As with the `GetParentForResolution` method, you'll get `H3Index.Invalid` if your child resolution is invalid (>= the index resolution).

## `GetChildrenForResolution` (`h3ToChildren`)
Gets all of the child (finer) indexes at the specified resolution as an `IEnumerable<H3Index>`.

```cs
var index = new H3Index(0x89283080dcbffff);
var children = index.GetChildrenAtResolution(12);

// iterate, use .ToList() etc..
```

## `Compact` (`compact`)
Takes a set of cells and compacts them by removing duplicates and pruning full child branches to the parent level. This is also done for all parents recursively to get the minimum number of indexes that perfectly cover the defined space. Returns a `List<H3Index>` (**not** an `IEnumerable<H3Index>`, so be weary of large numbers of input cells as we have to iterate / track them all as part of the compaction algorithm).

```cs
// given some enumerable set of indexes..
IEnumerable<H3Index> indexes = ...;
// .. return the compacted set
var compacted = indexes.Compact();
```

## `UncompactToResolution` (`uncompact`)
Takes a compacted set of cells and expands back to the original set of cells at a specific resolution. Returns an `IEnumerable<H3Index>`.

```cs
// given a compacted set of indexes
var compactedIndexes = ...;
// .. get the uncompacted set at res 10
var uncompacted = compactedIndexes.UncompactToResolution(10);
```
64 changes: 64 additions & 0 deletions docs/api-indexing.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
# Indexing functions

## From/To Value Types

```cs
// from string (stringToH3)
var index = new H3Index("89283080dcbffff");

// to string (h3ToString)
var str = index.ToString();
```
```cs
// from ulong
var index = new H3Index(0x89283080dcbffff);

// operators allow for implicit casting
H3Index index = 0x89283080dcbffff;
ulong value = index;
```

## From/To Geospatial Coordinates

To create an index based upon a geospatial coordinate and a target H3 resolution (`geoToH3`):

```cs
// from lon/lat, with a resolution of 9 -- note NTS Coordinates are longitude (X) first,
// latitude (Y) second, unlike upstream/Model.GeoCoord which is latitude then longitude
// (AND in radians, not degrees).
var coordinate = new Coordinate(-122.40898669969357, 37.81331899988944);
var index = coordinate.ToH3Index(9);
```
```cs
// or if you have a Point
var index = H3Index.FromPoint(point, 9);
```

You can use `GeoCoord`s with radian inputs as well, which can be handy if you've got existing code using them you want to port over:

```cs
using H3.Model;

var index = H3Index.FromGeoCoord(new GeoCoord(0.659966917655, -2.1364398519396), 9);
```

To get the centroid of a given H3 index (`h3ToGeo`):

```cs
// gives you a NTS Coordinate
var coordinate = index.ToCoordinate();

// gives you a NTS Point
var point = index.ToPoint();

// gives you GeoCoord
var geoCoord = index.ToGeoCoord();
```

## `GetCellBoundary` (`h3ToGeoBoundary`)
Returns the `Polygon` cell boundary of a given H3 index.

```cs
// gives you a NTS Polygon
var polygon = index.GetCellBoundary();
```
39 changes: 39 additions & 0 deletions docs/api-inspection.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# Index inspection functions
The `H3Index` class provides a wrapper around an index value, and provides convenient computed fields/properties for various index attributes:

```cs
// h3GetResolution
int resolution = index.Resolution;

// h3GetBaseCell
Nodel.BaseCell baseCell = index.BaseCell;

// h3IsValid
bool isValid = index.IsValid;

// h3IsResClassII
bool isResClass3 = Utils.IsResolutionClass3(index.Resolution);

// h3IsPentagon
bool isPentagon = index.IsPentagon;

// h3MaxFaceCount
int maxFaces = index.MaximumFaceCount;
```

Note some of these properties are computed when you access them via bitwise manipulations of the index value. So, while they aren't overly expensive, if you're repeatedly checking certain properties in a loop or somthing similar you may want to consider caching their values, e.g.:

```cs
// get the resolution once, instead of each loop iteration
var resolution = index.Resolution;
for (var r = 0; r <= resolution; r += 1>) {
// ...
}
```

## `GetFaces` (`h3GetFaces`)
Find all icosahedron faces intersected by a given index. Faces are represented as integers from 0-19, inclusive. The array is sparse, and empty (no intersection) array values are represented by -1.

```cs
var faces = index.GetFaces()
```
31 changes: 31 additions & 0 deletions docs/api-regions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# Region Functions
Converts H3 indexes to and from polygonal areas.

## `Fill` (`polyfill`)
Fills a `Geometry` (`Polygon` or `LinearRing`) with the indexes who's centroids are contained within. Returns `IEnumerable<H3Index>`. Supports polygons with holes.

```cs
var reader = new WKTReader(geometryFactory);
var somePolygon = reader.Read("POLYGON ((-122.40898669969356 37.81331899988944, -122.47987669969707 37.81515719990604, -122.52471869969825 37.783587199903444, -122.51234369969448 37.70761319990403, -122.35447369969584 37.719806199904276, -122.38054369969613 37.78663019990699, -122.40898669969356 37.81331899988944))");
var filled = somePolygon.Fill(7).ToList();
```

### Sample Output

`Polygon` being filled:
![polyfill boundary](./polyfill-boundary.png)

Output `MultiPolygon` showing all indicies at resolution 7:
![polyfill result](./polyfill-result.png)

There's also an overload that takes a `LineString` and traces it, returning the indexes along the path.

## `GetCellBoundaries` (`h3SetToMultiPolygon`):
Returns a `MultiPolygon` containing all of the cell boundaries of the input set of indices.

```cs
var filled = somePolygon.Fill(7).ToList();
var multiPolygon = indexes.GetCellBoundaries();
```

This produces the second image shown above.
Loading

0 comments on commit 4a2b833

Please sign in to comment.