Skip to content

Commit

Permalink
Release 3.7.2.1 (#52)
Browse files Browse the repository at this point in the history
  * GH-42: improve performance for polyfill + lines
  * GH-44: basic docs
  * GH-45: implement polyfill modes
  * GH-46: add support for netstandard2.1
  * GH-49: add support for netstandard2.0
  • Loading branch information
pocketken committed Dec 16, 2021
1 parent 1268a70 commit 4d39883
Show file tree
Hide file tree
Showing 67 changed files with 3,633 additions and 1,115 deletions.
21 changes: 19 additions & 2 deletions .github/workflows/dotnet.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,30 @@ jobs:

steps:
- uses: actions/checkout@v2
- name: Setup .NET

- name: Setup .NET 5.0
uses: actions/setup-dotnet@v1
with:
dotnet-version: 5.0.x

- name: Setup .NET 6.0
uses: actions/setup-dotnet@v1
with:
dotnet-version: '6.0.x'
include-prerelease: true

- name: Setup .NET Core 3.1
uses: actions/setup-dotnet@v1
with:
dotnet-version: 3.1.x

- name: Restore dependencies
run: dotnet restore

# note: we disable parallel builds to prevent spurious
# build failures w/code generator
- name: Build
run: dotnet build --no-restore
run: dotnet build --no-restore /m:1

- name: Test
run: dotnet test --no-build --verbosity normal
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
pocketken.H3.*.xml
Generated/

## Ignore Visual Studio temporary files, build results, and
## files generated by popular Visual Studio add-ons.
Expand Down
14 changes: 14 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,19 @@
# pocketken.H3 Change Log

### 3.7.2.1 - 2021-12-16

##### Breaking Changes :mega:

- Visibility on several classes + methods changed from `public` to `internal` in an effort to clean up the public-facing API. Let me know if I've changed anything you're using
- `sealed` added to all classes

##### Enhancements :tada:

- Added multi-target support for `net6.0`, `net5.0`, `netstandard2.0` [#49](https://github.com/pocketken/H3.net/issues/49) and `netstandard2.1` [#46](https://github.com/pocketken/H3.net/issues/46)
- Implement polyfill modes [#45](https://github.com/pocketken/H3.net/issues/45)
- Reduced allocations / increased performance in `H3.Algorithms.Polyfill.Fill` and `H3.Algorithms.Lines.LineTo` [#42](https://github.com/pocketken/H3.net/issues/42)
- Other minor performance improvements and tweaks

### 3.7.2 - 2021-07-19

##### Breaking Changes :mega:
Expand Down
6 changes: 6 additions & 0 deletions H3.sln
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "H3.Test", "test\H3.Test\H3.
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "H3.Benchmarks", "test\H3.Benchmarks\H3.Benchmarks.csproj", "{7DD3EC1D-F4F5-4A63-801D-F1D82E740453}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "H3.Data", "src\H3.Data\H3.Data.csproj", "{C6A058BB-87EB-4048-93B1-CAC628AB5769}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand All @@ -27,6 +29,10 @@ Global
{7DD3EC1D-F4F5-4A63-801D-F1D82E740453}.Debug|Any CPU.Build.0 = Debug|Any CPU
{7DD3EC1D-F4F5-4A63-801D-F1D82E740453}.Release|Any CPU.ActiveCfg = Release|Any CPU
{7DD3EC1D-F4F5-4A63-801D-F1D82E740453}.Release|Any CPU.Build.0 = Release|Any CPU
{C6A058BB-87EB-4048-93B1-CAC628AB5769}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{C6A058BB-87EB-4048-93B1-CAC628AB5769}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C6A058BB-87EB-4048-93B1-CAC628AB5769}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C6A058BB-87EB-4048-93B1-CAC628AB5769}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down
91 changes: 9 additions & 82 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,100 +1,27 @@
<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#, 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`, `net5.0`, `net6.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/).

```
PM> Install-Package pocketken.H3 -Version 3.7.2
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.:
## 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.

```sh
$ dotnet run --configuration Release --project .\test\H3.Benchmarks\H3.Benchmarks.csproj --filter *Uncompact* --join
```
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!

All numbers here are from my primary Windows development VM:
## Benchmarks
See these likely-out-of-date [benchmarks](docs/benchmarks.md), or you can run them yourself - e.g.:

``` 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

Job=.NET Core 5.0 Runtime=.NET Core 5.0
```sh
dotnet run --configuration Release --project .\test\H3.Benchmarks\H3.Benchmarks.csproj --join --framework net6.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 | Gen 1 | Gen 2 | Allocated |
|------------------------------------ |----------:|----------:|----------:|-------:|------:|------:|----------:|
| pocketken.H3.GetParentForResolution | 4.458 ns | 0.0474 ns | 0.0443 ns | 0.0029 | - | - | 24 B |
| H3Lib.ToParent | 20.817 ns | 0.1196 ns | 0.1118 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.128 ms | 0.1809 ms | 0.2415 ms | 796.8750 | 781.2500 | 484.3750 | 4.69 MB |
| H3Lib.ToChildren | 9.671 ms | 0.1904 ms | 0.2266 ms | 3453.1250 | 1671.8750 | 984.3750 | 23.55 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 | 972.3 ms | 8.73 ms | 7.29 ms | 45000.0000 | 11000.0000 | 1000.0000 | 355.44 MB |
| H3Lib.LineTo | 4,422.1 ms | 10.23 ms | 9.57 ms | 1057000.0000 | 3000.0000 | 1000.0000 | 8449.31 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)' | 460.1 us | 1.99 us | 1.86 us | 73.7305 | 36.6211 | - | 607.75 KB |
| 'pocketken.H3.GetKRingFast(hex, k = 50)' | 471.4 us | 2.48 us | 2.32 us | 66.4063 | 33.2031 | - | 547.92 KB |
| 'pocketken.H3.GetKRingSlow(hex, k = 50)' | 4,852.8 us | 38.70 us | 36.20 us | 179.6875 | 85.9375 | 85.9375 | 1634.09 KB |
| 'H3Lib.KRingDistances(hex, k = 50)' | 381.6 us | 0.87 us | 0.68 us | 99.6094 | 99.6094 | 99.6094 | 486.59 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)' | 4.088 ms | 0.0263 ms | 0.0246 ms | 179.6875 | 85.9375 | 85.9375 | 1.6 MB |
| 'pocketken.H3.GetKRingSlow(pent, k = 50)' | 3.985 ms | 0.0138 ms | 0.0115 ms | 179.6875 | 85.9375 | 85.9375 | 1.6 MB |
| 'H3Lib.KRingDistances(pent, k = 50)' | 59,216.102 ms | 960.4448 ms | 898.4006 ms | 7692000.0000 | 6112000.0000 | 5064000.0000 | 71358.66 MB |

### 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 4d39883

Please sign in to comment.