diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml index 659168f..6179a2f 100644 --- a/.github/workflows/dotnet.yml +++ b/.github/workflows/dotnet.yml @@ -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 diff --git a/.gitignore b/.gitignore index fdd0bc8..f28b98d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ pocketken.H3.*.xml +Generated/ ## Ignore Visual Studio temporary files, build results, and ## files generated by popular Visual Studio add-ons. diff --git a/CHANGES.md b/CHANGES.md index 96f54dd..fb86a34 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -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: diff --git a/H3.sln b/H3.sln index 5702966..f50c1e7 100644 --- a/H3.sln +++ b/H3.sln @@ -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 @@ -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 diff --git a/README.md b/README.md index a1baebb..0ab8d93 100644 --- a/README.md +++ b/README.md @@ -1,100 +1,27 @@ H3 Logo # 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 | diff --git a/docs/api-hierarchy.md b/docs/api-hierarchy.md new file mode 100644 index 0000000..b4c9f74 --- /dev/null +++ b/docs/api-hierarchy.md @@ -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`. + +```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` (**not** an `IEnumerable`, 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 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`. + +```cs +// given a compacted set of indexes +var compactedIndexes = ...; +// .. get the uncompacted set at res 10 +var uncompacted = compactedIndexes.UncompactToResolution(10); +``` diff --git a/docs/api-indexing.md b/docs/api-indexing.md new file mode 100644 index 0000000..8915ea7 --- /dev/null +++ b/docs/api-indexing.md @@ -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(); +``` diff --git a/docs/api-inspection.md b/docs/api-inspection.md new file mode 100644 index 0000000..8387a39 --- /dev/null +++ b/docs/api-inspection.md @@ -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() +``` diff --git a/docs/api-regions.md b/docs/api-regions.md new file mode 100644 index 0000000..28c7da9 --- /dev/null +++ b/docs/api-regions.md @@ -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`. 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. diff --git a/docs/api-traversal.md b/docs/api-traversal.md new file mode 100644 index 0000000..0137122 --- /dev/null +++ b/docs/api-traversal.md @@ -0,0 +1,78 @@ +# Grid traversal functions + +## `GetKRing` (`kRingDistances`) +Produces indices within `k` distance of the origin index. Output may be returned in no particular order, and is an `IEnumerable` (simple holding struct that contains an `Index` and `Distance` from the origin). + +```cs +var ring = index.GetKRing(5); +// iterate, use .ToList() etc.. +``` + +> **Implementation Notes**: This is implemented similarly to the upstream `kRingDistances` function in that it tries to use `GetKRingFast(k).ToList()` (`hexRangeDistances`) first, and if that throws an exception due to pentagonal distortion it falls back to calling `GetKRingSlow(k)` (the non-recursive equivalent to the upstream library's internal `_kRingInternal` method). Depending on your input index, resolution, value of `k` and so on, you may find calling `GetKRingFast` or `GetKRingSlow` directly ends up performing better and/or requires less memory than calling `GetKRing` due to not having to try and materialize the `List`. You may want to experiment and see what works best for your use case. + +## `GetKRingSlow` (`_kRingInternal`) +Produces indices within `k` distance of the origin index. Output may be returned in no particular order, and is an `IEnumerable` (simple holding struct that contains an `Index` and `Distance` from the origin). + +This is the "safe but slow" version of the k-ring (grid disk) algorithm implemented using a `Queue` instead of recursion. This reduces stack depth for large values of `k` and and allow us to easily return an `IEnumerable`. + +```cs +var ring = index.GetKRingSlow(5); +// iterate, use .ToList() etc.. +``` + +## `GetKRingFast` (`hexRangeDistances`) +Produces indices within `k` distance of the origin index. Output may be returned in no particular order, and is an `IEnumerable` (simple holding struct that contains an `Index` and `Distance` from the origin). + +This is the "unsafe" version of the k-ring (grid disk) algorithm that has "undefined behaviour" (throws an exception) if/when one of the indexes returned by this method is a pentagon or is in the pentagon distortion area. + +```cs +var ring = index.GetKRingFast(5); +// iterate, use .ToList() etc.. +``` + +## `GetHexRing` (`hexRing`) +Produces the hollow hexagonal ring centered at origin with sides of length `k` (gives you all of the indicies for just the ring at distance `k`). Returns `IEnumerable`. + +Similar to `GetKRingFast` this utilizes an "unsafe" version of the k-ring (grid disk) algorithm that has "undefined behaviour" (throws an exception) if/when one of the indexes returned by this method is a pentagon or is in the pentagon distortion area. + +```cs +var hexRing = index.GetHexRing(5); +// iterate, use .ToList() etc.. +``` + +## `DistanceTo` (`h3Distance`) +Returns the distance in grid cells between the two indexes. Will return -1 if finding the distance failed (different resolutions, too far apart, seaprated by pentagonal distortion). + +```cs +var distance = index.DistanceTo(someOtherIndex); +``` + +## `LineTo` (`h3Line`) +Given two H3 indexes, returns the line of indexes between them (inclusive) as an `IEnumerable`. + +As per the upstream library, there are some things to consider: + + * This function may fail to find the distance between two indexes, for example if they are very far apart or the indexes are on opposite sides of a pentagon + * The specific output of this function should not be considered stable across library versions; the only guarantees are that the line length will be `DistanceTo(start, end) + 1` and that every index in the line will be a neighbour of the preceding index + * Lines are drawn in grid space, and may not correspond exactly to either Cartesian lines or great arcs. You can always use the `Fill` polyfill method on a NTS `LineString` geometry if you need that sort of thing. + +```cs +var line = originIndex.LineTo(destinationIndex); +// iterate, use .ToList() etc.. +``` + +## `ToLocalIJ` (`experimentalH3ToLocalIj`) +Produces local IJ coordinates for an H3 index anchored by an origin. + +Experiemental, output is not guaranteed, may eat your children etc.. + +```cs +Model.CoordIJ localIj = originIndex.ToLocalIj(otherIndex); +``` + +## `FromLocalIJ` (`experimentalLocalIjToH3`) +Produices an H3 index from local IJ coordinates anchored by an origin. + +```cs +var otherIndex = origin.FromLocalIJ(localIj); +``` diff --git a/docs/basic-usage.md b/docs/basic-usage.md new file mode 100644 index 0000000..25f23a3 --- /dev/null +++ b/docs/basic-usage.md @@ -0,0 +1,25 @@ +# Basic Usage / Quick Start +Here's some (really) quick and dirty examples to get you started with using the library, broken down in (more or less) the same format as the [upstream API documentation](https://h3geo.org/docs/api/indexing). Note that these examples assume you've imported `H3.Algorithms` and `H3.Extensions`, i.e.: + +```cs +using H3; +using H3.Algorithms; +using H3.Extensions; +``` + +Upstream C function names are also provided for reference. + +* [Indexing functions](api-indexing.md) - from/to `string` and `ulong`; creating an index from a geospatial coordinate; getting the centroid of an index +* [Index inspection functions](api-inspection.md) - get an indexes resolution, base cell, validity, whether it's a pentagon +* [Grid traversal functions](api-traversal.md) - k-rings + lines, local IJ coordinates +* [Hierarchial grid functions](api-hierarchy.md) - parent/child/children indices +* [Region functions](api-regions.md) - polyfill + +Note that this does not represent the entirety of the library's API; many functions and low-level operations that are internal to the upstream library are (presently) exposed as public APIs, allowing you to pretty much manipulate an index as you see fit. + +The [H3.Test project](../test/H3.Test) contains examples of using various parts of the library's API, and there's decent-ish documentation within the actual [source](../src/H3). + +## Other Examples +If you have any examples you'd like to share here, feel free to submit a PR! + +* [Server-Side Geospatial Clustering using H3](https://shawinnes.com/server-side-spatial-clustering/) by [Shaw Innes](https://github.com/ShawInnes/learn-geospatial) diff --git a/docs/benchmarks.md b/docs/benchmarks.md new file mode 100644 index 0000000..072c905 --- /dev/null +++ b/docs/benchmarks.md @@ -0,0 +1,117 @@ +## 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 +``` + +All numbers here are from my primary Windows development VM: + +``` 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 +``` + +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 | diff --git a/docs/polyfill-boundary.png b/docs/polyfill-boundary.png new file mode 100644 index 0000000..b5da552 Binary files /dev/null and b/docs/polyfill-boundary.png differ diff --git a/docs/polyfill-result.png b/docs/polyfill-result.png new file mode 100644 index 0000000..026b2bb Binary files /dev/null and b/docs/polyfill-result.png differ diff --git a/src/H3.Data/BaseCellData.cs b/src/H3.Data/BaseCellData.cs new file mode 100644 index 0000000..f0f03f3 --- /dev/null +++ b/src/H3.Data/BaseCellData.cs @@ -0,0 +1,49 @@ +namespace H3.Data { + + /// + /// Definition for one of the 122 base cells that form the H3 indexing scheme. + /// + public sealed class BaseCellData { + /// + /// The cell number, from 0 - 121. + /// + public int Cell { get; private set; } + + /// + /// The home face and IJK address of the cell. + /// + public BaseCellHomeAddress Home { get; private set; } + + /// + /// Whether or not this base cell is a pentagon. + /// + public bool IsPentagon { get; private set; } + + /// + /// If a pentagon, the cell's two clockwise offset faces. + /// + public int[] ClockwiseOffsetPent { get; private set; } + + private BaseCellData() { } + + /// + /// Creates a from an input set of parameters. + /// + /// + /// + public static implicit operator BaseCellData((int, (int, (int, int, int)), int, (int, int)) tuple) => + new BaseCellData { + Cell = tuple.Item1, + Home = new BaseCellHomeAddress { + Face = tuple.Item2.Item1, + I = tuple.Item2.Item2.Item1, + J = tuple.Item2.Item2.Item2, + K = tuple.Item2.Item2.Item3 + }, + IsPentagon = tuple.Item3 == 1, + ClockwiseOffsetPent = new[] { tuple.Item4.Item1, tuple.Item4.Item2 } + }; + + } + +} diff --git a/src/H3.Data/BaseCellHomeAddress.cs b/src/H3.Data/BaseCellHomeAddress.cs new file mode 100644 index 0000000..c25be2f --- /dev/null +++ b/src/H3.Data/BaseCellHomeAddress.cs @@ -0,0 +1,10 @@ +namespace H3.Data +{ + public struct BaseCellHomeAddress { + public int Face; + public int I; + public int J; + public int K; + } + +} diff --git a/src/H3.Data/BaseCellRotation.cs b/src/H3.Data/BaseCellRotation.cs new file mode 100644 index 0000000..421f738 --- /dev/null +++ b/src/H3.Data/BaseCellRotation.cs @@ -0,0 +1,34 @@ +using static H3.Data.Constants; + +namespace H3.Data { + + public struct BaseCellRotation { + public int Cell { get; private set; } + public int CounterClockwiseRotations { get; private set; } + + public const int InvalidRotations = -1; + + public static implicit operator BaseCellRotation((int, int) tuple) => + new BaseCellRotation { + Cell = tuple.Item1, + CounterClockwiseRotations = tuple.Item2, + }; + + public static int GetRotations(int cell, int face) { + if (face < 0 || face > NUM_ICOSA_FACES) return InvalidRotations; + + for (var i = 0; i < 3; i+= 1) { + for (var j = 0; j < 3; j += 1) { + for (var k = 0; k < 3; k += 1) { + var e = LookupTables.FaceIjkBaseCells[face, i, j, k]; + if (e.Cell == cell) return e.CounterClockwiseRotations; + } + } + } + + return InvalidRotations; + } + + } + +} diff --git a/src/H3.Data/BaseCellsGenerator.cs b/src/H3.Data/BaseCellsGenerator.cs new file mode 100644 index 0000000..efd7424 --- /dev/null +++ b/src/H3.Data/BaseCellsGenerator.cs @@ -0,0 +1,82 @@ +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Microsoft.CodeAnalysis; + +namespace H3.Data { + + // TODO use templating for the actual code bits, just feed in vars + + [Generator] + public class BaseCellsGenerator : ISourceGenerator { + + private static readonly string _indent = string.Empty.PadLeft(12, ' '); + + private const string Template = @"using System.Collections.Generic; + +namespace H3.Model {{ + + /// + /// All of the 122 base cells that comprise the H3 indexing scheme. + /// + public sealed class BaseCells {{ + + private static readonly sbyte[] NotAPentagonOffsets = {{ 0, 0 }}; +{0} + public static readonly BaseCell[] Cells = {{ +{1} + }}; + + }} + +}} +"; + + // TODO precompute base cell radius per resolution; will need geocoords for each resolution though (we could probably use that as well) + + public void Execute(GeneratorExecutionContext context) { + var cells = new StringBuilder(); + var cellNames = new StringBuilder(); + + for (var c = 0; c < Constants.NUM_BASE_CELLS; c += 1) { + var cell = LookupTables.BaseCells[c]; + cellNames.Append($"{_indent}BaseCell{c}{(c < 121 ? ",\n" : "")}"); + + var neighbouringCells = new List(); + var neighbourRotations = new List(); + var neighbourDirections = new Dictionary(); + + for (var d = 0; d < 7; d += 1) { + var n = (sbyte)LookupTables.Neighbours[c, d]; + neighbouringCells.Add(n); + neighbourRotations.Add(LookupTables.NeighbourCounterClockwiseRotations[c, d]); + if (n != LookupTables.INVALID_BASE_CELL) neighbourDirections[n] = (Direction)d; + } + + cells.Append($@" + private static readonly BaseCell BaseCell{c} = new() {{ + Cell = {c}, + Home = new FaceIJK {{ + Face = {cell.Home.Face}, + Coord = new CoordIJK {{ I = {cell.Home.I}, J = {cell.Home.J}, K = {cell.Home.K} }} + }}, + IsPentagon = {(cell.IsPentagon ? "true" : "false")}, + IsPolarPentagon = {(c == 4 || c == 117 ? "true" : "false")}, + ClockwiseOffsetPent = {(cell.IsPentagon ? $"new sbyte[] {{ {cell.ClockwiseOffsetPent[0]}, {cell.ClockwiseOffsetPent[1]} }}" : "NotAPentagonOffsets")}, + NeighbouringCells = new sbyte[] {{ {string.Join(", ", neighbouringCells)} }}, + NeighbourRotations = new sbyte[] {{ {string.Join(", ", neighbourRotations)} }}, + NeighbourDirections = new Dictionary {{ + {string.Join(", ", neighbourDirections.Select(e => $"{{ {e.Key}, Direction.{e.Value} }}"))} + }} + }}; +"); + } + + context.AddSource("BaseCells.cs", string.Format(Template, cells, cellNames)); + } + + public void Initialize(GeneratorInitializationContext context) { } + + } + +} \ No newline at end of file diff --git a/src/H3.Data/Constants.cs b/src/H3.Data/Constants.cs new file mode 100644 index 0000000..da968af --- /dev/null +++ b/src/H3.Data/Constants.cs @@ -0,0 +1,55 @@ +namespace H3.Data { + + public class Constants { + public const double M_PI = 3.14159265358979323846; + public const double M_PI_2 = 1.5707963267948966; + public const double M_2PI = 6.28318530717958647692528676655900576839433; // 2 * Math.PI; + + public const double M_PI_180 = 0.0174532925199432957692369076848861271111; // pi / 180 + public const double M_180_PI = 57.29577951308232087679815481410517033240547; // pi * 180 + + public const double EPSILON = 0.0000000000000001; // threshold epsilon + public const double M_SQRT3_2 = 0.8660254037844386467637231707529361834714; // sqrt(3) / 2.0 + public const double M_SIN60 = M_SQRT3_2; // sin(60') + public const double M_SQRT7 = 2.6457513110645905905016157536392604257102; + + // rotation angle between Class II and Class III resolution axes + public const double M_AP7_ROT_RADS = 0.333473172251832115336090755351601070065900389; // (asin(sqrt(3.0 / 28.0))) + + // earth radius in kilometers using WGS84 authalic radius + public const double EARTH_RADIUS_KM = 6371.007180918475; + + /* scaling factor from hex2d resolution 0 unit length + * (or distance between adjacent cell center points + * on the plane) to gnomonic unit length. */ + public const double RES0_U_GNOMONIC = 0.38196601125010500003; + + // max H3 resolution; H3 version 1 has 16 resolutions, numbered 0 through 15 + public const int MAX_H3_RES = 15; + + // The number of faces on an icosahedron + public const int NUM_ICOSA_FACES = 20; + + // The number of H3 base cells + public const int NUM_BASE_CELLS = 122; + + // The number of pentagon cells + public const int NUM_PENTAGONS = 12; + + // The number of vertices in a hexagon + public const int NUM_HEX_VERTS = 6; + + // The number of vertices in a pentagon + public const int NUM_PENT_VERTS = 5; + + /** epsilon of ~0.1mm in degrees */ + public const double EPSILON_DEG = .000000001; + + /** epsilon of ~0.1mm in radians */ + public const double EPSILON_RAD = EPSILON_DEG * M_PI_180; + + // maximum number of face coordinates + public const int MAX_FACE_COORD = 2; + } + +} diff --git a/src/H3.Data/Direction.cs b/src/H3.Data/Direction.cs new file mode 100644 index 0000000..b4f7e3d --- /dev/null +++ b/src/H3.Data/Direction.cs @@ -0,0 +1,14 @@ +namespace H3.Data +{ + public enum Direction { + Center = 0, + K = 1, + J = 2, + JK = 3, + I = 4, + IK = 5, + IJ = 6, + Invalid = 7 + } + +} diff --git a/src/H3.Data/H3.Data.csproj b/src/H3.Data/H3.Data.csproj new file mode 100644 index 0000000..f35c31d --- /dev/null +++ b/src/H3.Data/H3.Data.csproj @@ -0,0 +1,14 @@ + + + + netstandard2.0 + 8.0 + false + + + + + + + + diff --git a/src/H3.Data/H3IndexBitwiseOpsGenerator.cs b/src/H3.Data/H3IndexBitwiseOpsGenerator.cs new file mode 100644 index 0000000..5439b0e --- /dev/null +++ b/src/H3.Data/H3IndexBitwiseOpsGenerator.cs @@ -0,0 +1,74 @@ +using System.Text; +using Microsoft.CodeAnalysis; + +namespace H3.Data { + + [Generator] + public class H3IndexBitwiseOpsGenerator : ISourceGenerator { + + private static readonly string _indent = string.Empty.PadLeft(16, ' '); + + private const string TEMPLATE = @"using H3.Model; +using System.Runtime.CompilerServices; + +namespace H3 {{ + + public sealed partial class H3Index {{ + + /// + /// Perform in-place 60 degree clockwise rotation(s) of the index. + /// + /// number of rotations to perform + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void RotateClockwise(int rotations) {{ + if (rotations <= 0) return; + Value = Resolution switch {{{0} + _ => Value + }}; + }} + + /// + /// Perform in-place 60 degree clockwise rotation(s) of the index. + /// + /// number of rotations to perform + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void RotateCounterClockwise(int rotations) {{ + if (rotations <= 0) return; + Value = Resolution switch {{{1} + _ => Value + }}; + }} + + }} + +}}"; + + public void Initialize(GeneratorInitializationContext context) { } + + public void Execute(GeneratorExecutionContext context) { + var rotateHexCw = new StringBuilder(); + var rotateHexCcw = new StringBuilder(); + + for (var r = 1; r <= 15; r += 1) { + var mask = ~0UL; + mask <<= 3 * r; + mask = ~mask; + mask <<= 3 * (15 - r); + mask = ~mask; + + rotateHexCw.Append($"\n{_indent}{r} => (Value & {mask}UL) |\n"); + rotateHexCcw.Append($"\n{_indent}{r} => (Value & {mask}UL) |\n"); + + for (var c = 1; c <= r; c += 1) { + var offset = (15 - c) * 3; + var eol = c == r ? "," : " |\n"; + rotateHexCw.Append($"{_indent} ((ulong)((Direction)((Value >> {offset}) & H3_DIGIT_MASK)).RotateClockwise(rotations) << {offset}){eol}"); + rotateHexCcw.Append($"{_indent} ((ulong)((Direction)((Value >> {offset}) & H3_DIGIT_MASK)).RotateCounterClockwise(rotations) << {offset}){eol}"); + } + } + + context.AddSource("H3Index.BitwiseOperations.cs", string.Format(TEMPLATE, rotateHexCw, rotateHexCcw)); + } + + } +} diff --git a/src/H3.Data/LookupTables.cs b/src/H3.Data/LookupTables.cs new file mode 100644 index 0000000..2b580e5 --- /dev/null +++ b/src/H3.Data/LookupTables.cs @@ -0,0 +1,1513 @@ +namespace H3.Data { + + public static class LookupTables { + + #region basecells + public static readonly BaseCellData[] BaseCells = { + (0, (1, (1, 0, 0)), 0, (0, 0)), // base cell 0 + (1, (2, (1, 1, 0)), 0, (0, 0)), // base cell 1 + (2, (1, (0, 0, 0)), 0, (0, 0)), // base cell 2 + (3, (2, (1, 0, 0)), 0, (0, 0)), // base cell 3 + (4, (0, (2, 0, 0)), 1, (-1, -1)), // base cell 4 + (5, (1, (1, 1, 0)), 0, (0, 0)), // base cell 5 + (6, (1, (0, 0, 1)), 0, (0, 0)), // base cell 6 + (7, (2, (0, 0, 0)), 0, (0, 0)), // base cell 7 + (8, (0, (1, 0, 0)), 0, (0, 0)), // base cell 8 + (9, (2, (0, 1, 0)), 0, (0, 0)), // base cell 9 + (10, (1, (0, 1, 0)), 0, (0, 0)), // base cell 10 + (11, (1, (0, 1, 1)), 0, (0, 0)), // base cell 11 + (12, (3, (1, 0, 0)), 0, (0, 0)), // base cell 12 + (13, (3, (1, 1, 0)), 0, (0, 0)), // base cell 13 + (14, (11, (2, 0, 0)), 1, (2, 6)), // base cell 14 + (15, (4, (1, 0, 0)), 0, (0, 0)), // base cell 15 + (16, (0, (0, 0, 0)), 0, (0, 0)), // base cell 16 + (17, (6, (0, 1, 0)), 0, (0, 0)), // base cell 17 + (18, (0, (0, 0, 1)), 0, (0, 0)), // base cell 18 + (19, (2, (0, 1, 1)), 0, (0, 0)), // base cell 19 + (20, (7, (0, 0, 1)), 0, (0, 0)), // base cell 20 + (21, (2, (0, 0, 1)), 0, (0, 0)), // base cell 21 + (22, (0, (1, 1, 0)), 0, (0, 0)), // base cell 22 + (23, (6, (0, 0, 1)), 0, (0, 0)), // base cell 23 + (24, (10, (2, 0, 0)), 1, (1, 5)), // base cell 24 + (25, (6, (0, 0, 0)), 0, (0, 0)), // base cell 25 + (26, (3, (0, 0, 0)), 0, (0, 0)), // base cell 26 + (27, (11, (1, 0, 0)), 0, (0, 0)), // base cell 27 + (28, (4, (1, 1, 0)), 0, (0, 0)), // base cell 28 + (29, (3, (0, 1, 0)), 0, (0, 0)), // base cell 29 + (30, (0, (0, 1, 1)), 0, (0, 0)), // base cell 30 + (31, (4, (0, 0, 0)), 0, (0, 0)), // base cell 31 + (32, (5, (0, 1, 0)), 0, (0, 0)), // base cell 32 + (33, (0, (0, 1, 0)), 0, (0, 0)), // base cell 33 + (34, (7, (0, 1, 0)), 0, (0, 0)), // base cell 34 + (35, (11, (1, 1, 0)), 0, (0, 0)), // base cell 35 + (36, (7, (0, 0, 0)), 0, (0, 0)), // base cell 36 + (37, (10, (1, 0, 0)), 0, (0, 0)), // base cell 37 + (38, (12, (2, 0, 0)), 1, (3, 7)), // base cell 38 + (39, (6, (1, 0, 1)), 0, (0, 0)), // base cell 39 + (40, (7, (1, 0, 1)), 0, (0, 0)), // base cell 40 + (41, (4, (0, 0, 1)), 0, (0, 0)), // base cell 41 + (42, (3, (0, 0, 1)), 0, (0, 0)), // base cell 42 + (43, (3, (0, 1, 1)), 0, (0, 0)), // base cell 43 + (44, (4, (0, 1, 0)), 0, (0, 0)), // base cell 44 + (45, (6, (1, 0, 0)), 0, (0, 0)), // base cell 45 + (46, (11, (0, 0, 0)), 0, (0, 0)), // base cell 46 + (47, (8, (0, 0, 1)), 0, (0, 0)), // base cell 47 + (48, (5, (0, 0, 1)), 0, (0, 0)), // base cell 48 + (49, (14, (2, 0, 0)), 1, (0, 9)), // base cell 49 + (50, (5, (0, 0, 0)), 0, (0, 0)), // base cell 50 + (51, (12, (1, 0, 0)), 0, (0, 0)), // base cell 51 + (52, (10, (1, 1, 0)), 0, (0, 0)), // base cell 52 + (53, (4, (0, 1, 1)), 0, (0, 0)), // base cell 53 + (54, (12, (1, 1, 0)), 0, (0, 0)), // base cell 54 + (55, (7, (1, 0, 0)), 0, (0, 0)), // base cell 55 + (56, (11, (0, 1, 0)), 0, (0, 0)), // base cell 56 + (57, (10, (0, 0, 0)), 0, (0, 0)), // base cell 57 + (58, (13, (2, 0, 0)), 1, (4, 8)), // base cell 58 + (59, (10, (0, 0, 1)), 0, (0, 0)), // base cell 59 + (60, (11, (0, 0, 1)), 0, (0, 0)), // base cell 60 + (61, (9, (0, 1, 0)), 0, (0, 0)), // base cell 61 + (62, (8, (0, 1, 0)), 0, (0, 0)), // base cell 62 + (63, (6, (2, 0, 0)), 1, (11, 15)), // base cell 63 + (64, (8, (0, 0, 0)), 0, (0, 0)), // base cell 64 + (65, (9, (0, 0, 1)), 0, (0, 0)), // base cell 65 + (66, (14, (1, 0, 0)), 0, (0, 0)), // base cell 66 + (67, (5, (1, 0, 1)), 0, (0, 0)), // base cell 67 + (68, (16, (0, 1, 1)), 0, (0, 0)), // base cell 68 + (69, (8, (1, 0, 1)), 0, (0, 0)), // base cell 69 + (70, (5, (1, 0, 0)), 0, (0, 0)), // base cell 70 + (71, (12, (0, 0, 0)), 0, (0, 0)), // base cell 71 + (72, (7, (2, 0, 0)), 1, (12, 16)), // base cell 72 + (73, (12, (0, 1, 0)), 0, (0, 0)), // base cell 73 + (74, (10, (0, 1, 0)), 0, (0, 0)), // base cell 74 + (75, (9, (0, 0, 0)), 0, (0, 0)), // base cell 75 + (76, (13, (1, 0, 0)), 0, (0, 0)), // base cell 76 + (77, (16, (0, 0, 1)), 0, (0, 0)), // base cell 77 + (78, (15, (0, 1, 1)), 0, (0, 0)), // base cell 78 + (79, (15, (0, 1, 0)), 0, (0, 0)), // base cell 79 + (80, (16, (0, 1, 0)), 0, (0, 0)), // base cell 80 + (81, (14, (1, 1, 0)), 0, (0, 0)), // base cell 81 + (82, (13, (1, 1, 0)), 0, (0, 0)), // base cell 82 + (83, (5, (2, 0, 0)), 1, (10, 19)), // base cell 83 + (84, (8, (1, 0, 0)), 0, (0, 0)), // base cell 84 + (85, (14, (0, 0, 0)), 0, (0, 0)), // base cell 85 + (86, (9, (1, 0, 1)), 0, (0, 0)), // base cell 86 + (87, (14, (0, 0, 1)), 0, (0, 0)), // base cell 87 + (88, (17, (0, 0, 1)), 0, (0, 0)), // base cell 88 + (89, (12, (0, 0, 1)), 0, (0, 0)), // base cell 89 + (90, (16, (0, 0, 0)), 0, (0, 0)), // base cell 90 + (91, (17, (0, 1, 1)), 0, (0, 0)), // base cell 91 + (92, (15, (0, 0, 1)), 0, (0, 0)), // base cell 92 + (93, (16, (1, 0, 1)), 0, (0, 0)), // base cell 93 + (94, (9, (1, 0, 0)), 0, (0, 0)), // base cell 94 + (95, (15, (0, 0, 0)), 0, (0, 0)), // base cell 95 + (96, (13, (0, 0, 0)), 0, (0, 0)), // base cell 96 + (97, (8, (2, 0, 0)), 1, (13, 17)), // base cell 97 + (98, (13, (0, 1, 0)), 0, (0, 0)), // base cell 98 + (99, (17, (1, 0, 1)), 0, (0, 0)), // base cell 99 + (100, (19, (0, 1, 0)), 0, (0, 0)), // base cell 100 + (101, (14, (0, 1, 0)), 0, (0, 0)), // base cell 101 + (102, (19, (0, 1, 1)), 0, (0, 0)), // base cell 102 + (103, (17, (0, 1, 0)), 0, (0, 0)), // base cell 103 + (104, (13, (0, 0, 1)), 0, (0, 0)), // base cell 104 + (105, (17, (0, 0, 0)), 0, (0, 0)), // base cell 105 + (106, (16, (1, 0, 0)), 0, (0, 0)), // base cell 106 + (107, (9, (2, 0, 0)), 1, (14, 18)), // base cell 107 + (108, (15, (1, 0, 1)), 0, (0, 0)), // base cell 108 + (109, (15, (1, 0, 0)), 0, (0, 0)), // base cell 109 + (110, (18, (0, 1, 1)), 0, (0, 0)), // base cell 110 + (111, (18, (0, 0, 1)), 0, (0, 0)), // base cell 111 + (112, (19, (0, 0, 1)), 0, (0, 0)), // base cell 112 + (113, (17, (1, 0, 0)), 0, (0, 0)), // base cell 113 + (114, (19, (0, 0, 0)), 0, (0, 0)), // base cell 114 + (115, (18, (0, 1, 0)), 0, (0, 0)), // base cell 115 + (116, (18, (1, 0, 1)), 0, (0, 0)), // base cell 116 + (117, (19, (2, 0, 0)), 1, (-1, -1)), // base cell 117 + (118, (19, (1, 0, 0)), 0, (0, 0)), // base cell 118 + (119, (18, (0, 0, 0)), 0, (0, 0)), // base cell 119 + (120, (19, (1, 0, 1)), 0, (0, 0)), // base cell 120 + (121, (18, (1, 0, 0)), 0, (0, 0)) // base cell 121 + }; + + public static readonly int[,] NeighbourCounterClockwiseRotations = { + { 0, 5, 0, 0, 1, 5, 1}, // base cell 0 + { 0, 0, 1, 0, 1, 0, 1}, // base cell 1 + { 0, 0, 0, 0, 0, 5, 0}, // base cell 2 + { 0, 5, 0, 0, 2, 5, 1}, // base cell 3 + { 0, -1, 1, 0, 3, 4, 2}, // base cell 4 (pentagon) + { 0, 0, 1, 0, 1, 0, 1}, // base cell 5 + { 0, 0, 0, 3, 5, 5, 0}, // base cell 6 + { 0, 0, 0, 0, 0, 5, 0}, // base cell 7 + { 0, 5, 0, 0, 0, 5, 1}, // base cell 8 + { 0, 0, 1, 3, 0, 0, 1}, // base cell 9 + { 0, 0, 1, 3, 0, 0, 1}, // base cell 10 + { 0, 3, 3, 3, 0, 0, 0}, // base cell 11 + { 0, 5, 0, 0, 3, 5, 1}, // base cell 12 + { 0, 0, 1, 0, 1, 0, 1}, // base cell 13 + { 0, -1, 3, 0, 5, 2, 0}, // base cell 14 (pentagon) + { 0, 5, 0, 0, 4, 5, 1}, // base cell 15 + { 0, 0, 0, 0, 0, 5, 0}, // base cell 16 + { 0, 3, 3, 3, 3, 0, 3}, // base cell 17 + { 0, 0, 0, 3, 5, 5, 0}, // base cell 18 + { 0, 3, 3, 3, 0, 0, 0}, // base cell 19 + { 0, 3, 3, 3, 0, 3, 0}, // base cell 20 + { 0, 0, 0, 3, 5, 5, 0}, // base cell 21 + { 0, 0, 1, 0, 1, 0, 1}, // base cell 22 + { 0, 3, 3, 3, 0, 3, 0}, // base cell 23 + { 0, -1, 3, 0, 5, 2, 0}, // base cell 24 (pentagon) + { 0, 0, 0, 3, 0, 0, 3}, // base cell 25 + { 0, 0, 0, 0, 0, 5, 0}, // base cell 26 + { 0, 3, 0, 0, 0, 3, 3}, // base cell 27 + { 0, 0, 1, 0, 1, 0, 1}, // base cell 28 + { 0, 0, 1, 3, 0, 0, 1}, // base cell 29 + { 0, 3, 3, 3, 0, 0, 0}, // base cell 30 + { 0, 0, 0, 0, 0, 5, 0}, // base cell 31 + { 0, 3, 3, 3, 3, 0, 3}, // base cell 32 + { 0, 0, 1, 3, 0, 0, 1}, // base cell 33 + { 0, 3, 3, 3, 3, 0, 3}, // base cell 34 + { 0, 0, 3, 0, 3, 0, 3}, // base cell 35 + { 0, 0, 0, 3, 0, 0, 3}, // base cell 36 + { 0, 3, 0, 0, 0, 3, 3}, // base cell 37 + { 0, -1, 3, 0, 5, 2, 0}, // base cell 38 (pentagon) + { 0, 3, 0, 0, 3, 3, 0}, // base cell 39 + { 0, 3, 0, 0, 3, 3, 0}, // base cell 40 + { 0, 0, 0, 3, 5, 5, 0}, // base cell 41 + { 0, 0, 0, 3, 5, 5, 0}, // base cell 42 + { 0, 3, 3, 3, 0, 0, 0}, // base cell 43 + { 0, 0, 1, 3, 0, 0, 1}, // base cell 44 + { 0, 0, 3, 0, 0, 3, 3}, // base cell 45 + { 0, 0, 0, 3, 0, 3, 0}, // base cell 46 + { 0, 3, 3, 3, 0, 3, 0}, // base cell 47 + { 0, 3, 3, 3, 0, 3, 0}, // base cell 48 + { 0, -1, 3, 0, 5, 2, 0}, // base cell 49 (pentagon) + { 0, 0, 0, 3, 0, 0, 3}, // base cell 50 + { 0, 3, 0, 0, 0, 3, 3}, // base cell 51 + { 0, 0, 3, 0, 3, 0, 3}, // base cell 52 + { 0, 3, 3, 3, 0, 0, 0}, // base cell 53 + { 0, 0, 3, 0, 3, 0, 3}, // base cell 54 + { 0, 0, 3, 0, 0, 3, 3}, // base cell 55 + { 0, 3, 3, 3, 0, 0, 3}, // base cell 56 + { 0, 0, 0, 3, 0, 3, 0}, // base cell 57 + { 0, -1, 3, 0, 5, 2, 0}, // base cell 58 (pentagon) + { 0, 3, 3, 3, 3, 3, 0}, // base cell 59 + { 0, 3, 3, 3, 3, 3, 0}, // base cell 60 + { 0, 3, 3, 3, 3, 0, 3}, // base cell 61 + { 0, 3, 3, 3, 3, 0, 3}, // base cell 62 + { 0, -1, 3, 0, 5, 2, 0}, // base cell 63 (pentagon) + { 0, 0, 0, 3, 0, 0, 3}, // base cell 64 + { 0, 3, 3, 3, 0, 3, 0}, // base cell 65 + { 0, 3, 0, 0, 0, 3, 3}, // base cell 66 + { 0, 3, 0, 0, 3, 3, 0}, // base cell 67 + { 0, 3, 3, 3, 0, 0, 0}, // base cell 68 + { 0, 3, 0, 0, 3, 3, 0}, // base cell 69 + { 0, 0, 3, 0, 0, 3, 3}, // base cell 70 + { 0, 0, 0, 3, 0, 3, 0}, // base cell 71 + { 0, -1, 3, 0, 5, 2, 0}, // base cell 72 (pentagon) + { 0, 3, 3, 3, 0, 0, 3}, // base cell 73 + { 0, 3, 3, 3, 0, 0, 3}, // base cell 74 + { 0, 0, 0, 3, 0, 0, 3}, // base cell 75 + { 0, 3, 0, 0, 0, 3, 3}, // base cell 76 + { 0, 0, 0, 3, 0, 5, 0}, // base cell 77 + { 0, 3, 3, 3, 0, 0, 0}, // base cell 78 + { 0, 0, 1, 3, 1, 0, 1}, // base cell 79 + { 0, 0, 1, 3, 1, 0, 1}, // base cell 80 + { 0, 0, 3, 0, 3, 0, 3}, // base cell 81 + { 0, 0, 3, 0, 3, 0, 3}, // base cell 82 + { 0, -1, 3, 0, 5, 2, 0}, // base cell 83 (pentagon) + { 0, 0, 3, 0, 0, 3, 3}, // base cell 84 + { 0, 0, 0, 3, 0, 3, 0}, // base cell 85 + { 0, 3, 0, 0, 3, 3, 0}, // base cell 86 + { 0, 3, 3, 3, 3, 3, 0}, // base cell 87 + { 0, 0, 0, 3, 0, 5, 0}, // base cell 88 + { 0, 3, 3, 3, 3, 3, 0}, // base cell 89 + { 0, 0, 0, 0, 0, 0, 1}, // base cell 90 + { 0, 3, 3, 3, 0, 0, 0}, // base cell 91 + { 0, 0, 0, 3, 0, 5, 0}, // base cell 92 + { 0, 5, 0, 0, 5, 5, 0}, // base cell 93 + { 0, 0, 3, 0, 0, 3, 3}, // base cell 94 + { 0, 0, 0, 0, 0, 0, 1}, // base cell 95 + { 0, 0, 0, 3, 0, 3, 0}, // base cell 96 + { 0, -1, 3, 0, 5, 2, 0}, // base cell 97 (pentagon) + { 0, 3, 3, 3, 0, 0, 3}, // base cell 98 + { 0, 5, 0, 0, 5, 5, 0}, // base cell 99 + { 0, 0, 1, 3, 1, 0, 1}, // base cell 100 + { 0, 3, 3, 3, 0, 0, 3}, // base cell 101 + { 0, 3, 3, 3, 0, 0, 0}, // base cell 102 + { 0, 0, 1, 3, 1, 0, 1}, // base cell 103 + { 0, 3, 3, 3, 3, 3, 0}, // base cell 104 + { 0, 0, 0, 0, 0, 0, 1}, // base cell 105 + { 0, 0, 1, 0, 3, 5, 1}, // base cell 106 + { 0, -1, 3, 0, 5, 2, 0}, // base cell 107 (pentagon) + { 0, 5, 0, 0, 5, 5, 0}, // base cell 108 + { 0, 0, 1, 0, 4, 5, 1}, // base cell 109 + { 0, 3, 3, 3, 0, 0, 0}, // base cell 110 + { 0, 0, 0, 3, 0, 5, 0}, // base cell 111 + { 0, 0, 0, 3, 0, 5, 0}, // base cell 112 + { 0, 0, 1, 0, 2, 5, 1}, // base cell 113 + { 0, 0, 0, 0, 0, 0, 1}, // base cell 114 + { 0, 0, 1, 3, 1, 0, 1}, // base cell 115 + { 0, 5, 0, 0, 5, 5, 0}, // base cell 116 + { 0, -1, 1, 0, 3, 4, 2}, // base cell 117 (pentagon) + { 0, 0, 1, 0, 0, 5, 1}, // base cell 118 + { 0, 0, 0, 0, 0, 0, 1}, // base cell 119 + { 0, 5, 0, 0, 5, 5, 0}, // base cell 120 + { 0, 0, 1, 0, 1, 5, 1}, // base cell 121 + }; + + public const int INVALID_BASE_CELL = 127; + + public static readonly int[,] Neighbours = { + {0, 1, 5, 2, 4, 3, 8}, // base cell 0 + {1, 7, 6, 9, 0, 3, 2}, // base cell 1 + {2, 6, 10, 11, 0, 1, 5}, // base cell 2 + {3, 13, 1, 7, 4, 12, 0}, // base cell 3 + {4, INVALID_BASE_CELL, 15, 8, 3, 0, 12}, // base cell 4 (pentagon) + {5, 2, 18, 10, 8, 0, 16}, // base cell 5 + {6, 14, 11, 17, 1, 9, 2}, // base cell 6 + {7, 21, 9, 19, 3, 13, 1}, // base cell 7 + {8, 5, 22, 16, 4, 0, 15}, // base cell 8 + {9, 19, 14, 20, 1, 7, 6}, // base cell 9 + {10, 11, 24, 23, 5, 2, 18}, // base cell 10 + {11, 17, 23, 25, 2, 6, 10}, // base cell 11 + {12, 28, 13, 26, 4, 15, 3}, // base cell 12 + {13, 26, 21, 29, 3, 12, 7}, // base cell 13 + {14, INVALID_BASE_CELL, 17, 27, 9, 20, 6}, // base cell 14 (pentagon) + {15, 22, 28, 31, 4, 8, 12}, // base cell 15 + {16, 18, 33, 30, 8, 5, 22}, // base cell 16 + {17, 11, 14, 6, 35, 25, 27}, // base cell 17 + {18, 24, 30, 32, 5, 10, 16}, // base cell 18 + {19, 34, 20, 36, 7, 21, 9}, // base cell 19 + {20, 14, 19, 9, 40, 27, 36}, // base cell 20 + {21, 38, 19, 34, 13, 29, 7}, // base cell 21 + {22, 16, 41, 33, 15, 8, 31}, // base cell 22 + {23, 24, 11, 10, 39, 37, 25}, // base cell 23 + {24, INVALID_BASE_CELL, 32, 37, 10, 23, 18}, // base cell 24 (pentagon) + {25, 23, 17, 11, 45, 39, 35}, // base cell 25 + {26, 42, 29, 43, 12, 28, 13}, // base cell 26 + {27, 40, 35, 46, 14, 20, 17}, // base cell 27 + {28, 31, 42, 44, 12, 15, 26}, // base cell 28 + {29, 43, 38, 47, 13, 26, 21}, // base cell 29 + {30, 32, 48, 50, 16, 18, 33}, // base cell 30 + {31, 41, 44, 53, 15, 22, 28}, // base cell 31 + {32, 30, 24, 18, 52, 50, 37}, // base cell 32 + {33, 30, 49, 48, 22, 16, 41}, // base cell 33 + {34, 19, 38, 21, 54, 36, 51}, // base cell 34 + {35, 46, 45, 56, 17, 27, 25}, // base cell 35 + {36, 20, 34, 19, 55, 40, 54}, // base cell 36 + {37, 39, 52, 57, 24, 23, 32}, // base cell 37 + {38, INVALID_BASE_CELL, 34, 51, 29, 47, 21}, // base cell 38 (pentagon) + {39, 37, 25, 23, 59, 57, 45}, // base cell 39 + {40, 27, 36, 20, 60, 46, 55}, // base cell 40 + {41, 49, 53, 61, 22, 33, 31}, // base cell 41 + {42, 58, 43, 62, 28, 44, 26}, // base cell 42 + {43, 62, 47, 64, 26, 42, 29}, // base cell 43 + {44, 53, 58, 65, 28, 31, 42}, // base cell 44 + {45, 39, 35, 25, 63, 59, 56}, // base cell 45 + {46, 60, 56, 68, 27, 40, 35}, // base cell 46 + {47, 38, 43, 29, 69, 51, 64}, // base cell 47 + {48, 49, 30, 33, 67, 66, 50}, // base cell 48 + {49, INVALID_BASE_CELL, 61, 66, 33, 48, 41}, // base cell 49 (pentagon) + {50, 48, 32, 30, 70, 67, 52}, // base cell 50 + {51, 69, 54, 71, 38, 47, 34}, // base cell 51 + {52, 57, 70, 74, 32, 37, 50}, // base cell 52 + {53, 61, 65, 75, 31, 41, 44}, // base cell 53 + {54, 71, 55, 73, 34, 51, 36}, // base cell 54 + {55, 40, 54, 36, 72, 60, 73}, // base cell 55 + {56, 68, 63, 77, 35, 46, 45}, // base cell 56 + {57, 59, 74, 78, 37, 39, 52}, // base cell 57 + {58, INVALID_BASE_CELL, 62, 76, 44, 65, 42}, // base cell 58 (pentagon) + {59, 63, 78, 79, 39, 45, 57}, // base cell 59 + {60, 72, 68, 80, 40, 55, 46}, // base cell 60 + {61, 53, 49, 41, 81, 75, 66}, // base cell 61 + {62, 43, 58, 42, 82, 64, 76}, // base cell 62 + {63, INVALID_BASE_CELL, 56, 45, 79, 59, 77}, // base cell 63 (pentagon) + {64, 47, 62, 43, 84, 69, 82}, // base cell 64 + {65, 58, 53, 44, 86, 76, 75}, // base cell 65 + {66, 67, 81, 85, 49, 48, 61}, // base cell 66 + {67, 66, 50, 48, 87, 85, 70}, // base cell 67 + {68, 56, 60, 46, 90, 77, 80}, // base cell 68 + {69, 51, 64, 47, 89, 71, 84}, // base cell 69 + {70, 67, 52, 50, 83, 87, 74}, // base cell 70 + {71, 89, 73, 91, 51, 69, 54}, // base cell 71 + {72, INVALID_BASE_CELL, 73, 55, 80, 60, 88}, // base cell 72 (pentagon) + {73, 91, 72, 88, 54, 71, 55}, // base cell 73 + {74, 78, 83, 92, 52, 57, 70}, // base cell 74 + {75, 65, 61, 53, 94, 86, 81}, // base cell 75 + {76, 86, 82, 96, 58, 65, 62}, // base cell 76 + {77, 63, 68, 56, 93, 79, 90}, // base cell 77 + {78, 74, 59, 57, 95, 92, 79}, // base cell 78 + {79, 78, 63, 59, 93, 95, 77}, // base cell 79 + {80, 68, 72, 60, 99, 90, 88}, // base cell 80 + {81, 85, 94, 101, 61, 66, 75}, // base cell 81 + {82, 96, 84, 98, 62, 76, 64}, // base cell 82 + {83, INVALID_BASE_CELL, 74, 70, 100, 87, 92}, // base cell 83 (pentagon) + {84, 69, 82, 64, 97, 89, 98}, // base cell 84 + {85, 87, 101, 102, 66, 67, 81}, // base cell 85 + {86, 76, 75, 65, 104, 96, 94}, // base cell 86 + {87, 83, 102, 100, 67, 70, 85}, // base cell 87 + {88, 72, 91, 73, 99, 80, 105}, // base cell 88 + {89, 97, 91, 103, 69, 84, 71}, // base cell 89 + {90, 77, 80, 68, 106, 93, 99}, // base cell 90 + {91, 73, 89, 71, 105, 88, 103}, // base cell 91 + {92, 83, 78, 74, 108, 100, 95}, // base cell 92 + {93, 79, 90, 77, 109, 95, 106}, // base cell 93 + {94, 86, 81, 75, 107, 104, 101}, // base cell 94 + {95, 92, 79, 78, 109, 108, 93}, // base cell 95 + {96, 104, 98, 110, 76, 86, 82}, // base cell 96 + {97, INVALID_BASE_CELL, 98, 84, 103, 89, 111}, // base cell 97 (pentagon) + {98, 110, 97, 111, 82, 96, 84}, // base cell 98 + {99, 80, 105, 88, 106, 90, 113}, // base cell 99 + {100, 102, 83, 87, 108, 114, 92}, // base cell 100 + {101, 102, 107, 112, 81, 85, 94}, // base cell 101 + {102, 101, 87, 85, 114, 112, 100}, // base cell 102 + {103, 91, 97, 89, 116, 105, 111}, // base cell 103 + {104, 107, 110, 115, 86, 94, 96}, // base cell 104 + {105, 88, 103, 91, 113, 99, 116}, // base cell 105 + {106, 93, 99, 90, 117, 109, 113}, // base cell 106 + {107, INVALID_BASE_CELL, 101, 94, 115, 104, 112}, // base cell 107 (pentagon) + {108, 100, 95, 92, 118, 114, 109}, // base cell 108 + {109, 108, 93, 95, 117, 118, 106}, // base cell 109 + {110, 98, 104, 96, 119, 111, 115}, // base cell 110 + {111, 97, 110, 98, 116, 103, 119}, // base cell 111 + {112, 107, 102, 101, 120, 115, 114}, // base cell 112 + {113, 99, 116, 105, 117, 106, 121}, // base cell 113 + {114, 112, 100, 102, 118, 120, 108}, // base cell 114 + {115, 110, 107, 104, 120, 119, 112}, // base cell 115 + {116, 103, 119, 111, 113, 105, 121}, // base cell 116 + {117, INVALID_BASE_CELL, 109, 118, 113, 121, 106}, // base cell 117 (pentagon) + {118, 120, 108, 114, 117, 121, 109}, // base cell 118 + {119, 111, 115, 110, 121, 116, 120}, // base cell 119 + {120, 115, 114, 112, 121, 119, 118}, // base cell 120 + {121, 116, 120, 119, 117, 113, 118}, // base cell 121 + }; + + /// + /// Resolution 0 base cell lookup table for each face. + /// + /// Given the face number and a resolution 0 ijk+ coordinate in that face's + /// face-centered ijk coordinate system, gives the base cell located at that + /// coordinate and the number of 60 ccw rotations to rotate into that base + /// cell's orientation. + /// + /// Valid lookup coordinates are from(0, 0, 0) to(2, 2, 2). + /// + public static readonly BaseCellRotation[,,,] FaceIjkBaseCells = { + { + // face 0 + { + // i 0 + {(16, 0), (18, 0), (24, 0)}, // j 0 + {(33, 0), (30, 0), (32, 3)}, // j 1 + {(49, 1), (48, 3), (50, 3)} // j 2 + }, + { + // i 1 + {(8, 0), (5, 5), (10, 5)}, // j 0 + {(22, 0), (16, 0), (18, 0)}, // j 1 + {(41, 1), (33, 0), (30, 0)} // j 2 + }, + { + // i 2 + {(4, 0), (0, 5), (2, 5)}, // j 0 + {(15, 1), (8, 0), (5, 5)}, // j 1 + {(31, 1), (22, 0), (16, 0)} // j 2 + } + }, + { + // face 1 + { + // i 0 + {(2, 0), (6, 0), (14, 0)}, // j 0 + {(10, 0), (11, 0), (17, 3)}, // j 1 + {(24, 1), (23, 3), (25, 3)} // j 2 + }, + { + // i 1 + {(0, 0), (1, 5), (9, 5)}, // j 0 + {(5, 0), (2, 0), (6, 0)}, // j 1 + {(18, 1), (10, 0), (11, 0)} // j 2 + }, + { + // i 2 + {(4, 1), (3, 5), (7, 5)}, // j 0 + {(8, 1), (0, 0), (1, 5)}, // j 1 + {(16, 1), (5, 0), (2, 0)} // j 2 + } + }, + { + // face 2 + { + // i 0 + {(7, 0), (21, 0), (38, 0)}, // j 0 + {(9, 0), (19, 0), (34, 3)}, // j 1 + {(14, 1), (20, 3), (36, 3)} // j 2 + }, + { + // i 1 + {(3, 0), (13, 5), (29, 5)}, // j 0 + {(1, 0), (7, 0), (21, 0)}, // j 1 + {(6, 1), (9, 0), (19, 0)} // j 2 + }, + { + // i 2 + {(4, 2), (12, 5), (26, 5)}, // j 0 + {(0, 1), (3, 0), (13, 5)}, // j 1 + {(2, 1), (1, 0), (7, 0)} // j 2 + } + }, + { + // face 3 + { + // i 0 + {(26, 0), (42, 0), (58, 0)}, // j 0 + {(29, 0), (43, 0), (62, 3)}, // j 1 + {(38, 1), (47, 3), (64, 3)} // j 2 + }, + { + // i 1 + {(12, 0), (28, 5), (44, 5)}, // j 0 + {(13, 0), (26, 0), (42, 0)}, // j 1 + {(21, 1), (29, 0), (43, 0)} // j 2 + }, + { + // i 2 + {(4, 3), (15, 5), (31, 5)}, // j 0 + {(3, 1), (12, 0), (28, 5)}, // j 1 + {(7, 1), (13, 0), (26, 0)} // j 2 + } + }, + { + // face 4 + { + // i 0 + {(31, 0), (41, 0), (49, 0)}, // j 0 + {(44, 0), (53, 0), (61, 3)}, // j 1 + {(58, 1), (65, 3), (75, 3)} // j 2 + }, + { + // i 1 + {(15, 0), (22, 5), (33, 5)}, // j 0 + {(28, 0), (31, 0), (41, 0)}, // j 1 + {(42, 1), (44, 0), (53, 0)} // j 2 + }, + { + // i 2 + {(4, 4), (8, 5), (16, 5)}, // j 0 + {(12, 1), (15, 0), (22, 5)}, // j 1 + {(26, 1), (28, 0), (31, 0)} // j 2 + } + }, + { + // face 5 + { + // i 0 + {(50, 0), (48, 0), (49, 3)}, // j 0 + {(32, 0), (30, 3), (33, 3)}, // j 1 + {(24, 3), (18, 3), (16, 3)} // j 2 + }, + { + // i 1 + {(70, 0), (67, 0), (66, 3)}, // j 0 + {(52, 3), (50, 0), (48, 0)}, // j 1 + {(37, 3), (32, 0), (30, 3)} // j 2 + }, + { + // i 2 + {(83, 0), (87, 3), (85, 3)}, // j 0 + {(74, 3), (70, 0), (67, 0)}, // j 1 + {(57, 1), (52, 3), (50, 0)} // j 2 + } + }, + { + // face 6 + { + // i 0 + {(25, 0), (23, 0), (24, 3)}, // j 0 + {(17, 0), (11, 3), (10, 3)}, // j 1 + {(14, 3), (6, 3), (2, 3)} // j 2 + }, + { + // i 1 + {(45, 0), (39, 0), (37, 3)}, // j 0 + {(35, 3), (25, 0), (23, 0)}, // j 1 + {(27, 3), (17, 0), (11, 3)} // j 2 + }, + { + // i 2 + {(63, 0), (59, 3), (57, 3)}, // j 0 + {(56, 3), (45, 0), (39, 0)}, // j 1 + {(46, 3), (35, 3), (25, 0)} // j 2 + } + }, + { + // face 7 + { + // i 0 + {(36, 0), (20, 0), (14, 3)}, // j 0 + {(34, 0), (19, 3), (9, 3)}, // j 1 + {(38, 3), (21, 3), (7, 3)} // j 2 + }, + { + // i 1 + {(55, 0), (40, 0), (27, 3)}, // j 0 + {(54, 3), (36, 0), (20, 0)}, // j 1 + {(51, 3), (34, 0), (19, 3)} // j 2 + }, + { + // i 2 + {(72, 0), (60, 3), (46, 3)}, // j 0 + {(73, 3), (55, 0), (40, 0)}, // j 1 + {(71, 3), (54, 3), (36, 0)} // j 2 + } + }, + { + // face 8 + { + // i 0 + {(64, 0), (47, 0), (38, 3)}, // j 0 + {(62, 0), (43, 3), (29, 3)}, // j 1 + {(58, 3), (42, 3), (26, 3)} // j 2 + }, + { + // i 1 + {(84, 0), (69, 0), (51, 3)}, // j 0 + {(82, 3), (64, 0), (47, 0)}, // j 1 + {(76, 3), (62, 0), (43, 3)} // j 2 + }, + { + // i 2 + {(97, 0), (89, 3), (71, 3)}, // j 0 + {(98, 3), (84, 0), (69, 0)}, // j 1 + {(96, 3), (82, 3), (64, 0)} // j 2 + } + }, + { + // face 9 + { + // i 0 + {(75, 0), (65, 0), (58, 3)}, // j 0 + {(61, 0), (53, 3), (44, 3)}, // j 1 + {(49, 3), (41, 3), (31, 3)} // j 2 + }, + { + // i 1 + {(94, 0), (86, 0), (76, 3)}, // j 0 + {(81, 3), (75, 0), (65, 0)}, // j 1 + {(66, 3), (61, 0), (53, 3)} // j 2 + }, + { + // i 2 + {(107, 0), (104, 3), (96, 3)}, // j 0 + {(101, 3), (94, 0), (86, 0)}, // j 1 + {(85, 3), (81, 3), (75, 0)} // j 2 + } + }, + { + // face 10 + { + // i 0 + {(57, 0), (59, 0), (63, 3)}, // j 0 + {(74, 0), (78, 3), (79, 3)}, // j 1 + {(83, 3), (92, 3), (95, 3)} // j 2 + }, + { + // i 1 + {(37, 0), (39, 3), (45, 3)}, // j 0 + {(52, 0), (57, 0), (59, 0)}, // j 1 + {(70, 3), (74, 0), (78, 3)} // j 2 + }, + { + // i 2 + {(24, 0), (23, 3), (25, 3)}, // j 0 + {(32, 3), (37, 0), (39, 3)}, // j 1 + {(50, 3), (52, 0), (57, 0)} // j 2 + } + }, + { + // face 11 + { + // i 0 + {(46, 0), (60, 0), (72, 3)}, // j 0 + {(56, 0), (68, 3), (80, 3)}, // j 1 + {(63, 3), (77, 3), (90, 3)} // j 2 + }, + { + // i 1 + {(27, 0), (40, 3), (55, 3)}, // j 0 + {(35, 0), (46, 0), (60, 0)}, // j 1 + {(45, 3), (56, 0), (68, 3)} // j 2 + }, + { + // i 2 + {(14, 0), (20, 3), (36, 3)}, // j 0 + {(17, 3), (27, 0), (40, 3)}, // j 1 + {(25, 3), (35, 0), (46, 0)} // j 2 + } + }, + { + // face 12 + { + // i 0 + {(71, 0), (89, 0), (97, 3)}, // j 0 + {(73, 0), (91, 3), (103, 3)}, // j 1 + {(72, 3), (88, 3), (105, 3)} // j 2 + }, + { + // i 1 + {(51, 0), (69, 3), (84, 3)}, // j 0 + {(54, 0), (71, 0), (89, 0)}, // j 1 + {(55, 3), (73, 0), (91, 3)} // j 2 + }, + { + // i 2 + {(38, 0), (47, 3), (64, 3)}, // j 0 + {(34, 3), (51, 0), (69, 3)}, // j 1 + {(36, 3), (54, 0), (71, 0)} // j 2 + } + }, + { + // face 13 + { + // i 0 + {(96, 0), (104, 0), (107, 3)}, // j 0 + {(98, 0), (110, 3), (115, 3)}, // j 1 + {(97, 3), (111, 3), (119, 3)} // j 2 + }, + { + // i 1 + {(76, 0), (86, 3), (94, 3)}, // j 0 + {(82, 0), (96, 0), (104, 0)}, // j 1 + {(84, 3), (98, 0), (110, 3)} // j 2 + }, + { + // i 2 + {(58, 0), (65, 3), (75, 3)}, // j 0 + {(62, 3), (76, 0), (86, 3)}, // j 1 + {(64, 3), (82, 0), (96, 0)} // j 2 + } + }, + { + // face 14 + { + // i 0 + {(85, 0), (87, 0), (83, 3)}, // j 0 + {(101, 0), (102, 3), (100, 3)}, // j 1 + {(107, 3), (112, 3), (114, 3)} // j 2 + }, + { + // i 1 + {(66, 0), (67, 3), (70, 3)}, // j 0 + {(81, 0), (85, 0), (87, 0)}, // j 1 + {(94, 3), (101, 0), (102, 3)} // j 2 + }, + { + // i 2 + {(49, 0), (48, 3), (50, 3)}, // j 0 + {(61, 3), (66, 0), (67, 3)}, // j 1 + {(75, 3), (81, 0), (85, 0)} // j 2 + } + }, + { + // face 15 + { + // i 0 + {(95, 0), (92, 0), (83, 0)}, // j 0 + {(79, 0), (78, 0), (74, 3)}, // j 1 + {(63, 1), (59, 3), (57, 3)} // j 2 + }, + { + // i 1 + {(109, 0), (108, 0), (100, 5)}, // j 0 + {(93, 1), (95, 0), (92, 0)}, // j 1 + {(77, 1), (79, 0), (78, 0)} // j 2 + }, + { + // i 2 + {(117, 4), (118, 5), (114, 5)}, // j 0 + {(106, 1), (109, 0), (108, 0)}, // j 1 + {(90, 1), (93, 1), (95, 0)} // j 2 + } + }, + { + // face 16 + { + // i 0 + {(90, 0), (77, 0), (63, 0)}, // j 0 + {(80, 0), (68, 0), (56, 3)}, // j 1 + {(72, 1), (60, 3), (46, 3)} // j 2 + }, + { + // i 1 + {(106, 0), (93, 0), (79, 5)}, // j 0 + {(99, 1), (90, 0), (77, 0)}, // j 1 + {(88, 1), (80, 0), (68, 0)} // j 2 + }, + { + // i 2 + {(117, 3), (109, 5), (95, 5)}, // j 0 + {(113, 1), (106, 0), (93, 0)}, // j 1 + {(105, 1), (99, 1), (90, 0)} // j 2 + } + }, + { + // face 17 + { + // i 0 + {(105, 0), (88, 0), (72, 0)}, // j 0 + {(103, 0), (91, 0), (73, 3)}, // j 1 + {(97, 1), (89, 3), (71, 3)} // j 2 + }, + { + // i 1 + {(113, 0), (99, 0), (80, 5)}, // j 0 + {(116, 1), (105, 0), (88, 0)}, // j 1 + {(111, 1), (103, 0), (91, 0)} // j 2 + }, + { + // i 2 + {(117, 2), (106, 5), (90, 5)}, // j 0 + {(121, 1), (113, 0), (99, 0)}, // j 1 + {(119, 1), (116, 1), (105, 0)} // j 2 + } + }, + { + // face 18 + { + // i 0 + {(119, 0), (111, 0), (97, 0)}, // j 0 + {(115, 0), (110, 0), (98, 3)}, // j 1 + {(107, 1), (104, 3), (96, 3)} // j 2 + }, + { + // i 1 + {(121, 0), (116, 0), (103, 5)}, // j 0 + {(120, 1), (119, 0), (111, 0)}, // j 1 + {(112, 1), (115, 0), (110, 0)} // j 2 + }, + { + // i 2 + {(117, 1), (113, 5), (105, 5)}, // j 0 + {(118, 1), (121, 0), (116, 0)}, // j 1 + {(114, 1), (120, 1), (119, 0)} // j 2 + } + }, + { + // face 19 + { + // i 0 + {(114, 0), (112, 0), (107, 0)}, // j 0 + {(100, 0), (102, 0), (101, 3)}, // j 1 + {(83, 1), (87, 3), (85, 3)} // j 2 + }, + { + // i 1 + {(118, 0), (120, 0), (115, 5)}, // j 0 + {(108, 1), (114, 0), (112, 0)}, // j 1 + {(92, 1), (100, 0), (102, 0)} // j 2 + }, + { + // i 2 + {(117, 0), (121, 5), (119, 5)}, // j 0 + {(109, 1), (118, 0), (120, 0)}, // j 1 + {(95, 1), (108, 1), (114, 0)} // j 2 + } + } + }; + + #endregion basecells + + #region coordinates + unit vectors + public static readonly (int, int, int)[] UnitVectors = { + (0, 0, 0), // Center + (0, 0, 1), // K + (0, 1, 0), // J + (0, 1, 1), // JK + (1, 0, 0), // I + (1, 0, 1), // IK + (1, 1, 0) // IJ + }; + + /// + /// The vertexes of an origin-centered cell in a Class II resolution on a + /// substrate grid with aperture sequence 33r. The aperture 3 gets us the + /// vertices, and the 3r gets us back to Class II. vertices listed ccw + /// from the i-axes + /// + public static readonly (int, int, int)[] Class2HexVertices = { + (2, 1, 0), + (1, 2, 0), + (0, 2, 1), + (0, 1, 2), + (1, 0, 2), + (2, 0, 1) + }; + + /// + /// the vertexes of an origin-centered cell in a Class III resolution on a + /// substrate grid with aperture sequence 33r7r. The aperture 3 gets us the + /// vertices, and the 3r7r gets us to Class II. vertices listed ccw from + /// the i-axes + /// + public static readonly (int, int, int)[] Class3HexVertices = { + (5, 4, 0), + (1, 5, 0), + (0, 5, 4), + (0, 1, 5), + (4, 0, 5), + (5, 0, 1) + }; + + /// + /// the vertexes of an origin-centered pentagon in a Class II resolution on a + /// substrate grid with aperture sequence 33r. The aperture 3 gets us the + /// vertices, and the 3r gets us back to Class II. vertices listed ccw from + /// the i-axes + /// + public static readonly (int, int, int)[] Class2PentagonVertices = { + (2, 1, 0), + (1, 2, 0), + (0, 2, 1), + (0, 1, 2), + (1, 0, 2) + }; + + /// + /// the vertexes of an origin-centered pentagon in a Class III resolution on + /// a substrate grid with aperture sequence 33r7r. The aperture 3 gets us the + /// vertices, and the 3r7r gets us to Class II. vertices listed ccw from the + /// i-axes + /// + public static readonly (int, int, int)[] Class3PentagonVertices = { + (5, 4, 0), + (1, 5, 0), + (0, 5, 4), + (0, 1, 5), + (4, 0, 5) + }; + + #endregion coordinates + unit vectors + + #region faces + + public static readonly double[] AxisAzimuths = { + 5.619958268523939882, // face 0 + 5.760339081714187279, // face 1 + 0.780213654393430055, // face 2 + 0.430469363979999913, // face 3 + 6.130269123335111400, // face 4 + 2.692877706530642877, // face 5 + 2.982963003477243874, // face 6 + 3.532912002790141181, // face 7 + 3.494305004259568154, // face 8 + 3.003214169499538391, // face 9 + 5.930472956509811562, // face 10 + 0.138378484090254847, // face 11 + 0.448714947059150361, // face 12 + 0.158629650112549365, // face 13 + 5.891865957979238535, // face 14 + 2.711123289609793325, // face 15 + 3.294508837434268316, // face 16 + 3.804819692245439833, // face 17 + 3.664438879055192436, // face 18 + 2.361378999196363184, // face 19 + }; + + public const int IJ = 1; + public const int KI = 2; + public const int JK = 3; + + public static readonly int[,] AdjacentFaceDirections = { + {0, KI, -1, -1, IJ, JK, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}, // face 0 + {IJ, 0, KI, -1, -1, -1, JK, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}, // face 1 + {-1, IJ, 0, KI, -1, -1, -1, JK, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}, // face 2 + {-1, -1, IJ, 0, KI, -1, -1, -1, JK, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}, // face 3 + {KI, -1, -1, IJ, 0, -1, -1, -1, -1, JK, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}, // face 4 + {JK, -1, -1, -1, -1, 0, -1, -1, -1, -1, + IJ, -1, -1, -1, KI, -1, -1, -1, -1, -1}, // face 5 + {-1, JK, -1, -1, -1, -1, 0, -1, -1, -1, + KI, IJ, -1, -1, -1, -1, -1, -1, -1, -1}, // face 6 + {-1, -1, JK, -1, -1, -1, -1, 0, -1, -1, + -1, KI, IJ, -1, -1, -1, -1, -1, -1, -1}, // face 7 + {-1, -1, -1, JK, -1, -1, -1, -1, 0, -1, + -1, -1, KI, IJ, -1, -1, -1, -1, -1, -1}, // face 8 + {-1, -1, -1, -1, JK, -1, -1, -1, -1, 0, + -1, -1, -1, KI, IJ, -1, -1, -1, -1, -1}, // face 9 + {-1, -1, -1, -1, -1, IJ, KI, -1, -1, -1, + 0, -1, -1, -1, -1, JK, -1, -1, -1, -1}, // face 10 + {-1, -1, -1, -1, -1, -1, IJ, KI, -1, -1, + -1, 0, -1, -1, -1, -1, JK, -1, -1, -1}, // face 11 + {-1, -1, -1, -1, -1, -1, -1, IJ, KI, -1, + -1, -1, 0, -1, -1, -1, -1, JK, -1, -1}, // face 12 + {-1, -1, -1, -1, -1, -1, -1, -1, IJ, KI, + -1, -1, -1, 0, -1, -1, -1, -1, JK, -1}, // face 13 + {-1, -1, -1, -1, -1, KI, -1, -1, -1, IJ, + -1, -1, -1, -1, 0, -1, -1, -1, -1, JK}, // face 14 + {-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + JK, -1, -1, -1, -1, 0, IJ, -1, -1, KI}, // face 15 + {-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, JK, -1, -1, -1, KI, 0, IJ, -1, -1}, // face 16 + {-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, JK, -1, -1, -1, KI, 0, IJ, -1}, // face 17 + {-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, JK, -1, -1, -1, KI, 0, IJ}, // face 18 + {-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, JK, IJ, -1, -1, KI, 0} // face 19 + }; + + public static readonly (int, (int, int, int), int)[,] OrientedFaceNeighbours = { + { + // face 0 + (0, (0, 0, 0), 0), // central face + (4, (2, 0, 2), 1), // ij quadrant + (1, (2, 2, 0), 5), // ki quadrant + (5, (0, 2, 2), 3) // jk quadrant + }, + { + // face 1 + (1, (0, 0, 0), 0), // central face + (0, (2, 0, 2), 1), // ij quadrant + (2, (2, 2, 0), 5), // ki quadrant + (6, (0, 2, 2), 3) // jk quadrant + }, + { + // face 2 + (2, (0, 0, 0), 0), // central face + (1, (2, 0, 2), 1), // ij quadrant + (3, (2, 2, 0), 5), // ki quadrant + (7, (0, 2, 2), 3) // jk quadrant + }, + { + // face 3 + (3, (0, 0, 0), 0), // central face + (2, (2, 0, 2), 1), // ij quadrant + (4, (2, 2, 0), 5), // ki quadrant + (8, (0, 2, 2), 3) // jk quadrant + }, + { + // face 4 + (4, (0, 0, 0), 0), // central face + (3, (2, 0, 2), 1), // ij quadrant + (0, (2, 2, 0), 5), // ki quadrant + (9, (0, 2, 2), 3) // jk quadrant + }, + { + // face 5 + (5, (0, 0, 0), 0), // central face + (10, (2, 2, 0), 3), // ij quadrant + (14, (2, 0, 2), 3), // ki quadrant + (0, (0, 2, 2), 3) // jk quadrant + }, + { + // face 6 + (6, (0, 0, 0), 0), // central face + (11, (2, 2, 0), 3), // ij quadrant + (10, (2, 0, 2), 3), // ki quadrant + (1, (0, 2, 2), 3) // jk quadrant + }, + { + // face 7 + (7, (0, 0, 0), 0), // central face + (12, (2, 2, 0), 3), // ij quadrant + (11, (2, 0, 2), 3), // ki quadrant + (2, (0, 2, 2), 3) // jk quadrant + }, + { + // face 8 + (8, (0, 0, 0), 0), // central face + (13, (2, 2, 0), 3), // ij quadrant + (12, (2, 0, 2), 3), // ki quadrant + (3, (0, 2, 2), 3) // jk quadrant + }, + { + // face 9 + (9, (0, 0, 0), 0), // central face + (14, (2, 2, 0), 3), // ij quadrant + (13, (2, 0, 2), 3), // ki quadrant + (4, (0, 2, 2), 3) // jk quadrant + }, + { + // face 10 + (10, (0, 0, 0), 0), // central face + (5, (2, 2, 0), 3), // ij quadrant + (6, (2, 0, 2), 3), // ki quadrant + (15, (0, 2, 2), 3) // jk quadrant + }, + { + // face 11 + (11, (0, 0, 0), 0), // central face + (6, (2, 2, 0), 3), // ij quadrant + (7, (2, 0, 2), 3), // ki quadrant + (16, (0, 2, 2), 3) // jk quadrant + }, + { + // face 12 + (12, (0, 0, 0), 0), // central face + (7, (2, 2, 0), 3), // ij quadrant + (8, (2, 0, 2), 3), // ki quadrant + (17, (0, 2, 2), 3) // jk quadrant + }, + { + // face 13 + (13, (0, 0, 0), 0), // central face + (8, (2, 2, 0), 3), // ij quadrant + (9, (2, 0, 2), 3), // ki quadrant + (18, (0, 2, 2), 3) // jk quadrant + }, + { + // face 14 + (14, (0, 0, 0), 0), // central face + (9, (2, 2, 0), 3), // ij quadrant + (5, (2, 0, 2), 3), // ki quadrant + (19, (0, 2, 2), 3) // jk quadrant + }, + { + // face 15 + (15, (0, 0, 0), 0), // central face + (16, (2, 0, 2), 1), // ij quadrant + (19, (2, 2, 0), 5), // ki quadrant + (10, (0, 2, 2), 3) // jk quadrant + }, + { + // face 16 + (16, (0, 0, 0), 0), // central face + (17, (2, 0, 2), 1), // ij quadrant + (15, (2, 2, 0), 5), // ki quadrant + (11, (0, 2, 2), 3) // jk quadrant + }, + { + // face 17 + (17, (0, 0, 0), 0), // central face + (18, (2, 0, 2), 1), // ij quadrant + (16, (2, 2, 0), 5), // ki quadrant + (12, (0, 2, 2), 3) // jk quadrant + }, + { + // face 18 + (18, (0, 0, 0), 0), // central face + (19, (2, 0, 2), 1), // ij quadrant + (17, (2, 2, 0), 5), // ki quadrant + (13, (0, 2, 2), 3) // jk quadrant + }, + { + // face 19 + (19, (0, 0, 0), 0), // central face + (15, (2, 0, 2), 1), // ij quadrant + (18, (2, 2, 0), 5), // ki quadrant + (14, (0, 2, 2), 3) // jk quadrant + } + }; + + public static readonly (double, double)[] GeoFaceCenters = { + (0.803582649718989942, 1.248397419617396099), // face 0 + (1.307747883455638156, 2.536945009877921159), // face 1 + (1.054751253523952054, -1.347517358900396623), // face 2 + (0.600191595538186799, -0.450603909469755746), // face 3 + (0.491715428198773866, 0.401988202911306943), // face 4 + (0.172745327415618701, 1.678146885280433686), // face 5 + (0.605929321571350690, 2.953923329812411617), // face 6 + (0.427370518328979641, -1.888876200336285401), // face 7 + (-0.079066118549212831, -0.733429513380867741), // face 8 + (-0.230961644455383637, 0.506495587332349035), // face 9 + (0.079066118549212831, 2.408163140208925497), // face 10 + (0.230961644455383637, -2.635097066257444203), // face 11 + (-0.172745327415618701, -1.463445768309359553), // face 12 + (-0.605929321571350690, -0.187669323777381622), // face 13 + (-0.427370518328979641, 1.252716453253507838), // face 14 + (-0.600191595538186799, 2.690988744120037492), // face 15 + (-0.491715428198773866, -2.739604450678486295), // face 16 + (-0.803582649718989942, -1.893195233972397139), // face 17 + (-1.307747883455638156, -0.604647643711872080), // face 18 + (-1.054751253523952054, 1.794075294689396615), // face 19 + }; + + public static readonly (double, double, double)[] FaceCenters = { + (0.2199307791404606, 0.6583691780274996, 0.7198475378926182), // face 0 + (-0.2139234834501421, 0.1478171829550703, 0.9656017935214205), // face 1 + (0.1092625278784797, -0.4811951572873210, 0.8697775121287253), // face 2 + (0.7428567301586791, -0.3593941678278028, 0.5648005936517033), // face 3 + (0.8112534709140969, 0.3448953237639384, 0.4721387736413930), // face 4 + (-0.1055498149613921, 0.9794457296411413, 0.1718874610009365), // face 5 + (-0.8075407579970092, 0.1533552485898818, 0.5695261994882688), // face 6 + (-0.2846148069787907, -0.8644080972654206, 0.4144792552473539), // face 7 + (0.7405621473854482, -0.6673299564565524, -0.0789837646326737), // face 8 + (0.8512303986474293, 0.4722343788582681, -0.2289137388687808), // face 9 + (-0.7405621473854481, 0.6673299564565524, 0.0789837646326737), // face 10 + (-0.8512303986474292, -0.4722343788582682, 0.2289137388687808), // face 11 + (0.1055498149613919, -0.9794457296411413, -0.1718874610009365), // face 12 + (0.8075407579970092, -0.1533552485898819, -0.5695261994882688), // face 13 + (0.2846148069787908, 0.8644080972654204, -0.4144792552473539), // face 14 + (-0.7428567301586791, 0.3593941678278027, -0.5648005936517033), // face 15 + (-0.8112534709140971, -0.3448953237639382, -0.4721387736413930), // face 16 + (-0.2199307791404607, -0.6583691780274996, -0.7198475378926182), // face 17 + (0.2139234834501420, -0.1478171829550704, -0.9656017935214205), // face 18 + (-0.1092625278784796, 0.4811951572873210, -0.8697775121287253), // face 19 + }; + + /// + /// Table of direction-to-face mapping for each pentagon. Note that + /// faces are in directional order, starting at J_AXES_DIGIT. + /// + public static readonly (int, (int, int, int, int, int))[] PentagonDirectionFaces = { + (4, (4, 0, 2, 1, 3)), + (14, (6, 11, 2, 7, 1)), + (24, (5, 10, 1, 6, 0)), + (38, (7, 12, 3, 8, 2)), + (49, (9, 14, 0, 5, 4)), + (58, (8, 13, 4, 9, 3)), + (63, (11, 6, 15, 10, 16)), + (72, (12, 7, 16, 11, 17)), + (83, (10, 5, 19, 14, 15)), + (97, (13, 8, 17, 12, 18)), + (107, (14, 9, 18, 13, 19)), + (117, (15, 19, 17, 18, 16)) + }; + + + #endregion faces + + #region other + public static readonly int[] MaxDistanceByClass2Res = { + 2, // res 0 + -1, // res 1 + 14, // res 2 + -1, // res 3 + 98, // res 4 + -1, // res 5 + 686, // res 6 + -1, // res 7 + 4802, // res 8 + -1, // res 9 + 33614, // res 10 + -1, // res 11 + 235298, // res 12 + -1, // res 13 + 1647086, // res 14 + -1, // res 15 + 11529602 // res 16 + }; + + public static readonly int[] UnitScaleByClass2Res = { + 1, // res 0 + -1, // res 1 + 7, // res 2 + -1, // res 3 + 49, // res 4 + -1, // res 5 + 343, // res 6 + -1, // res 7 + 2401, // res 8 + -1, // res 9 + 16807, // res 10 + -1, // res 11 + 117649, // res 12 + -1, // res 13 + 823543, // res 14 + -1, // res 15 + 5764801 // res 16 + }; + + /// + /// Directions used for traversing a hexagonal ring counterclockwise around + /// {1, 0, 0}. + /// + ///
+        ///       _
+        ///     _/ \\_
+        ///    / \\5/ \\
+        ///    \\0/ \\4/
+        ///    / \\_/ \\
+        ///    \\1/ \\3/
+        ///      \\2/
+        /// 
+ ///
+ public static readonly Direction[] CounterClockwiseDirections = { + Direction.J, + Direction.JK, + Direction.K, + Direction.IK, + Direction.I, + Direction.IJ + }; + + /// + /// Direction used for traversing to the next outward hexagonal ring. + /// + public const Direction NextRingDirection = Direction.I; + + /// + /// New digit when traversing along class II grids. + /// + /// Current digit -> direction -> new digit. + /// + public static readonly Direction[,] NewDirectionClass2 = { + { + Direction.Center, Direction.K, Direction.J, Direction.JK, Direction.I, + Direction.IK, Direction.IJ + }, + { + Direction.K, Direction.I, Direction.JK, Direction.IJ, Direction.IK, + Direction.J, Direction.Center + }, + { + Direction.J, Direction.JK, Direction.K, Direction.I, Direction.IJ, + Direction.Center, Direction.IK + }, + { + Direction.JK, Direction.IJ, Direction.I, Direction.IK, Direction.Center, + Direction.K, Direction.J + }, + { + Direction.I, Direction.IK, Direction.IJ, Direction.Center, Direction.J, + Direction.JK, Direction.K + }, + { + Direction.IK, Direction.J, Direction.Center, Direction.K, Direction.JK, + Direction.IJ, Direction.I + }, + { + Direction.IJ, Direction.Center, Direction.IK, Direction.J, Direction.K, + Direction.I, Direction.JK + } + }; + + /// + /// New traversal direction when traversing along class II grids. + /// + /// Current digit -> direction -> new ap7 move (at coarser level). + /// + public static readonly Direction[,] NewAdjustmentClass2 = { + { + Direction.Center, Direction.Center, Direction.Center, Direction.Center, Direction.Center, + Direction.Center, Direction.Center + }, + { + Direction.Center, Direction.K, Direction.Center, Direction.K, Direction.Center, + Direction.IK, Direction.Center + }, + { + Direction.Center, Direction.Center, Direction.J, Direction.JK, Direction.Center, + Direction.Center, Direction.J + }, + { + Direction.Center, Direction.K, Direction.JK, Direction.JK, Direction.Center, + Direction.Center, Direction.Center + }, + { + Direction.Center, Direction.Center, Direction.Center, Direction.Center, Direction.I, + Direction.I, Direction.IJ + }, + { + Direction.Center, Direction.IK, Direction.Center, Direction.Center, Direction.I, + Direction.IK, Direction.Center + }, + { + Direction.Center, Direction.Center, Direction.J, Direction.Center, Direction.IJ, + Direction.Center, Direction.IJ + } + }; + + /// + /// New traversal direction when traversing along class III grids. + /// + /// Current digit -> direction -> new digit. + /// + public static readonly Direction[,] NewDirectionClass3 = { + { + Direction.Center, Direction.K, Direction.J, Direction.JK, Direction.I, + Direction.IK, Direction.IJ + }, + { + Direction.K, Direction.J, Direction.JK, Direction.I, Direction.IK, + Direction.IJ, Direction.Center + }, + { + Direction.J, Direction.JK, Direction.I, Direction.IK, Direction.IJ, + Direction.Center, Direction.K + }, + { + Direction.JK, Direction.I, Direction.IK, Direction.IJ, Direction.Center, + Direction.K, Direction.J + }, + { + Direction.I, Direction.IK, Direction.IJ, Direction.Center, Direction.K, + Direction.J, Direction.JK + }, + { + Direction.IK, Direction.IJ, Direction.Center, Direction.K, Direction.J, + Direction.JK, Direction.I + }, + { + Direction.IJ, Direction.Center, Direction.K, Direction.J, Direction.JK, + Direction.I, Direction.IK + } + }; + + /// + /// New traversal direction when traversing along class III grids. + /// + /// Current digit -> direction -> new ap7 move (at coarser level). + /// + public static readonly Direction[,] NewAdjustmentClass3 = { + { + Direction.Center, Direction.Center, Direction.Center, Direction.Center, Direction.Center, + Direction.Center, Direction.Center + }, + { + Direction.Center, Direction.K, Direction.Center, Direction.JK, Direction.Center, + Direction.K, Direction.Center + }, + { + Direction.Center, Direction.Center, Direction.J, Direction.J, Direction.Center, + Direction.Center, Direction.IJ + }, + { + Direction.Center, Direction.JK, Direction.J, Direction.JK, Direction.Center, + Direction.Center, Direction.Center + }, + { + Direction.Center, Direction.Center, Direction.Center, Direction.Center, Direction.I, + Direction.IK, Direction.I + }, + { + Direction.Center, Direction.K, Direction.Center, Direction.Center, Direction.IK, + Direction.IK, Direction.Center + }, + { + Direction.Center, Direction.Center, Direction.IJ, Direction.Center, Direction.I, + Direction.Center, Direction.IJ + } + }; + + /// + /// Origin leading digit -> index leading digit -> rotations 60 cw + /// Either being 1 (K axis) is invalid. + /// No good default at 0. + /// + public static readonly int[,] PentagonRotations = { + {0, -1, 0, 0, 0, 0, 0}, // 0 + {-1, -1, -1, -1, -1, -1, -1}, // 1 + {0, -1, 0, 0, 0, 1, 0}, // 2 + {0, -1, 0, 0, 1, 1, 0}, // 3 + {0, -1, 0, 5, 0, 0, 0}, // 4 + {0, -1, 5, 5, 0, 0, 0}, // 5 + {0, -1, 0, 0, 0, 0, 0}, // 6 + }; + + /// + /// Reverse base cell direction -> leading index digit -> rotations 60 ccw. + /// For reversing the rotation introduced in PnetagonRotations when + /// the origin is on a pentagon (regardless of the base cell of the index.) + /// + public static readonly int[,] PentagonRotationsInReverse = { + {0, 0, 0, 0, 0, 0, 0}, // 0 + {-1, -1, -1, -1, -1, -1, -1}, // 1 + {0, 1, 0, 0, 0, 0, 0}, // 2 + {0, 1, 0, 0, 0, 1, 0}, // 3 + {0, 5, 0, 0, 0, 0, 0}, // 4 + {0, 5, 0, 5, 0, 0, 0}, // 5 + {0, 0, 0, 0, 0, 0, 0}, // 6 + }; + + /// + /// Reverse base cell direction -> leading index digit -> rotations 60 ccw. + /// For reversing the rotation introduced in PentagonRotations when the index is + /// on a pentagon and the origin is not. + /// + public static readonly int[,] NonPolarPentagonRotationsInReverse = { + {0, 0, 0, 0, 0, 0, 0}, // 0 + {-1, -1, -1, -1, -1, -1, -1}, // 1 + {0, 1, 0, 0, 0, 0, 0}, // 2 + {0, 1, 0, 0, 0, 1, 0}, // 3 + {0, 5, 0, 0, 0, 0, 0}, // 4 + {0, 1, 0, 5, 1, 1, 0}, // 5 + {0, 0, 0, 0, 0, 0, 0}, // 6 + }; + + /// + /// Reverse base cell direction -> leading index digit -> rotations 60 ccw. + /// For reversing the rotation introduced in PentagonRotations when the index is + /// on a polar pentagon and the origin is not. + /// + public static readonly int[,] PolarPentagonRotationsInReverse = { + {0, 0, 0, 0, 0, 0, 0}, // 0 + {-1, -1, -1, -1, -1, -1, -1}, // 1 + {0, 1, 1, 1, 1, 1, 1}, // 2 + {0, 1, 0, 0, 0, 1, 0}, // 3 + {0, 1, 0, 0, 1, 1, 1}, // 4 + {0, 1, 0, 5, 1, 1, 0}, // 5 + {0, 1, 1, 0, 1, 1, 1}, // 6 + }; + + /// + /// Prohibited directions when unfolding a pentagon. + /// + /// + /// Indexes by two directions, both relative to the pentagon base cell. The first + /// is the direction of the origin index and the second is the direction of the + /// index to unfold. Direction refers to the direction from base cell to base + /// cell if the indexes are on different base cells, or the leading digit if + /// within the pentagon base cell. + /// + /// This previously included a Class II/Class III check but these were removed + /// due to failure cases. It's possible this could be restricted to a narrower + /// set of a failure cases. Currently, the logic is any unfolding across more + /// than one icosahedron face is not permitted. + /// + public static readonly bool[,] UnfoldableDirections = { + {false, false, false, false, false, false, false}, // 0 + {false, false, false, false, false, false, false}, // 1 + {false, false, false, false, true, true, false}, // 2 + {false, false, false, false, true, false, true}, // 3 + {false, false, true, true, false, false, false}, // 4 + {false, false, true, false, false, false, true}, // 5 + {false, false, false, true, false, true, false}, // 6 + }; + + /// + /// The area of hexagon cells at each resolution in km^2 + /// + public static readonly double[] HexgonAreasInKm2 = { + 4250546.848, 607220.9782, 86745.85403, 12392.26486, + 1770.323552, 252.9033645, 36.1290521, 5.1612932, + 0.7373276, 0.1053325, 0.0150475, 0.0021496, + 0.0003071, 0.0000439, 0.0000063, 0.0000009 + }; + + /// + /// The area of hexagon cells at each resolution in m^2 + /// + public static readonly double[] HexagonAreasInM2 = { + 4.25055E+12, 6.07221E+11, 86745854035, 12392264862, + 1770323552, 252903364.5, 36129052.1, 5161293.2, + 737327.6, 105332.5, 15047.5, 2149.6, + 307.1, 43.9, 6.3, 0.9 + }; + + /// + /// TODO figure out what these are actually used for and doc accordingly + /// + public static readonly double[] EdgeLengthsInKm = { + 1107.712591, 418.6760055, 158.2446558, 59.81085794, + 22.6063794, 8.544408276, 3.229482772, 1.220629759, + 0.461354684, 0.174375668, 0.065907807, 0.024910561, + 0.009415526, 0.003559893, 0.001348575, 0.000509713 + }; + + /// + /// TODO figure out what these are actually used for and doc accordingly + /// + public static readonly double[] EdgeLengthsInM = { + 1107712.591, 418676.0055, 158244.6558, 59810.85794, + 22606.3794, 8544.408276, 3229.482772, 1220.629759, + 461.3546837, 174.3756681, 65.90780749, 24.9105614, + 9.415526211, 3.559893033, 1.348574562, 0.509713273 + }; + + #endregion other + + } +} diff --git a/src/H3/Algorithms/Lines.cs b/src/H3/Algorithms/Lines.cs index 386cfe3..9006070 100644 --- a/src/H3/Algorithms/Lines.cs +++ b/src/H3/Algorithms/Lines.cs @@ -19,8 +19,8 @@ public static class Lines { /// grid distance in cells; -1 if could not be computed public static int DistanceTo(this H3Index origin, H3Index destination) { try { - CoordIJK originIJK = LocalCoordIJK.ToLocalIJK(origin, origin); - CoordIJK destinationIJK = LocalCoordIJK.ToLocalIJK(origin, destination); + var originIJK = LocalCoordIJK.ToLocalIJK(origin, origin); + var destinationIJK = LocalCoordIJK.ToLocalIJK(origin, destination); return originIJK.GetDistanceTo(destinationIJK); } catch { @@ -49,6 +49,8 @@ public static int DistanceTo(this H3Index origin, H3Index destination) { public static IEnumerable LineTo(this H3Index origin, H3Index destination) { CoordIJK startIjk; CoordIJK endIjk; + var workIjk1 = new CoordIJK(); + var workIjk2 = new CoordIJK(); // translate to local coordinates try { @@ -81,7 +83,7 @@ public static IEnumerable LineTo(this H3Index origin, H3Index destinati startK + kStep * n, endIjk ).Uncube(); - yield return LocalCoordIJK.ToH3Index(origin, endIjk); + yield return LocalCoordIJK.ToH3Index(origin, endIjk, startIjk, workIjk1, workIjk2); } } diff --git a/src/H3/Algorithms/Polyfill.cs b/src/H3/Algorithms/Polyfill.cs index a9c7336..3d06fb3 100644 --- a/src/H3/Algorithms/Polyfill.cs +++ b/src/H3/Algorithms/Polyfill.cs @@ -1,5 +1,7 @@ using System; using System.Collections.Generic; +using System.Linq; +using System.Runtime.CompilerServices; using H3.Extensions; using H3.Model; using static H3.Constants; @@ -9,6 +11,7 @@ #nullable enable +[assembly: InternalsVisibleTo("H3.Benchmarks")] namespace H3.Algorithms { internal sealed class PositiveLonFilter : ICoordinateSequenceFilter { @@ -18,7 +21,7 @@ internal sealed class PositiveLonFilter : ICoordinateSequenceFilter { public bool GeometryChanged => true; public void Filter(CoordinateSequence seq, int i) { - double x = seq.GetX(i); + var x = seq.GetX(i); seq.SetOrdinate(i, Ordinate.X, x < 0 ? x + 360.0 : x); } @@ -31,20 +34,45 @@ internal sealed class NegativeLonFilter : ICoordinateSequenceFilter { public bool GeometryChanged => true; public void Filter(CoordinateSequence seq, int i) { - double x = seq.GetX(i); + var x = seq.GetX(i); seq.SetOrdinate(i, Ordinate.X, x > 0 ? x - 360.0 : x); } } + /// + /// The vertex testing mode to use when checking containment during + /// polyfill operations. + /// + public enum VertexTestMode { + /// + /// Specifies that the index's center vertex should be contained + /// within the geometry. This matches the polyfill behaviour of + /// the upstream library. + /// + Center, + + /// + /// Specifies that any of the index's boundary vertices can be + /// contained within the geometry. + /// + Any, + + /// + /// Specifies that all of the index's boundary vertices must be + /// contained within the geometry. + /// + All + } + /// /// Polyfill algorithms for H3Index. /// public static class Polyfill { - private static readonly ICoordinateSequenceFilter _negativeLonFilter = new NegativeLonFilter(); + private static readonly ICoordinateSequenceFilter NegativeLonFilter = new NegativeLonFilter(); - private static readonly ICoordinateSequenceFilter _positiveLonFilter = new PositiveLonFilter(); + private static readonly ICoordinateSequenceFilter PositiveLonFilter = new PositiveLonFilter(); /// /// Returns all of the H3 indexes that are contained within the provided @@ -52,19 +80,36 @@ public static class Polyfill { /// /// Containment polygon /// H3 resolution - /// Indicies where center point is contained within polygon - public static IEnumerable Fill(this Geometry polygon, int resolution) { - bool isTransMeridian = polygon.IsTransMeridian(); + /// Specify which to use when checking + /// index vertex containment. Defaults to . + /// Indices that are contained within polygon + public static IEnumerable Fill(this Geometry polygon, int resolution, VertexTestMode testMode = VertexTestMode.Center) { + if (polygon.IsEmpty) return Enumerable.Empty(); + var isTransMeridian = polygon.IsTransMeridian(); var testPoly = isTransMeridian ? SplitGeometry(polygon) : polygon; HashSet searched = new(); + Stack toSearch = new(); + toSearch.Push(testPoly.InteriorPoint.Coordinate.ToH3Index(resolution)); + IndexedPointInAreaLocator locator = new(testPoly); - Stack toSearch = new(TraceCoordinates(testPoly.Coordinates, resolution)); - if (toSearch.Count == 0 && !testPoly.IsEmpty) { - toSearch.Push(testPoly.InteriorPoint.Coordinate.ToH3Index(resolution)); - } + return testMode switch { + VertexTestMode.All => FillUsingAllVertices(locator, toSearch, searched), + VertexTestMode.Any => FillUsingAnyVertex(locator, toSearch, searched), + VertexTestMode.Center => FillUsingCenterVertex(locator, toSearch, searched), + _ => throw new ArgumentOutOfRangeException(nameof(testMode), "invalid vertex test mode") + }; + } - IndexedPointInAreaLocator locator = new(testPoly); + /// + /// Performs a polyfill operation utilizing the center of each index produced + /// during the fill. + /// + /// + /// + /// + /// + private static IEnumerable FillUsingCenterVertex(IPointOnGeometryLocator locator, Stack toSearch, ISet searched) { var coordinate = new Coordinate(); var faceIjk = new FaceIJK(); @@ -85,15 +130,83 @@ public static IEnumerable Fill(this Geometry polygon, int resolution) { } } + /// + /// Performs a polyfill operation utilizing any from the cell boundary of each + /// index produced during the fill. + /// + private static IEnumerable FillUsingAnyVertex(IPointOnGeometryLocator locator, Stack toSearch, ISet searched) { + var coordinate = new Coordinate(); + var faceIjk = new FaceIJK(); + + while (toSearch.Count != 0) { + var index = toSearch.Pop(); + + foreach (var neighbour in index.GetNeighbours()) { + if (searched.Contains(neighbour)) continue; + searched.Add(neighbour); + + foreach (var vertex in neighbour.GetCellBoundaryVertices(faceIjk)) { + coordinate.X = vertex.LongitudeDegrees; + coordinate.Y = vertex.LatitudeDegrees; + + var location = locator.Locate(coordinate); + if (location != Location.Interior) + continue; + + yield return neighbour; + toSearch.Push(neighbour); + break; + } + } + } + } + + /// + /// Performs a polyfill operation utilizing all s from the cell boundary of each + /// index produced during the fill. + /// + private static IEnumerable FillUsingAllVertices(IPointOnGeometryLocator locator, Stack toSearch, ISet searched) { + var coordinate = new Coordinate(); + var faceIjk = new FaceIJK(); + + while (toSearch.Count != 0) { + var index = toSearch.Pop(); + + foreach (var neighbour in index.GetNeighbours()) { + if (searched.Contains(neighbour)) continue; + searched.Add(neighbour); + + var matched = true; + + foreach (var vertex in neighbour.GetCellBoundaryVertices(faceIjk)) { + coordinate.X = vertex.LongitudeDegrees; + coordinate.Y = vertex.LatitudeDegrees; + + var location = locator.Locate(coordinate); + if (location == Location.Interior) + continue; + + matched = false; + break; + } + + if (!matched) continue; + + yield return neighbour; + toSearch.Push(neighbour); + } + } + } + /// /// Returns all of the H3 indexes that follow the provided LineString /// at the specified resolution. /// - /// + /// /// /// - public static IEnumerable Fill(this LineString polyline, int resolution) => - polyline.Coordinates.TraceCoordinates(resolution); + public static IEnumerable Fill(this LineString polyLine, int resolution) => + polyLine.Coordinates.TraceCoordinates(resolution); /// /// Gets all of the H3 indices that define the provided set of s. @@ -102,7 +215,7 @@ public static IEnumerable Fill(this LineString polyline, int resolution /// /// public static IEnumerable TraceCoordinates(this Coordinate[] coordinates, int resolution) { - HashSet indicies = new(); + HashSet indices = new(); // trace the coordinates var coordLen = coordinates.Length - 1; @@ -119,18 +232,18 @@ public static IEnumerable TraceCoordinates(this Coordinate[] coordinate v2.Longitude = vB.X * M_PI_180; v2.Latitude = vB.Y * M_PI_180; - // estimate number of indicies between points, use that as a + // estimate number of indices between points, use that as a // number of segments to chop the line into var count = v1.LineHexEstimate(v2, resolution); - for (int j = 1; j < count; j += 1) { + for (var j = 1; j < count; j += 1) { // interpolate line var interpolated = LinearLocation.PointAlongSegmentByFraction(vA, vB, (double)j / count); - indicies.Add(interpolated.ToH3Index(resolution, faceIjk, v3d)); + indices.Add(interpolated.ToH3Index(resolution, faceIjk, v3d)); } } - return indicies; + return indices; } /// @@ -152,11 +265,11 @@ public static bool IsTransMeridian(this Geometry geometry) { /// /// /// - private static Geometry SplitGeometry(Geometry originalGeometry) { + internal static Geometry SplitGeometry(Geometry originalGeometry) { var left = originalGeometry.Copy(); - left.Apply(_negativeLonFilter); + left.Apply(NegativeLonFilter); var right = originalGeometry.Copy(); - right.Apply(_positiveLonFilter); + right.Apply(PositiveLonFilter); var geometry = left.Union(right); return geometry.IsEmpty ? originalGeometry : geometry; diff --git a/src/H3/Algorithms/Rings.cs b/src/H3/Algorithms/Rings.cs index 6855d48..6c2d6ad 100644 --- a/src/H3/Algorithms/Rings.cs +++ b/src/H3/Algorithms/Rings.cs @@ -11,16 +11,23 @@ namespace H3.Algorithms { /// /// Holder for indexes produced from the k ring functions. /// - public record RingCell { + public readonly struct RingCell { + + public RingCell(H3Index index, int distance) { + Index = index; + Distance = distance; + } + /// /// H3 index /// - public H3Index Index { get; init; } = H3Index.Invalid; + public H3Index Index { get; } /// /// k cell distance from the origin (ring level) /// - public int Distance { get; init; } + public int Distance { get; } + } /// @@ -61,11 +68,11 @@ public static IEnumerable GetHexRing(this H3Index origin, int k) { yield break; } - H3Index index = origin; + var index = origin; // break out to the requested ring - int rotations = 0; - for (int ring = 0; ring < k; ring +=1 ) { + var rotations = 0; + for (var ring = 0; ring < k; ring +=1 ) { (index, rotations) = index.GetDirectNeighbour(LookupTables.NextRingDirection, rotations); if (index == H3Index.Invalid) throw new HexRingKSequenceException(); if (index.IsPentagon) throw new HexRingPentagonException(); @@ -74,18 +81,19 @@ public static IEnumerable GetHexRing(this H3Index origin, int k) { H3Index lastIndex = new(index); yield return index; - for (int direction = 0; direction < 6; direction += 1) { - for (int pos = 0; pos < k; pos += 1) { + for (var direction = 0; direction < 6; direction += 1) { + for (var pos = 0; pos < k; pos += 1) { (index, rotations) = index.GetDirectNeighbour(LookupTables.CounterClockwiseDirections[direction], rotations); if (index == H3Index.Invalid) throw new HexRingKSequenceException(); // Skip the very last index, it was already added. We do // however need to traverse to it because of the pentagonal // distortion check, below. - if (pos != k - 1 || direction != 5) { - yield return index; - if (index.IsPentagon) throw new HexRingPentagonException(); - } + if (pos == k - 1 && direction == 5) + continue; + + yield return index; + if (index.IsPentagon) throw new HexRingPentagonException(); } } @@ -130,7 +138,7 @@ public static IEnumerable GetKRingSlow(this H3Index origin, int k) { // since k >= 0, start with origin Queue queue = new(); Dictionary searched = new(); - queue.Enqueue(new RingCell { Index = origin, Distance = 0 }); + queue.Enqueue(new RingCell(origin, 0)); while (queue.Count != 0) { var cell = queue.Dequeue(); @@ -141,11 +149,11 @@ public static IEnumerable GetKRingSlow(this H3Index origin, int k) { continue; foreach (var neighbour in cell.Index.GetNeighbours()) { - if (neighbour == origin || searched.TryGetValue(neighbour, out int previousK) && previousK <= nextK) { + if (neighbour == origin || searched.TryGetValue(neighbour, out var previousK) && previousK <= nextK) { continue; } searched[neighbour] = nextK; - queue.Enqueue(new RingCell { Index = neighbour, Distance = nextK }); + queue.Enqueue(new RingCell(neighbour, nextK)); } } } @@ -165,10 +173,10 @@ public static IEnumerable GetKRingSlow(this H3Index origin, int k) { /// Enumerable set of RingCell, or an exception if a traversal error is /// encountered (eg pentagon) public static IEnumerable GetKRingFast(this H3Index origin, int k) { - H3Index index = origin; + var index = origin; // k must be >= 0, so origin is always needed - yield return new RingCell { Index = index, Distance = 0 }; + yield return new RingCell(index, 0); // Pentagon was encountered; bail out as user doesn't want this. if (index.IsPentagon) throw new HexRingPentagonException(); @@ -177,17 +185,17 @@ public static IEnumerable GetKRingFast(this H3Index origin, int k) { if (k == 0) yield break; // 0 < ring <= k, current ring - int ring = 1; + var ring = 1; // 0 <= direction < 6, current side of the ring - int direction = 0; + var direction = 0; // 0 <= i < ring, current position on the side of the ring - int i = 0; + var i = 0; // Number of 60 degree ccw rotations to perform on the direction (based on // which faces have been crossed.) - int rotations = 0; + var rotations = 0; while (ring <= k) { if (direction == 0 && i == 0) { @@ -211,7 +219,7 @@ public static IEnumerable GetKRingFast(this H3Index origin, int k) { throw new HexRingKSequenceException(); } - yield return new RingCell { Index = index, Distance = ring }; + yield return new RingCell(index, ring); i += 1; // Check if end of this side of the k-ring diff --git a/src/H3/Extensions/H3GeometryExtensions.cs b/src/H3/Extensions/H3GeometryExtensions.cs index 80dfc65..d85ade4 100644 --- a/src/H3/Extensions/H3GeometryExtensions.cs +++ b/src/H3/Extensions/H3GeometryExtensions.cs @@ -18,13 +18,15 @@ public static class H3GeometryExtensions { /// /// optional result object; defaults to new /// instance. + /// Optional object to use during + /// conversion (useful to reduce allocations when performing many coordinate conversions); + /// defaults to a new instance if not provided. /// public static Coordinate ToCoordinate(this H3Index inputIndex, Coordinate? result = default, FaceIJK? toUpdateFaceIjk = default) { result ??= new Coordinate(); - var index = inputIndex; - var resolution = index.Resolution; - var faceIjk = index.ToFaceIJK(toUpdateFaceIjk); + var resolution = inputIndex.Resolution; + var faceIjk = inputIndex.ToFaceIJK(toUpdateFaceIjk); var center = LookupTables.GeoFaceCenters[faceIjk.Face]; var (x, y) = faceIjk.Coord.GetVec2dOrdinates(); @@ -71,7 +73,11 @@ public static Coordinate ToCoordinate(this H3Index inputIndex, Coordinate? resul var cosP1Lat = Math.Cos(center.Latitude); var sinDist = Math.Sin(distance); var cosDist = Math.Cos(distance); + #if NETSTANDARD2_0 + var sinLat = Clamp(sinP1Lat * cosDist + cosP1Lat * sinDist * Math.Cos(azimuth), -1.0, 1.0); + #else var sinLat = Math.Clamp(sinP1Lat * cosDist + cosP1Lat * sinDist * Math.Cos(azimuth), -1.0, 1.0); + #endif latitude = Math.Asin(sinLat); if (Math.Abs(latitude - M_PI_2) < EPSILON) { @@ -84,8 +90,13 @@ public static Coordinate ToCoordinate(this H3Index inputIndex, Coordinate? resul longitude = 0; } else { var cosP2Lat = Math.Cos(latitude); + #if NETSTANDARD2_0 + var sinLon = Clamp(Math.Sin(azimuth) * sinDist / cosP2Lat, -1.0, 1.0); + var cosLon = Clamp((cosDist - sinP1Lat * Math.Sin(latitude)) / cosP1Lat / cosP2Lat, -1.0, 1.0); + #else var sinLon = Math.Clamp(Math.Sin(azimuth) * sinDist / cosP2Lat, -1.0, 1.0); var cosLon = Math.Clamp((cosDist - sinP1Lat * Math.Sin(latitude)) / cosP1Lat / cosP2Lat, -1.0, 1.0); + #endif longitude = ConstrainLongitude(center.Longitude + Math.Atan2(sinLon, cosLon)); } } @@ -127,61 +138,72 @@ public static H3Index ToH3Index(this Coordinate coordinate, int resolution, Face /// /// Faces intersected by the index public static int[] GetFaces(this H3Index index) { - int resolution = index.Resolution; - - // We can't use the vertex-based approach here for class II pentagons, - // because all their vertices are on the icosahedron edges. Their - // direct child pentagons cross the same faces, so use those instead. - if (index.IsPentagon && !IsResolutionClass3(resolution)) { - // Note that this would not work for res 15, but this is only run on - // Class II pentagons, it should never be invoked for a res 15 index. - return index.GetDirectChild(Direction.Center).GetFaces(); - } - - // convert to FaceIJK - FaceIJK fijk = index.ToFaceIJK(); - - // Get all vertices as FaceIJK addresses. For simplicity, always - // initialize the array with 6 verts, ignoring the last one for pentagons - int vertexCount; - FaceIJK[] vertices; - - if (index.IsPentagon) { - vertexCount = NUM_PENT_VERTS; - vertices = fijk.GetPentagonVertices(ref resolution); - } else { - vertexCount = NUM_HEX_VERTS; - vertices = fijk.GetHexVertices(ref resolution); - } + while (true) { + var resolution = index.Resolution; + + // We can't use the vertex-based approach here for class II pentagons, + // because all their vertices are on the icosahedron edges. Their + // direct child pentagons cross the same faces, so use those instead. + if (index.IsPentagon && !IsResolutionClass3(resolution)) { + // Note that this would not work for res 15, but this is only run on + // Class II pentagons, it should never be invoked for a res 15 index. + index = index.GetDirectChild(Direction.Center); + continue; + } - // We may not use all of the slots in the output array, - // so fill with invalid values to indicate unused slots - int[] result = new int[index.MaximumFaceCount]; - Array.Fill(result, -1); + // convert to FaceIJK + var fijk = index.ToFaceIJK(); - // add each vertex face, using the output array as a hash set - for (int i = 0; i < vertexCount; i += 1) { - FaceIJK vert = vertices[i]; + // Get all vertices as FaceIJK addresses. For simplicity, always + // initialize the array with 6 verts, ignoring the last one for pentagons + int vertexCount; + FaceIJK[] vertices; - // Adjust overage, determining whether this vertex is - // on another face if (index.IsPentagon) { - vert.AdjustPentagonVertexOverage(resolution); + vertexCount = NUM_PENT_VERTS; + vertices = fijk.GetPentagonVertices(ref resolution); } else { - vert.AdjustOverageClass2(resolution, false, true); + vertexCount = NUM_HEX_VERTS; + vertices = fijk.GetHexVertices(ref resolution); } - // Save the face to the output array - int face = vert.Face; - int pos = 0; + // We may not use all of the slots in the output array, + // so fill with invalid values to indicate unused slots + var result = new int[index.MaximumFaceCount]; +#if NETSTANDARD2_0 + for (var i = 0; i < index.MaximumFaceCount; i += 1) { + result[i] = -1; + } +#else + Array.Fill(result, -1); +#endif + + // add each vertex face, using the output array as a hash set + for (var i = 0; i < vertexCount; i += 1) { + var vert = vertices[i]; + + // Adjust overage, determining whether this vertex is + // on another face + if (index.IsPentagon) { + vert.AdjustPentagonVertexOverage(resolution); + } else { + vert.AdjustOverageClass2(resolution, false, true); + } + + // Save the face to the output array + var face = vert.Face; + var pos = 0; + + // Find the first empty output position, or the first position + // matching the current face + while (result[pos] != -1 && result[pos] != face) + pos++; + + result[pos] = face; + } - // Find the first empty output position, or the first position - // matching the current face - while (result[pos] != -1 && result[pos] != face) pos++; - result[pos] = face; + return result; } - - return result; } /// @@ -194,13 +216,17 @@ public static int[] GetFaces(this H3Index index) { /// H3 cell /// area in radians^2 public static double CellAreaInRadiansSquared(this H3Index index) { - GeoCoord c = index.ToGeoCoord(); - var boundary = index.GetCellBoundaryVertices().ToArray(); - double area = 0.0; - - for (int i = 0; i < boundary.Length; i += 1) { - int j = (i + 1) % boundary.Length; - area += GeoCoord.GetTriangleArea(boundary[i], boundary[j], c); + var resolution = index.Resolution; + var faceIjk = index.ToFaceIJK(); + var center = faceIjk.ToGeoCoord(resolution); + var boundary = (index.IsPentagon + ? faceIjk.GetPentagonBoundary(resolution, 0, NUM_PENT_VERTS) + : faceIjk.GetHexagonBoundary(resolution, 0, NUM_HEX_VERTS)).ToArray(); + var area = 0.0; + + for (var i = 0; i < boundary.Length; i += 1) { + var j = (i + 1) % boundary.Length; + area += GeoCoord.GetTriangleArea(boundary[i], boundary[j], center); } return area; @@ -228,8 +254,12 @@ public static double CellAreaInMSquared(this H3Index index) => /// H3Index to get area for /// public static double GetRadiusInKm(this H3Index index) { - GeoCoord center = index.ToGeoCoord(); - GeoCoord firstVertex = index.GetCellBoundaryVertices().First(); + var resolution = index.Resolution; + var faceIjk = index.ToFaceIJK(); + var center = faceIjk.ToGeoCoord(resolution); + var firstVertex = (index.IsPentagon + ? faceIjk.GetPentagonBoundary(resolution, 0, 1) + : faceIjk.GetHexagonBoundary(resolution, 0, 1)).First(); return center.GetPointDistanceInKm(firstVertex); } @@ -238,10 +268,14 @@ public static double GetRadiusInKm(this H3Index index) { /// a given H3 index. /// /// H3Index to get boundary for + /// Optional to be used + /// for index coordinate conversions; defaults to none. Useful for + /// reducing allocations when producing boundaries for a large number + /// of indices. /// boundary coordinates - public static IEnumerable GetCellBoundaryVertices(this H3Index index) { - FaceIJK face = index.ToFaceIJK(); - int resolution = index.Resolution; + public static IEnumerable GetCellBoundaryVertices(this H3Index index, FaceIJK? toUpdateIjk = default) { + var face = index.ToFaceIJK(toUpdateIjk); + var resolution = index.Resolution; return index.IsPentagon ? face.GetPentagonBoundary(resolution, 0, NUM_PENT_VERTS) : face.GetHexagonBoundary(resolution, 0, NUM_HEX_VERTS); @@ -267,14 +301,14 @@ public static Polygon GetCellBoundary(this H3Index index, GeometryFactory? geomF /// /// Generates a Multi-Polygon containing all of the cell boundaries for - /// a given set of H3 indicies. + /// a given set of H3 indices. /// - /// + /// /// /// - public static MultiPolygon GetCellBoundaries(this IEnumerable indicies, GeometryFactory? geomFactory = null) { + public static MultiPolygon GetCellBoundaries(this IEnumerable indices, GeometryFactory? geomFactory = null) { var gf = geomFactory ?? DefaultGeometryFactory; - return gf.CreateMultiPolygon(indicies.Select(index => index.GetCellBoundary()).ToArray()); + return gf.CreateMultiPolygon(indices.Select(index => index.GetCellBoundary()).ToArray()); } } diff --git a/src/H3/Extensions/H3HierarchyExtensions.cs b/src/H3/Extensions/H3HierarchyExtensions.cs index 4e00c3e..567612b 100644 --- a/src/H3/Extensions/H3HierarchyExtensions.cs +++ b/src/H3/Extensions/H3HierarchyExtensions.cs @@ -1,10 +1,8 @@ using System; using System.Collections.Generic; using H3.Model; -using H3.Algorithms; using static H3.Constants; using static H3.Utils; -using System.Linq; #nullable enable @@ -32,29 +30,28 @@ public static class H3HierarchyExtensions { public static (H3Index, int) GetDirectNeighbour(this H3Index origin, Direction direction, int rotations = 0) { H3Index outIndex = new(origin); - Direction dir = direction; - for (int r = 0; r < rotations; r += 1) dir = dir.RotateCounterClockwise(); + var dir = direction; + dir = dir.RotateCounterClockwise(rotations); - BaseCell? oldBaseCell = origin.BaseCell; + var oldBaseCell = origin.BaseCell; if (oldBaseCell == null) throw new Exception("origin is not a valid base cell"); - int newRotations = 0; - Direction oldLeadingDir = origin.LeadingNonZeroDirection; + var neighbourRotations = 0; // Adjust the indexing digits and, if needed, the base cell. - int resolution = outIndex.Resolution - 1; + var resolution = outIndex.Resolution - 1; while (true) { - int nextResolution = resolution + 1; if (resolution == -1) { - var newBaseCellNumber = LookupTables.Neighbours[oldBaseCell.Cell, (int)dir]; + var newBaseCellNumber = oldBaseCell.NeighbouringCells[(sbyte)dir]; + neighbourRotations = oldBaseCell.NeighbourRotations[(sbyte)dir]; + outIndex.BaseCellNumber = newBaseCellNumber; - newRotations = LookupTables.NeighbourCounterClockwiseRotations[oldBaseCell.Cell, (int)dir]; if (newBaseCellNumber == LookupTables.INVALID_BASE_CELL) { // Adjust for the deleted k vertex at the base cell level. // This edge actually borders a different neighbor. - outIndex.BaseCellNumber = LookupTables.Neighbours[oldBaseCell.Cell, (int)Direction.IK]; - newRotations = LookupTables.NeighbourCounterClockwiseRotations[oldBaseCell.Cell, (int)Direction.IK]; + outIndex.BaseCellNumber = oldBaseCell.NeighbouringCells[(sbyte)Direction.IK]; + neighbourRotations = oldBaseCell.NeighbourRotations[(sbyte)Direction.IK]; // perform the adjustment for the k-subsequence we're skipping // over. @@ -65,7 +62,8 @@ public static (H3Index, int) GetDirectNeighbour(this H3Index origin, Direction d break; } - Direction oldDir = outIndex.GetDirectionForResolution(nextResolution); + var nextResolution = resolution + 1; + var oldDir = outIndex.GetDirectionForResolution(nextResolution); Direction nextDir; if (oldDir == Direction.Invalid) { @@ -96,10 +94,10 @@ public static (H3Index, int) GetDirectNeighbour(this H3Index origin, Direction d } } - BaseCell newBaseCell = outIndex.BaseCell; + var newBaseCell = outIndex.BaseCell; if (newBaseCell.IsPentagon) { - bool alreadyAdjustedKSubsequence = false; + var alreadyAdjustedKSubsequence = false; // force rotation out of missing k-axes sub-sequence if (outIndex.LeadingNonZeroDirection == Direction.K) { @@ -118,7 +116,7 @@ public static (H3Index, int) GetDirectNeighbour(this H3Index origin, Direction d alreadyAdjustedKSubsequence = true; } else { // ReSharper disable once SwitchStatementHandlesSomeKnownEnumValuesWithDefault - switch (oldLeadingDir) { + switch (origin.LeadingNonZeroDirection) { // In this case, we traversed into the deleted // k subsequence from within the same pentagon // base cell. @@ -150,7 +148,7 @@ public static (H3Index, int) GetDirectNeighbour(this H3Index origin, Direction d } } - for (int i = 0; i < newRotations; i += 1) outIndex.RotatePentagonCounterClockwise(); + for (var i = 0; i < neighbourRotations; i += 1) outIndex.RotatePentagonCounterClockwise(); // Account for differing orientation of the base cells (this edge // might not follow properties of some other edges.) @@ -158,7 +156,7 @@ public static (H3Index, int) GetDirectNeighbour(this H3Index origin, Direction d if (newBaseCell.IsPolarPentagon) { // 'polar' base cells behave differently because they have all // i neighbors. - if (oldBaseCell.Cell != 118 && oldBaseCell.Cell != 8 && outIndex.LeadingNonZeroDirection != Direction.JK) { + if (oldBaseCell.Cell is not 118 or 8 && outIndex.LeadingNonZeroDirection != Direction.JK) { rotations += 1; } } else if (outIndex.LeadingNonZeroDirection == Direction.IK && !alreadyAdjustedKSubsequence) { @@ -168,10 +166,10 @@ public static (H3Index, int) GetDirectNeighbour(this H3Index origin, Direction d } } } else { - for (int i = 0; i < newRotations; i += 1) outIndex.RotateCounterClockwise(); + outIndex.RotateCounterClockwise(neighbourRotations); } - rotations = (rotations + newRotations) % 6; + rotations = (rotations + neighbourRotations) % 6; return (outIndex, rotations); } @@ -203,9 +201,9 @@ public static IEnumerable GetNeighbours(this H3Index origin) { /// /// public static Direction DirectionForNeighbour(this H3Index origin, H3Index destination) { - bool isPentagon = origin.IsPentagon; + var isPentagon = origin.IsPentagon; - for (Direction dir = isPentagon ? Direction.J : Direction.K; dir < Direction.Invalid; dir += 1) { + for (var dir = isPentagon ? Direction.J : Direction.K; dir < Direction.Invalid; dir += 1) { var neighbour = origin.GetDirectNeighbour(dir).Item1; if (neighbour == destination) return dir; } @@ -231,7 +229,7 @@ public static bool IsNeighbour(this H3Index origin, H3Index destination) { } // must be the same resolution - int resolution = origin.Resolution; + var resolution = origin.Resolution; if (resolution != destination.Resolution) { return false; } @@ -241,10 +239,10 @@ public static bool IsNeighbour(this H3Index origin, H3Index destination) { // children are neighbors with 3 of the 7 children. So a simple comparison // of origin and destination parents and then a lookup table of the children // is a super-cheap way to possibly determine they are neighbors. - int parentRes = resolution - 1; + var parentRes = resolution - 1; if (parentRes > 0 && origin.GetParentForResolution(parentRes) == destination.GetParentForResolution(parentRes)) { - Direction originResDigit = origin.Direction; - Direction destResDigit = destination.Direction; + var originResDigit = origin.Direction; + var destResDigit = destination.Direction; if (originResDigit == Direction.Center || destResDigit == Direction.Center) { return true; @@ -256,7 +254,11 @@ public static bool IsNeighbour(this H3Index origin, H3Index destination) { } // Otherwise, we have to determine the neighbor relationship the "hard" way. - return origin.GetKRing(1).Any(cell => cell.Index == destination); + foreach (var neighbour in origin.GetNeighbours()) { + if (neighbour == destination) return true; + } + + return false; } /// @@ -267,7 +269,7 @@ public static bool IsNeighbour(this H3Index origin, H3Index destination) { /// parent resolution, must be >= 0 < resolution /// H3Index of parent public static H3Index GetParentForResolution(this H3Index origin, int parentResolution) { - int resolution = origin.Resolution; + var resolution = origin.Resolution; // ask for an invalid resolution or resolution greater than ours? if (parentResolution is < 0 or > MAX_H3_RES || parentResolution > resolution) return H3Index.Invalid; @@ -305,7 +307,7 @@ public static H3Index GetParentForResolution(this H3Index origin, int parentReso /// the resolution to switch to, must be > resolution <= MAX_H3_RES /// H3Index of the center child, or H3Index.Invalid if you actually asked for a parent public static H3Index GetChildCenterForResolution(this H3Index origin, int childResolution) { - int resolution = origin.Resolution; + var resolution = origin.Resolution; if (!IsValidChildResolution(resolution, childResolution)) return H3Index.Invalid; if (resolution == childResolution) return origin; @@ -325,7 +327,7 @@ public static H3Index GetChildCenterForResolution(this H3Index origin, int child /// resolution of child level /// public static IEnumerable GetChildrenForResolution(this H3Index origin, int childResolution) { - int parentResolution = origin.Resolution; + var parentResolution = origin.Resolution; if (!IsValidChildResolution(parentResolution, childResolution)) { yield break; } @@ -342,22 +344,22 @@ public static IEnumerable GetChildrenForResolution(this H3Index origin, iterator.ZeroDirectionsForResolutionRange(parentResolution + 1, childResolution); // handle pentagons - int fnz = iterator.IsPentagon ? childResolution : -1; + var fnz = iterator.IsPentagon ? childResolution : -1; while (iterator != H3Index.Invalid) { yield return new H3Index(iterator); - int childRes = iterator.Resolution; + var childRes = iterator.Resolution; iterator.IncrementDirectionForResolution(childRes); - for (int i = childResolution; i >= parentResolution; i -= 1) { + for (var i = childResolution; i >= parentResolution; i -= 1) { // done iterating? if (i == parentResolution) { iterator = H3Index.Invalid; break; } - Direction dir = iterator.GetDirectionForResolution(i); + var dir = iterator.GetDirectionForResolution(i); // pentagon? if (i == fnz && dir == Direction.K) { diff --git a/src/H3/Extensions/H3LocalIJExtensions.cs b/src/H3/Extensions/H3LocalIJExtensions.cs index 5d4e1cc..ca611e0 100644 --- a/src/H3/Extensions/H3LocalIJExtensions.cs +++ b/src/H3/Extensions/H3LocalIJExtensions.cs @@ -133,17 +133,17 @@ public static class LocalCoordIJK { /// local IJ coordinates public static CoordIJK ToLocalIJK(H3Index origin, H3Index destination) { H3Index index = new(destination); - int resolution = origin.Resolution; + var resolution = origin.Resolution; if (resolution != index.Resolution) { throw new ArgumentOutOfRangeException(nameof(index), "must be same resolution as origin"); } - BaseCell originBaseCell = origin.BaseCell; - BaseCell baseCell = index.BaseCell; + var originBaseCell = origin.BaseCell; + var baseCell = index.BaseCell; // Direction from origin base cell to index base cell - Direction dir = Direction.Center; - Direction revDir = Direction.Center; + var dir = Direction.Center; + var revDir = Direction.Center; if (originBaseCell != baseCell) { dir = BaseCell.GetNeighbourDirection(originBaseCell.Cell, baseCell.Cell); @@ -154,38 +154,37 @@ public static CoordIJK ToLocalIJK(H3Index origin, H3Index destination) { revDir = BaseCell.GetNeighbourDirection(baseCell.Cell, originBaseCell.Cell); } - bool originOnPent = originBaseCell.IsPentagon; - bool indexOnPent = baseCell.IsPentagon; + var originOnPent = originBaseCell.IsPentagon; + var indexOnPent = baseCell.IsPentagon; if (dir != Direction.Center) { // Rotate index into the orientation of the origin base cell. // cw because we are undoing the rotation into that base cell. - int baseCellRotations = LookupTables.NeighbourCounterClockwiseRotations[originBaseCell.Cell, (int)dir]; + var baseCellRotations = originBaseCell.NeighbourRotations[(int)dir]; if (indexOnPent) { - for (int i = 0; i < baseCellRotations; i += 1) { + for (var i = 0; i < baseCellRotations; i += 1) { index.RotatePentagonClockwise(); revDir = revDir.RotateClockwise(); if (revDir == Direction.K) revDir = revDir.RotateClockwise(); } - } else { - for (int i = 0; i < baseCellRotations; i += 1) { - index.RotateClockwise(); - revDir = revDir.RotateClockwise(); - } + } else if (baseCellRotations > 0) { + index.RotateClockwise(baseCellRotations); + revDir = revDir.RotateClockwise(baseCellRotations); } } var indexFijk = new FaceIJK(); index.ToFaceWithInitializedFijk(indexFijk); + if (dir != Direction.Center) { if (originBaseCell == baseCell) throw new Exception("assertion failed; origin should not equal index cell"); if (originOnPent && indexOnPent) throw new Exception("assertion failed; origin and index cannot both be on a pentagon"); - int pentagonRotations = 0; - int directionRotations = 0; + var pentagonRotations = 0; + var directionRotations = 0; if (originOnPent) { - Direction originLeadingDigit = origin.LeadingNonZeroDirection; + var originLeadingDigit = origin.LeadingNonZeroDirection; if (LookupTables.UnfoldableDirections[(int)originLeadingDigit, (int)dir]) { // TODO: We may be unfolding the pentagon incorrectly in this // case; return an error code until this is guaranteed to be @@ -196,7 +195,7 @@ public static CoordIJK ToLocalIJK(H3Index origin, H3Index destination) { directionRotations = LookupTables.PentagonRotations[(int)originLeadingDigit, (int)dir]; pentagonRotations = directionRotations; } else if (indexOnPent) { - Direction indexLeadingDigit = index.LeadingNonZeroDirection; + var indexLeadingDigit = index.LeadingNonZeroDirection; if (LookupTables.UnfoldableDirections[(int)indexLeadingDigit, (int)revDir]) { // TODO: We may be unfolding the pentagon incorrectly in this // case; return an error code until this is guaranteed to be @@ -210,11 +209,11 @@ public static CoordIJK ToLocalIJK(H3Index origin, H3Index destination) { if (pentagonRotations < 0) throw new Exception("no pentagon rotations"); if (directionRotations < 0) throw new Exception("no direction rotations"); - for (int i = 0; i < pentagonRotations; i += 1) indexFijk.Coord.RotateClockwise(); + for (var i = 0; i < pentagonRotations; i += 1) indexFijk.Coord.RotateClockwise(); - CoordIJK offset = new CoordIJK(0, 0, 0).ToNeighbour(dir); + var offset = new CoordIJK(0, 0, 0).ToNeighbour(dir); // scale offset based upon resolution - for (int r = resolution - 1; r >= 0; r -= 1) { + for (var r = resolution - 1; r >= 0; r -= 1) { if (IsResolutionClass3(r + 1)) { offset.DownAperture7CounterClockwise(); } else { @@ -222,7 +221,7 @@ public static CoordIJK ToLocalIJK(H3Index origin, H3Index destination) { } } - for (int i = 0; i < directionRotations; i += 1) offset.RotateClockwise(); + for (var i = 0; i < directionRotations; i += 1) offset.RotateClockwise(); // perform necesary translation indexFijk.Coord.I += offset.I; @@ -235,8 +234,8 @@ public static CoordIJK ToLocalIJK(H3Index origin, H3Index destination) { // cell. if (originBaseCell != baseCell) throw new Exception("origin and index base cells must equal"); - Direction originLeadingDigit = origin.LeadingNonZeroDirection; - Direction indexLeadingDigit = index.LeadingNonZeroDirection; + var originLeadingDigit = origin.LeadingNonZeroDirection; + var indexLeadingDigit = index.LeadingNonZeroDirection; if (LookupTables.UnfoldableDirections[(int)originLeadingDigit, (int)indexLeadingDigit]) { // TODO: We may be unfolding the pentagon incorrectly in this case; @@ -244,9 +243,9 @@ public static CoordIJK ToLocalIJK(H3Index origin, H3Index destination) { throw new Exception("origin -> index results in unfoldable pentagon"); } - int withinPentagonRotations = LookupTables.PentagonRotations[(int)originLeadingDigit, (int)indexLeadingDigit]; + var withinPentagonRotations = LookupTables.PentagonRotations[(int)originLeadingDigit, (int)indexLeadingDigit]; - for (int i = 0; i < withinPentagonRotations; i += 1) indexFijk.Coord.RotateClockwise(); + for (var i = 0; i < withinPentagonRotations; i += 1) indexFijk.Coord.RotateClockwise(); } return indexFijk.Coord; @@ -261,11 +260,11 @@ public static CoordIJK ToLocalIJK(H3Index origin, H3Index destination) { /// /// /// - public static H3Index ToH3Index(H3Index origin, CoordIJK ijk) { - int resolution = origin.Resolution; - BaseCell? originBaseCell = origin.BaseCell; + public static H3Index ToH3Index(H3Index origin, CoordIJK ijk, CoordIJK? workIjk1 = default, CoordIJK? workIjk2 = default, CoordIJK? workIjk3 = default) { + var resolution = origin.Resolution; + var originBaseCell = origin.BaseCell; if (originBaseCell == null) throw new Exception("origin is not a valid base cell"); - bool originOnPent = originBaseCell.IsPentagon; + var originOnPent = originBaseCell.IsPentagon; H3Index index = new() { Mode = Mode.Cell, @@ -275,7 +274,7 @@ public static H3Index ToH3Index(H3Index origin, CoordIJK ijk) { if (resolution == 0) { if (ijk.I > 1 || ijk.J > 1 || ijk.K > 1) throw new Exception("input coordinates out of range"); - int newBaseCell = LookupTables.Neighbours[originBaseCell.Cell, (int)(Direction)ijk]; + var newBaseCell = originBaseCell.NeighbouringCells[(sbyte)(Direction)ijk]; if (newBaseCell == LookupTables.INVALID_BASE_CELL) throw new Exception("moved in invalid direction off pentagon"); index.BaseCellNumber = newBaseCell; @@ -285,14 +284,17 @@ public static H3Index ToH3Index(H3Index origin, CoordIJK ijk) { // we need to find the correct base cell offset (if any) for this H3 index; // start with the passed in base cell and resolution res ijk coordinates // in that base cell's coordinate system - CoordIJK ijkCopy = new(ijk); + var ijkCopy = workIjk1 ?? new CoordIJK(); + ijkCopy.I = ijk.I; + ijkCopy.J = ijk.J; + ijkCopy.K = ijk.K; // build the H3Index from finest res up // adjust r for the fact that the res 0 base cell offsets the indexing // digits - CoordIJK lastIJK = new(); - CoordIJK lastCenter = new(); - for (int r = resolution - 1; r >= 0; r -= 1) { + var lastIJK = workIjk2 ?? new CoordIJK(); + var lastCenter = workIjk3 ?? new CoordIJK(); + for (var r = resolution - 1; r >= 0; r -= 1) { lastIJK.I = ijkCopy.I; lastIJK.J = ijkCopy.J; lastIJK.K = ijkCopy.K; @@ -325,23 +327,23 @@ public static H3Index ToH3Index(H3Index origin, CoordIJK ijk) { // lookup correct base cell Direction dir = ijkCopy; - BaseCell? baseCell = originBaseCell.Neighbour(dir); + var baseCell = originBaseCell.Neighbour(dir); // If baseCell is invalid, it must be because the origin base cell is a // pentagon, and because pentagon base cells do not border each other, // baseCell must not be a pentagon. - bool indexOnPent = baseCell != null && baseCell.IsPentagon; + var indexOnPent = baseCell != null && baseCell.IsPentagon; if (dir != Direction.Center) { // If the index is in a warped direction, we need to unwarp the base // cell direction. There may be further need to rotate the index digits. - int pentagonRotations = 0; + var pentagonRotations = 0; if (originOnPent) { - Direction originLeadingDigit = origin.LeadingNonZeroDirection; + var originLeadingDigit = origin.LeadingNonZeroDirection; pentagonRotations = LookupTables.PentagonRotationsInReverse[(int)originLeadingDigit, (int)dir]; - for (int i = 0; i < pentagonRotations; i += 1) dir = dir.RotateCounterClockwise(); + dir = dir.RotateCounterClockwise(pentagonRotations); // The pentagon rotations are being chosen so that dir is not the // deleted direction. If it still happens, it means we're moving @@ -362,7 +364,7 @@ public static H3Index ToH3Index(H3Index origin, CoordIJK ijk) { // Now we can determine the relation between the origin and target base // cell. - int baseCellRotations = LookupTables.NeighbourCounterClockwiseRotations[originBaseCell.Cell, (int)dir]; + var baseCellRotations = originBaseCell.NeighbourRotations[(sbyte)dir]; if (baseCellRotations < 0) throw new Exception("invalid number of base cell rotations"); // Adjust for pentagon warping within the base cell. The base cell @@ -370,15 +372,15 @@ public static H3Index ToH3Index(H3Index origin, CoordIJK ijk) { // back. We might not need to check for errors since we would just be // double mapping. if (indexOnPent) { - Direction revDir = BaseCell.GetNeighbourDirection(baseCell.Cell, originBaseCell.Cell); + var revDir = BaseCell.GetNeighbourDirection(baseCell.Cell, originBaseCell.Cell); if (revDir == Direction.Invalid) throw new Exception("invalid rotation direction"); // Adjust for the different coordinate space in the two base cells. // This is done first because we need to do the pentagon rotations // based on the leading digit in the pentagon's coordinate system. - for (int i = 0; i < baseCellRotations; i += 1) index.RotateCounterClockwise(); + if (baseCellRotations > 0) index.RotateCounterClockwise(baseCellRotations); - Direction indexLeadingDigit = index.LeadingNonZeroDirection; + var indexLeadingDigit = index.LeadingNonZeroDirection; var table = baseCell.IsPolarPentagon ? LookupTables.PolarPentagonRotationsInReverse : LookupTables.NonPolarPentagonRotationsInReverse; @@ -386,23 +388,31 @@ public static H3Index ToH3Index(H3Index origin, CoordIJK ijk) { pentagonRotations = table[(int)revDir, (int)indexLeadingDigit]; if (pentagonRotations < 0) throw new Exception("invalid number of pentagon rotations"); - for (int i = 0; i < pentagonRotations; i += 1) index.RotatePentagonCounterClockwise(); + for (var i = 0; i < pentagonRotations; i += 1) index.RotatePentagonCounterClockwise(); } else { - if (pentagonRotations < 0) throw new Exception("invalid number of pentagon rotations"); - - for (int i = 0; i < pentagonRotations; i += 1) index.RotateCounterClockwise(); + switch (pentagonRotations) { + case < 0: + throw new Exception("invalid number of pentagon rotations"); + case > 0: + index.RotateCounterClockwise(pentagonRotations); + break; + } // Adjust for the different coordinate space in the two base cells. - for (int i = 0; i < baseCellRotations; i += 1) index.RotateCounterClockwise(); + if (baseCellRotations > 0) index.RotateCounterClockwise(baseCellRotations); } } else if (originOnPent && indexOnPent) { - Direction originLeadingDigit = origin.LeadingNonZeroDirection; - Direction indexLeadingDigit = index.LeadingNonZeroDirection; - - int withinPentagonRotations = LookupTables.PentagonRotationsInReverse[(int)originLeadingDigit, (int)indexLeadingDigit]; - if (withinPentagonRotations < 0) throw new Exception("invalid number of within pentagon rotations"); - - for (int i = 0; i < withinPentagonRotations; i += 1) index.RotateCounterClockwise(); + var originLeadingDigit = origin.LeadingNonZeroDirection; + var indexLeadingDigit = index.LeadingNonZeroDirection; + + var withinPentagonRotations = LookupTables.PentagonRotationsInReverse[(int)originLeadingDigit, (int)indexLeadingDigit]; + switch (withinPentagonRotations) { + case < 0: + throw new Exception("invalid number of within pentagon rotations"); + case > 0: + index.RotateCounterClockwise(withinPentagonRotations); + break; + } } if (indexOnPent) { diff --git a/src/H3/Extensions/H3SetExtensions.cs b/src/H3/Extensions/H3SetExtensions.cs index 739a35b..a283625 100644 --- a/src/H3/Extensions/H3SetExtensions.cs +++ b/src/H3/Extensions/H3SetExtensions.cs @@ -25,8 +25,8 @@ public static class H3SetExtensions { /// set of compacted cells public static List Compact(this IEnumerable indexEnumerable) { Dictionary> indexes = new(); - int maxResolution = -1; - int count = 0; + var maxResolution = -1; + var count = 0; // first group by resolution foreach (var index in indexEnumerable) { @@ -34,7 +34,7 @@ public static List Compact(this IEnumerable indexEnumerable) { continue; } - int indexResolution = index.Resolution; + var indexResolution = index.Resolution; maxResolution = Math.Max(maxResolution, indexResolution); if (!indexes.ContainsKey(indexResolution)) { @@ -51,9 +51,9 @@ public static List Compact(this IEnumerable indexEnumerable) { // loop backward through each resolution, throwing any compacted parents into // the resolution below us - for (int resolution = maxResolution; resolution > 0; resolution -= 1) { + for (var resolution = maxResolution; resolution > 0; resolution -= 1) { if (indexes.TryGetValue(resolution, out var toCompact)) { - int parentResolution = resolution - 1; + var parentResolution = resolution - 1; foreach (var index in toCompact) { var parent = index.GetParentForResolution(parentResolution); @@ -68,7 +68,13 @@ public static List Compact(this IEnumerable indexEnumerable) { // any parent that has enough children should be added // back in to be tested at the next lowest resolution. // anything else is uncompactable. + #if NETSTANDARD2_0 + foreach (var item in parents) { + var parent = item.Key; + var children = item.Value; + #else foreach (var (parent, children) in parents) { + #endif if (children.Count >= (parent.IsPentagon ? 6 : 7)) { if (!indexes.ContainsKey(parentResolution)) { indexes[parentResolution] = new HashSet(); @@ -95,18 +101,18 @@ public static List Compact(this IEnumerable indexEnumerable) { /// /// Takes a compacted set of cells and expands back to the original - /// set of cells at a specific resoution. + /// set of cells at a specific resolution. /// /// set of cells /// resolution to expand to - /// original set of cells. Thows ArgumentException if any + /// original set of cells. Throws ArgumentException if any /// cell in the set is smaller than the output resolution or invalid /// resolution is requested. public static IEnumerable UncompactToResolution(this IEnumerable indexes, int resolution) => indexes.Where(index => index != H3Index.Invalid) .Distinct() .SelectMany(index => { - int currentResolution = index.Resolution; + var currentResolution = index.Resolution; if (!IsValidChildResolution(currentResolution, resolution)) { throw new ArgumentException("set contains cell smaller than target resolution"); } @@ -132,7 +138,7 @@ public static IEnumerable UncompactToHighestResolution(this IEnumerable /// not. /// public static bool AreOfSameResolution(this IEnumerable indexes) { - int resolution = -1; + var resolution = -1; foreach (var index in indexes) { if (resolution == -1) { resolution = index.Resolution; diff --git a/src/H3/Extensions/H3UniEdgeExtensions.cs b/src/H3/Extensions/H3UniEdgeExtensions.cs index 4d65728..9dabd62 100644 --- a/src/H3/Extensions/H3UniEdgeExtensions.cs +++ b/src/H3/Extensions/H3UniEdgeExtensions.cs @@ -20,7 +20,7 @@ public static class H3UniEdgeExtensions { /// The unidirectional edge H3Index, or Invalid on failure. /// public static H3Index GetUnidirectionalEdge(this H3Index origin, H3Index destination) { - Direction direction = origin.DirectionForNeighbour(destination); + var direction = origin.DirectionForNeighbour(destination); // The direction will be invalid if the cells are not neighbors if (direction == Direction.Invalid) { @@ -41,12 +41,12 @@ public static H3Index GetUnidirectionalEdge(this H3Index origin, H3Index destina /// Origin H3 index /// All of the unidirectional edges for the H3 origin index. public static IEnumerable GetUnidirectionalEdges(this H3Index origin) { - bool isPentagon = origin.IsPentagon; + var isPentagon = origin.IsPentagon; // This is actually quite simple. Just modify the bits of the origin // slightly for each direction, except the 'k' direction in pentagons, // which is zeroed. - for (int d = 0; d < 6; d += 1) { + for (var d = 0; d < 6; d += 1) { if (isPentagon && d == 0) { yield return H3Index.Invalid; continue; @@ -105,17 +105,17 @@ public static IEnumerable GetUnidirectionalEdgeBoundaryVertices(this H if (!edge.IsUnidirectionalEdgeValid()) { return Enumerable.Empty(); } - Direction direction = (Direction)edge.ReservedBits; - H3Index origin = edge.GetOriginFromUnidirectionalEdge(); + var direction = (Direction)edge.ReservedBits; + var origin = edge.GetOriginFromUnidirectionalEdge(); // get the start vertex for the edge - int startVertex = origin.GetVertexNumberForDirection(direction); + var startVertex = origin.GetVertexNumberForDirection(direction); if (startVertex == H3VertexExtensions.InvalidVertex) { return Enumerable.Empty(); } - FaceIJK face = origin.ToFaceIJK(); - int resolution = origin.Resolution; + var face = origin.ToFaceIJK(); + var resolution = origin.Resolution; return origin.IsPentagon ? face.GetPentagonBoundary(resolution, startVertex, 2) @@ -131,10 +131,10 @@ public static IEnumerable GetUnidirectionalEdgeBoundaryVertices(this H public static double GetExactEdgeLengthInRadians(this H3Index edge) { var vertices = edge.GetUnidirectionalEdgeBoundaryVertices().ToArray(); - double length = 0.0; + var length = 0.0; if (vertices.Length == 0) return length; - for (int i = 0; i < vertices.Length - 1; i += 1) { + for (var i = 0; i < vertices.Length - 1; i += 1) { length += vertices[i].GetPointDistanceInRadians(vertices[i + 1]); } @@ -151,12 +151,12 @@ public static bool IsUnidirectionalEdgeValid(this H3Index edge) { return false; } - Direction neighbourDirection = (Direction)edge.ReservedBits; + var neighbourDirection = (Direction)edge.ReservedBits; if (neighbourDirection is <= Direction.Center or >= Direction.Invalid) { return false; } - H3Index origin = edge.GetOriginFromUnidirectionalEdge(); + var origin = edge.GetOriginFromUnidirectionalEdge(); if (origin.IsPentagon && neighbourDirection == Direction.K) { return false; } diff --git a/src/H3/Extensions/H3VertexExtensions.cs b/src/H3/Extensions/H3VertexExtensions.cs index 08e2a76..98cc617 100644 --- a/src/H3/Extensions/H3VertexExtensions.cs +++ b/src/H3/Extensions/H3VertexExtensions.cs @@ -80,39 +80,39 @@ private static int VertexRotations(this H3Index index) { var fijk = index.ToFaceIJK(); var baseFijk = index.BaseCell.Home; - int ccwRotations = BaseCellRotation + var ccwRotations = BaseCellRotation .GetCounterClockwiseRotationsForBaseCell(index.BaseCellNumber, fijk.Face); - if (index.BaseCell.IsPentagon) { - PentagonDirectionToFaceMapping? dirFaces = null; + if (!index.BaseCell.IsPentagon) + return ccwRotations; - // find the appropriate direction-to-face mapping - for (int p = 0; p < NUM_PENTAGONS; p += 1) { - if (LookupTables.PentagonDirectionFaces[p].BaseCellNumber == index.BaseCellNumber) { - dirFaces = LookupTables.PentagonDirectionFaces[p]; - break; - } - } + PentagonDirectionToFaceMapping? dirFaces = null; - if (dirFaces == null) { - throw new Exception("cant find pentagon direction to face mapping"); - } + // find the appropriate direction-to-face mapping + for (var p = 0; p < NUM_PENTAGONS; p += 1) { + if (LookupTables.PentagonDirectionFaces[p].BaseCellNumber != index.BaseCellNumber) + continue; - // additional CCW rotation for polar neighbors or IK neighbors - if (fijk.Face != baseFijk.Face && index.BaseCell.IsPolarPentagon || fijk.Face == dirFaces.Faces[(int)Direction.IK - DirectionIndexOffset]) { - ccwRotations = (ccwRotations + 1) % 6; - } + dirFaces = LookupTables.PentagonDirectionFaces[p]; + break; + } - // check whether the cell crosses a deleted pentagon subsequence - if (index.LeadingNonZeroDirection == Direction.JK && fijk.Face == dirFaces.Faces[(int)Direction.IK - DirectionIndexOffset]) { - // crosses from JK to IK: Rotate CW - ccwRotations = (ccwRotations + 5) % 6; - } else if (index.LeadingNonZeroDirection == Direction.IK && fijk.Face == dirFaces.Faces[(int)Direction.JK - DirectionIndexOffset]) { - // crosses from IK to JK: Rotate CCW - ccwRotations = (ccwRotations + 1) % 6; - } + if (dirFaces == null) { + throw new Exception("cant find pentagon direction to face mapping"); } + // additional CCW rotation for polar neighbors or IK neighbors + if (fijk.Face != baseFijk.Face && index.BaseCell.IsPolarPentagon || fijk.Face == dirFaces.Faces[(int)Direction.IK - DirectionIndexOffset]) { + ccwRotations = (ccwRotations + 1) % 6; + } + + ccwRotations = index.LeadingNonZeroDirection switch { + // check whether the cell crosses a deleted pentagon subsequence + Direction.JK when fijk.Face == dirFaces.Faces[(int)Direction.IK - DirectionIndexOffset] => (ccwRotations + 5) % 6, + Direction.IK when fijk.Face == dirFaces.Faces[(int)Direction.JK - DirectionIndexOffset] => (ccwRotations + 1) % 6, + _ => ccwRotations + }; + return ccwRotations; } @@ -126,7 +126,7 @@ private static int VertexRotations(this H3Index index) { /// The number for the first topological vertex, or INVALID_VERTEX_NUM /// if the direction is not valid for this cell public static int GetVertexNumberForDirection(this H3Index origin, Direction direction) { - bool isPentagon = origin.IsPentagon; + var isPentagon = origin.IsPentagon; // check for invalid directions if (direction is Direction.Center or >= Direction.Invalid || isPentagon && direction == Direction.K) { @@ -134,7 +134,7 @@ public static int GetVertexNumberForDirection(this H3Index origin, Direction dir } // Determine the vertex rotations for this cell - int rotations = VertexRotations(origin); + var rotations = VertexRotations(origin); // Find the appropriate vertex, rotating CCW if necessary return isPentagon @@ -151,7 +151,7 @@ public static int GetVertexNumberForDirection(this H3Index origin, Direction dir /// The direction for this vertex, or INVALID_DIGIT if the vertex /// number is invalid. public static Direction GetDirectionForVertexNumber(this H3Index origin, int vertexNum) { - bool isPentagon = origin.IsPentagon; + var isPentagon = origin.IsPentagon; // check for invalid vertexes if (vertexNum < 0 || vertexNum > (isPentagon ? NUM_PENT_VERTS : NUM_HEX_VERTS) - 1) { @@ -159,7 +159,7 @@ public static Direction GetDirectionForVertexNumber(this H3Index origin, int ver } // Determine the vertex rotations for this cell - int rotations = VertexRotations(origin); + var rotations = VertexRotations(origin); // Find the appropriate direction, rotating CW if necessary return isPentagon @@ -175,9 +175,9 @@ public static Direction GetDirectionForVertexNumber(this H3Index origin, int ver /// /// public static H3Index GetVertexIndex(this H3Index cell, int vertexNum) { - bool cellIsPentagon = cell.IsPentagon; - int cellNumVerts = cellIsPentagon ? NUM_PENT_VERTS : NUM_HEX_VERTS; - int res = cell.Resolution; + var cellIsPentagon = cell.IsPentagon; + var cellNumVerts = cellIsPentagon ? NUM_PENT_VERTS : NUM_HEX_VERTS; + var res = cell.Resolution; // Check for invalid vertexes if (vertexNum < 0 || vertexNum > cellNumVerts - 1) { @@ -185,66 +185,76 @@ public static H3Index GetVertexIndex(this H3Index cell, int vertexNum) { } // Default the owner and vertex number to the input cell - H3Index owner = cell; - int ownerVertexNum = vertexNum; + var owner = cell; + var ownerVertexNum = vertexNum; // Determine the owner, looking at the three cells that share the vertex. // By convention, the owner is the cell with the lowest numerical index. // If the cell is the center child of its parent, it will always have // the lowest index of any neighbor, so we can skip determining the owner - if (res == 0 || cell.Direction != Direction.Center) { - // Get the left neighbor of the vertex, with its rotations - Direction left = GetDirectionForVertexNumber(cell, vertexNum); + if (res != 0 && cell.Direction == Direction.Center) + return new H3Index(owner) { + Mode = Mode.Vertex, + ReservedBits = ownerVertexNum + }; + + // Get the left neighbor of the vertex, with its rotations + var left = GetDirectionForVertexNumber(cell, vertexNum); - if (left == Direction.Invalid) { + if (left == Direction.Invalid) { + return H3Index.Invalid; + } + + var (leftNeighbour, lRotations) = cell.GetDirectNeighbour(left); + + // Set to owner if lowest index + if (leftNeighbour < owner) { + owner = leftNeighbour; + } + + Direction dir; + + // As above, skip the right neighbor if the left is known lowest + if (res == 0 || leftNeighbour.GetDirectionForResolution(res) != Direction.Center) { + // Get the right neighbor of the vertex, with its rotations + // Note that vertex - 1 is the right side, as vertex numbers are CCW + var right = GetDirectionForVertexNumber(cell, (vertexNum - 1 + cellNumVerts) % cellNumVerts); + + if (right == Direction.Invalid) { return H3Index.Invalid; } - var (leftNeighbour, lRotations) = cell.GetDirectNeighbour(left); + var (rightNeighbour, rRotations) = cell.GetDirectNeighbour(right); // Set to owner if lowest index - if (leftNeighbour < owner) { - owner = leftNeighbour; + if (rightNeighbour < owner) { + owner = rightNeighbour; + dir = owner.IsPentagon + ? owner.DirectionForNeighbour(cell) + : HexDirections[(HexNeighbourDirections[(int)right] + rRotations) % NUM_HEX_VERTS]; + ownerVertexNum = GetVertexNumberForDirection(owner, dir); } + } - // As above, skip the right neighbor if the left is known lowest - if (res == 0 || leftNeighbour.GetDirectionForResolution(res) != Direction.Center) { - // Get the right neighbor of the vertex, with its rotations - // Note that vertex - 1 is the right side, as vertex numbers are CCW - Direction right = GetDirectionForVertexNumber(cell, (vertexNum - 1 + cellNumVerts) % cellNumVerts); - - if (right == Direction.Invalid) { - return H3Index.Invalid; - } - - var (rightNeighbour, rRotations) = cell.GetDirectNeighbour(right); - - // Set to owner if lowest index - if (rightNeighbour < owner) { - owner = rightNeighbour; - Direction dir = owner.IsPentagon - ? owner.DirectionForNeighbour(cell) - : HexDirections[(HexNeighbourDirections[(int)right] + rRotations) % NUM_HEX_VERTS]; - ownerVertexNum = GetVertexNumberForDirection(owner, dir); - } - } + // Determine the vertex number for the left neighbor + if (owner != leftNeighbour) + return new H3Index(owner) { + Mode = Mode.Vertex, + ReservedBits = ownerVertexNum + }; - // Determine the vertex number for the left neighbor - if (owner == leftNeighbour) { - bool ownerIsPentagon = owner.IsPentagon; - Direction dir = ownerIsPentagon - ? owner.DirectionForNeighbour(cell) - : HexDirections[(HexNeighbourDirections[(int)left] + lRotations) % NUM_HEX_VERTS]; + var ownerIsPentagon = owner.IsPentagon; + dir = ownerIsPentagon + ? owner.DirectionForNeighbour(cell) + : HexDirections[(HexNeighbourDirections[(int)left] + lRotations) % NUM_HEX_VERTS]; - // For the left neighbor, we need the second vertex of the - // edge, which may involve looping around the vertex nums - ownerVertexNum = GetVertexNumberForDirection(owner, dir) + 1; + // For the left neighbor, we need the second vertex of the + // edge, which may involve looping around the vertex nums + ownerVertexNum = GetVertexNumberForDirection(owner, dir) + 1; - if (ownerVertexNum == NUM_HEX_VERTS || ownerIsPentagon && ownerVertexNum == NUM_PENT_VERTS) { - ownerVertexNum = 0; - } - } + if (ownerVertexNum == NUM_HEX_VERTS || ownerIsPentagon && ownerVertexNum == NUM_PENT_VERTS) { + ownerVertexNum = 0; } // Create the vertex index @@ -260,8 +270,8 @@ public static H3Index GetVertexIndex(this H3Index cell, int vertexNum) { /// /// public static IEnumerable GetVertexIndicies(this H3Index cell) { - int count = cell.IsPentagon ? NUM_PENT_VERTS : NUM_HEX_VERTS; - for (int i = 0; i < count; i += 1) { + var count = cell.IsPentagon ? NUM_PENT_VERTS : NUM_HEX_VERTS; + for (var i = 0; i < count; i += 1) { yield return cell.GetVertexIndex(i); } } @@ -273,14 +283,14 @@ public static IEnumerable GetVertexIndicies(this H3Index cell) { /// public static GeoCoord VertexToGeoCoord(this H3Index vertex) { // Get the vertex number and owner from the vertex - int vertexNum = vertex.ReservedBits; + var vertexNum = vertex.ReservedBits; H3Index owner = new(vertex) { Mode = Mode.Cell, ReservedBits = 0 }; - FaceIJK fijk = owner.ToFaceIJK(); - int resolution = owner.Resolution; + var fijk = owner.ToFaceIJK(); + var resolution = owner.Resolution; var vertices = owner.IsPentagon ? fijk.GetPentagonBoundary(resolution, vertexNum, 1) @@ -299,7 +309,7 @@ public static bool IsValidVertex(this H3Index vertex) { return false; } - int vertexNum = vertex.ReservedBits; + var vertexNum = vertex.ReservedBits; H3Index owner = new(vertex) { Mode = Mode.Cell, ReservedBits = 0 @@ -311,7 +321,7 @@ public static bool IsValidVertex(this H3Index vertex) { // The easiest way to ensure that the owner + vertex number is valid, // and that the vertex is canonical, is to recreate and compare. - H3Index canonical = owner.GetVertexIndex(vertexNum); + var canonical = owner.GetVertexIndex(vertexNum); return vertex == canonical; } diff --git a/src/H3/H3.csproj b/src/H3/H3.csproj index 203577a..fe03105 100644 --- a/src/H3/H3.csproj +++ b/src/H3/H3.csproj @@ -1,11 +1,12 @@ - pocketken.H3.3.7.2.0 - net5.0 + pocketken.H3.3.7.2.1 + net6.0;net5.0;netstandard2.1;netstandard2.0 + 9.0 true pocketken.H3 - 3.7.2.0 + 3.7.2.1 pocketken Port of Uber's H3 to .NET https://github.com/pocketken/H3.net @@ -14,27 +15,59 @@ true LICENSE - 3.7.2.0 + 3.7.2.1 - C:\Dev\H3.net\src\H3\pocketken.H3.3.7.2.0.xml + pocketken.H3.3.7.2.1.xml + + + + true + $(MSBuildThisFileDirectory)/Generated - + + + + + + + + + + + + + + + + + false + Analyzer + + + + + + + + - - + True + diff --git a/src/H3/H3Index.cs b/src/H3/H3Index.cs index bfe1b06..52933b4 100644 --- a/src/H3/H3Index.cs +++ b/src/H3/H3Index.cs @@ -12,7 +12,7 @@ namespace H3 { [JsonConverter(typeof(H3IndexJsonConverter))] - public sealed class H3Index : IComparable { + public sealed partial class H3Index : IComparable { #region constants @@ -59,7 +59,7 @@ public sealed class H3Index : IComparable { public BaseCell BaseCell { [MethodImpl(MethodImplOptions.AggressiveInlining)] - get => LookupTables.BaseCells[BaseCellNumber]; + get => BaseCells.Cells[BaseCellNumber]; } /// @@ -155,7 +155,7 @@ public bool IsValid { // The first nonzero digit can't be a 1 (i.e., "deleted subsequence", // PENTAGON_SKIPPED_DIGIT, or K_AXES_DIGIT). // Test for pentagon base cell first to avoid this loop if possible. - if (LookupTables.BaseCells[bc].IsPentagon) { + if (BaseCells.Cells[bc].IsPentagon) { while (r <= res) { var d = value.GetTopBits(N_BITS_DIGIT); if (d == (ulong)Direction.Center) { @@ -163,8 +163,6 @@ public bool IsValid { } else if (d == (ulong)Direction.K) { return false; } else { - // But don't increment `r`, since we still need to - // check that it isn't INVALID_DIGIT. break; } r++; @@ -181,7 +179,7 @@ public bool IsValid { // Now check that all the unused digits after `res` are // set to 7 (INVALID_DIGIT). - int shift = (int)(15 - res) * 3; + var shift = (int)(15 - res) * 3; ulong mask = 0; mask = ~mask; mask >>= shift; @@ -196,8 +194,8 @@ public bool IsValid { public Direction LeadingNonZeroDirection { [MethodImpl(MethodImplOptions.AggressiveInlining)] get { - int resolution = Resolution; - for (int r = 1; r <= resolution; r += 1) { + var resolution = Resolution; + for (var r = 1; r <= resolution; r += 1) { var idx = GetDirectionForResolution(r); if (idx != Direction.Center) { return idx; @@ -233,7 +231,7 @@ public H3Index(ulong value) { } public H3Index(string value) { - if (ulong.TryParse(value, NumberStyles.HexNumber, null, out ulong parsed)) Value = parsed; + if (ulong.TryParse(value, NumberStyles.HexNumber, null, out var parsed)) Value = parsed; } public static H3Index Create(int resolution, int baseCell, Direction direction) { @@ -244,7 +242,7 @@ public static H3Index Create(int resolution, int baseCell, Direction direction) BaseCellNumber = baseCell }; - for (int r = 1; r <= resolution; r += 1) index.SetDirectionForResolution(r, direction); + for (var r = 1; r <= resolution; r += 1) index.SetDirectionForResolution(r, direction); return index; } @@ -270,7 +268,7 @@ public Direction GetDirectionForResolution(int resolution) { /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public void SetDirectionForResolution(int resolution, Direction direction) { - int offset = (MAX_H3_RES - resolution) * H3_PER_DIGIT_OFFSET; + var offset = (MAX_H3_RES - resolution) * H3_PER_DIGIT_OFFSET; Value = (Value & ~(H3_DIGIT_MASK << offset)) | ((ulong)direction << offset); } @@ -281,7 +279,7 @@ public void SetDirectionForResolution(int resolution, Direction direction) { /// [MethodImpl(MethodImplOptions.AggressiveInlining)] internal void IncrementDirectionForResolution(int resolution) { - ulong val = 1UL; + var val = 1UL; val <<= H3_PER_DIGIT_OFFSET * (15 - resolution); Value += val; } @@ -296,7 +294,7 @@ internal void IncrementDirectionForResolution(int resolution) { internal void ZeroDirectionsForResolutionRange(int startResolution, int endResolution) { if (startResolution > endResolution) return; - ulong m = ~0UL; + var m = ~0UL; m <<= H3_PER_DIGIT_OFFSET * (endResolution - startResolution + 1); m = ~m; m <<= H3_PER_DIGIT_OFFSET * (15 - endResolution); @@ -315,7 +313,7 @@ internal void ZeroDirectionsForResolutionRange(int startResolution, int endResol internal void InvalidateDirectionsForResolutionRange(int startResolution, int endResolution) { if (startResolution > endResolution) return; - ulong m = ~0UL; + var m = ~0UL; m <<= H3_PER_DIGIT_OFFSET * (endResolution - startResolution + 1); m = ~m; m <<= H3_PER_DIGIT_OFFSET * (15 - endResolution); @@ -323,14 +321,27 @@ internal void InvalidateDirectionsForResolutionRange(int startResolution, int en Value |= m; } + /// + /// Performs an in-place 60 degree clockwise rotation of the index. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void RotateClockwise() => RotateClockwise(1); + + /// + /// Performs an in-place 60 degree clockwise rotation of the index. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void RotateCounterClockwise() => RotateCounterClockwise(1); + /// /// Performs an in-place 60 degree counter-clockwise pentagonal rotation of the index. /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] public void RotatePentagonCounterClockwise() { - int resolution = Resolution; - bool foundFirstNonZeroDigit = false; + var resolution = Resolution; + var foundFirstNonZeroDigit = false; - for (int r = 1; r <= resolution; r += 1) { + for (var r = 1; r <= resolution; r += 1) { // rotate digit SetDirectionForResolution(r, GetDirectionForResolution(r).RotateCounterClockwise()); @@ -352,11 +363,12 @@ public void RotatePentagonCounterClockwise() { /// /// Performs an in-place 60 degree clockwise pentagonal rotation of the index. /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] public void RotatePentagonClockwise() { - int resolution = Resolution; - bool foundFirstNonZeroDigit = false; + var resolution = Resolution; + var foundFirstNonZeroDigit = false; - for (int r = 1; r <= resolution; r += 1) { + for (var r = 1; r <= resolution; r += 1) { // rotate digit SetDirectionForResolution(r, GetDirectionForResolution(r).RotateClockwise()); @@ -375,26 +387,6 @@ public void RotatePentagonClockwise() { } } - /// - /// Performs an in-place 60 degree counter-clockwise rotation of the index. - /// - public void RotateCounterClockwise() { - // rotate in place - int resolution = Resolution; - for (int r = 1; r <= resolution; r += 1) - SetDirectionForResolution(r, GetDirectionForResolution(r).RotateCounterClockwise()); - } - - /// - /// Performs an in-place 60 degree clockwise rotation of the index. - /// - public void RotateClockwise() { - // rotate in place - int resolution = Resolution; - for (int r = 1; r <= resolution; r += 1) - SetDirectionForResolution(r, GetDirectionForResolution(r).RotateClockwise()); - } - #endregion manipulations #region conversions @@ -406,12 +398,12 @@ public void RotateClockwise() { /// /// public bool ToFaceWithInitializedFijk(FaceIJK faceIjk) { - int resolution = Resolution; + var resolution = Resolution; // center base cell hierarchy is entirely on this face - bool possibleOverage = !(!BaseCell.IsPentagon && (resolution == 0 || faceIjk.Coord.I == 0 && faceIjk.Coord.J == 0 && faceIjk.Coord.K == 0)); + var possibleOverage = !(!BaseCell.IsPentagon && (resolution == 0 || faceIjk.Coord.I == 0 && faceIjk.Coord.J == 0 && faceIjk.Coord.K == 0)); - for (int r = 1; r <= resolution; r += 1) { + for (var r = 1; r <= resolution; r += 1) { if (IsResolutionClass3(r)) { faceIjk.Coord.DownAperture7CounterClockwise(); } else { @@ -429,7 +421,7 @@ public bool ToFaceWithInitializedFijk(FaceIJK faceIjk) { /// /// public FaceIJK ToFaceIJK(FaceIJK? toUpdateFijk = default) { - H3Index index = this; + var index = this; if (BaseCell.IsPentagon && LeadingNonZeroDirection == Direction.IK) { index = new(this); @@ -492,7 +484,7 @@ public FaceIJK ToFaceIJK(FaceIJK? toUpdateFijk = default) { /// /// Determines the spherical coordinates of the center point of a H3 - /// index, and returns it as a NTS Point. + /// index, and returns it as a NTS . /// /// GeometryFactory to be used to create /// point; defaults to DefaultGeometryFactory. Note that coordinates @@ -583,9 +575,7 @@ public static H3Index FromFaceIJK(FaceIJK face, int resolution) { index.RotatePentagonCounterClockwise(); } } else { - for (var i = 0; i < numRotations; i += 1) { - index.RotateCounterClockwise(); - } + index.RotateCounterClockwise(numRotations); } return index; diff --git a/src/H3/IsExternalInit.cs b/src/H3/IsExternalInit.cs new file mode 100644 index 0000000..f85947c --- /dev/null +++ b/src/H3/IsExternalInit.cs @@ -0,0 +1,16 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.ComponentModel; + +// ReSharper disable once CheckNamespace +namespace System.Runtime.CompilerServices { + /// + /// Reserved to be used by the compiler for tracking metadata. + /// This class should not be used by developers in source code. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public static class IsExternalInit { } + +} \ No newline at end of file diff --git a/src/H3/Model/BaseCell.cs b/src/H3/Model/BaseCell.cs index 9177b41..bbc2b4e 100644 --- a/src/H3/Model/BaseCell.cs +++ b/src/H3/Model/BaseCell.cs @@ -1,88 +1,88 @@ using System; +using System.Collections.Generic; #nullable enable namespace H3.Model { - /// - /// Definition for one of the 122 base cells that form the H3 indexing scheme. - /// public sealed class BaseCell { + /// /// The cell number, from 0 - 121. /// - public int Cell { get; private init; } + public sbyte Cell { get; internal init; } /// /// The home face and IJK address of the cell. /// - public FaceIJK Home { get; private init; } = null!; + public FaceIJK Home { get; internal init; } = null!; /// /// Whether or not this base cell is a pentagon. /// - public bool IsPentagon { get; private init; } + public bool IsPentagon { get; internal init; } /// /// If a pentagon, the cell's two clockwise offset faces. /// - public int[] ClockwiseOffsetPent { get; private init; } = null!; + internal sbyte[] ClockwiseOffsetPent { get; init; } = null!; /// /// Whether or not the cell is a polar pentagon. /// - public bool IsPolarPentagon => Cell is 4 or 117; + public bool IsPolarPentagon { get; internal init; } + + /// + /// All of the neighbouring s of this cell, by + /// . + /// + public sbyte[] NeighbouringCells { get; internal init; } = null!; - private BaseCell() { } + /// + /// Indicates the number of counter-clockwise rotations that should + /// take place to rotate to a given neighbour, by . + /// + public sbyte[] NeighbourRotations { get; internal init; } = null!; + + /// + /// A map of neighbour cell number to . + /// + public Dictionary NeighbourDirections { get; internal init; } = null!; /// /// Whether or not the specified matches one of this - /// base cell's values. + /// base cell's offset values. /// /// /// - public bool FaceMatchesOffset(int face) => ClockwiseOffsetPent[0] == face || ClockwiseOffsetPent[1] == face; + public bool FaceMatchesOffset(int face) { + return face == ClockwiseOffsetPent[0] || face == ClockwiseOffsetPent[1]; + } /// - /// Returns the neighbouring in the specified . + /// Gets the that is in the specified + /// from the current base cell. /// /// /// public BaseCell? Neighbour(Direction direction) { - var neighbourIndex = LookupTables.Neighbours[Cell, (int)direction]; - return neighbourIndex == LookupTables.INVALID_BASE_CELL ? null : LookupTables.BaseCells[neighbourIndex]; + var cellNumber = NeighbouringCells[(int)direction]; + return cellNumber == LookupTables.INVALID_BASE_CELL ? null : BaseCells.Cells[cellNumber]; } /// - /// Gets the required to move between the two specified - /// numbers. Returns if the cells are not neighbours. + /// Gets the that is required to move in + /// order to go from to + /// . /// /// - /// + /// /// - public static Direction GetNeighbourDirection(int originCell, int neighbouringCell) { - for (var idx = Direction.Center; idx < Direction.Invalid; idx += 1) { - if (LookupTables.Neighbours[originCell, (int)idx] == neighbouringCell) { - return idx; - } - } - - return Direction.Invalid; + public static Direction GetNeighbourDirection(sbyte originCell, sbyte destinationCell) { + var originBaseCell = BaseCells.Cells[originCell]; + return originBaseCell.NeighbourDirections.TryGetValue(destinationCell, out var direction) ? direction : Direction.Invalid; } - /// - /// Creates a from an input set of parameters. - /// - /// - /// - public static implicit operator BaseCell((int, (int, (int, int, int)), int, (int, int)) tuple) => - new() { - Cell = tuple.Item1, - Home = new FaceIJK(tuple.Item2.Item1, tuple.Item2.Item2), - IsPentagon = tuple.Item3 == 1, - ClockwiseOffsetPent = new[] { tuple.Item4.Item1, tuple.Item4.Item2 } - }; - /// /// Whether or not two instances are equal. /// @@ -111,7 +111,8 @@ public override bool Equals(object? other) { return other is BaseCell b && Cell == b.Cell; } - public override int GetHashCode() => HashCode.Combine(Cell, Home, IsPentagon, ClockwiseOffsetPent); + public override int GetHashCode() => HashCode.Combine(Cell); + } -} +} \ No newline at end of file diff --git a/src/H3/Model/BaseCellRotation.cs b/src/H3/Model/BaseCellRotation.cs index 2d21dc7..ec82826 100644 --- a/src/H3/Model/BaseCellRotation.cs +++ b/src/H3/Model/BaseCellRotation.cs @@ -18,7 +18,7 @@ public static implicit operator BaseCellRotation((int, int) tuple) => new() { Cell = tuple.Item1, CounterClockwiseRotations = tuple.Item2, - BaseCell = LookupTables.BaseCells[tuple.Item1] + BaseCell = BaseCells.Cells[tuple.Item1] }; public static int GetCounterClockwiseRotationsForBaseCell(int cell, int face) { diff --git a/src/H3/Model/CoordIJK.cs b/src/H3/Model/CoordIJK.cs index 5335f6c..bc581fb 100644 --- a/src/H3/Model/CoordIJK.cs +++ b/src/H3/Model/CoordIJK.cs @@ -112,11 +112,11 @@ public static CoordIJK FromVec2d(double x, double y, CoordIJK? destination = def if (h.J % 2 == 0) { // even long axisi = h.J / 2; - long diff = h.I - axisi; + var diff = h.I - axisi; h.I = (int)(h.I - 2.0 * diff); } else { long axisi = (h.J + 1) / 2; - long diff = h.I - axisi; + var diff = h.I - axisi; h.I = (int)(h.I - (2.0 * diff + 1)); } } @@ -179,14 +179,15 @@ public CoordIJK Normalize() { /// Rotates ijk coordinates 60 degrees counter-clockwise. Works in place. /// /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] public CoordIJK RotateCounterClockwise() { - var uVecI = LookupTables.DirectionToUnitVector[Direction.IJ]; - var uVecJ = LookupTables.DirectionToUnitVector[Direction.JK]; - var uVecK = LookupTables.DirectionToUnitVector[Direction.IK]; + var uVecI = LookupTables.UnitVectors[(int)Direction.IJ]; + var uVecJ = LookupTables.UnitVectors[(int)Direction.JK]; + var uVecK = LookupTables.UnitVectors[(int)Direction.IK]; - int i = I * uVecI.I + J * uVecJ.I + K * uVecK.I; - int j = I * uVecI.J + J * uVecJ.J + K * uVecK.J; - int k = I * uVecI.K + J * uVecJ.K + K * uVecK.K; + var i = I * uVecI.I + J * uVecJ.I + K * uVecK.I; + var j = I * uVecI.J + J * uVecJ.J + K * uVecK.J; + var k = I * uVecI.K + J * uVecJ.K + K * uVecK.K; I = i; J = j; @@ -199,14 +200,15 @@ public CoordIJK RotateCounterClockwise() { /// Rotates ijk coordinates 60 degrees clockwise. Works in place. /// /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] public CoordIJK RotateClockwise() { - var uVecI = LookupTables.DirectionToUnitVector[Direction.IK]; - var uVecJ = LookupTables.DirectionToUnitVector[Direction.IJ]; - var uVecK = LookupTables.DirectionToUnitVector[Direction.JK]; + var uVecI = LookupTables.UnitVectors[(int)Direction.IK]; + var uVecJ = LookupTables.UnitVectors[(int)Direction.IJ]; + var uVecK = LookupTables.UnitVectors[(int)Direction.JK]; - int i = I * uVecI.I + J * uVecJ.I + K * uVecK.I; - int j = I * uVecI.J + J * uVecJ.J + K * uVecK.J; - int k = I * uVecI.K + J * uVecJ.K + K * uVecK.K; + var i = I * uVecI.I + J * uVecJ.I + K * uVecK.I; + var j = I * uVecI.J + J * uVecJ.J + K * uVecK.J; + var k = I * uVecI.K + J * uVecJ.K + K * uVecK.K; I = i; J = j; @@ -220,9 +222,10 @@ public CoordIJK RotateClockwise() { /// counter-clockwise aperture 7 grid. Works in place. /// /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] public CoordIJK UpAperture7CounterClockwise() { - int i = I - K; - int j = J - K; + var i = I - K; + var j = J - K; I = (int)Math.Round((3 * i - j) / 7.0, MidpointRounding.AwayFromZero); J = (int)Math.Round((i + 2 * j) / 7.0, MidpointRounding.AwayFromZero); @@ -236,9 +239,10 @@ public CoordIJK UpAperture7CounterClockwise() { /// clockwise aperture 7 grid. Works in place. /// /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] public CoordIJK UpAperture7Clockwise() { - int i = I - K; - int j = J - K; + var i = I - K; + var j = J - K; I = (int)Math.Round((2 * i + j) / 7.0, MidpointRounding.AwayFromZero); J = (int)Math.Round((3 * j - i) / 7.0, MidpointRounding.AwayFromZero); @@ -253,10 +257,11 @@ public CoordIJK UpAperture7Clockwise() { /// place. /// /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] public CoordIJK DownAperture7CounterClockwise() { - int i = 3 * I + J; - int j = 3 * J + K; - int k = I + 3 * K; + var i = 3 * I + J; + var j = 3 * J + K; + var k = I + 3 * K; I = i; J = j; @@ -270,10 +275,11 @@ public CoordIJK DownAperture7CounterClockwise() { /// hex at the next finer aperture 7 clockwise resolution. Works in place. /// /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] public CoordIJK DownAperture7Clockwise() { - int i = 3 * I + K; - int j = I + 3 * J; - int k = J + 3 * K; + var i = 3 * I + K; + var j = I + 3 * J; + var k = J + 3 * K; I = i; J = j; @@ -288,10 +294,11 @@ public CoordIJK DownAperture7Clockwise() { /// place. /// /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] public CoordIJK DownAperture3CounterClockwise() { - int i = 2 * I + J; - int j = 2 * J + K; - int k = I + 2 * K; + var i = 2 * I + J; + var j = 2 * J + K; + var k = I + 2 * K; I = i; J = j; @@ -305,10 +312,11 @@ public CoordIJK DownAperture3CounterClockwise() { /// hex at the next finer aperture 3 clockwise resolution. Works in place. /// /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] public CoordIJK DownAperture3Clockwise() { - int i = 2 * I + K; - int j = I + 2 * J; - int k = J + 2 * K; + var i = 2 * I + K; + var j = I + 2 * J; + var k = J + 2 * K; I = i; J = j; @@ -321,6 +329,7 @@ public CoordIJK DownAperture3Clockwise() { /// Convert IJK coordinates to cube coordinates, in place. /// /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] public CoordIJK Cube() { I = -I + K; J -= K; @@ -332,6 +341,7 @@ public CoordIJK Cube() { /// Convert cube coordinates to IJK coordinates, in place /// /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] public CoordIJK Uncube() { I = -I; K = 0; @@ -344,11 +354,12 @@ public CoordIJK Uncube() { /// /// The digit direction from the original ijk coordinates. /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] public CoordIJK ToNeighbour(Direction direction) { if (direction is <= Direction.Center or >= Direction.Invalid) return this; - var unitVector = LookupTables.DirectionToUnitVector[direction]; + var unitVector = LookupTables.UnitVectors[(int)direction]; I += unitVector.I; J += unitVector.J; K += unitVector.K; @@ -370,9 +381,17 @@ public Vec2d ToVec2d() { return new Vec2d(GetVec2dOrdinates()); } + public Vec2d ToVec2d(Vec2d toUpdate) { + var (x, y) = GetVec2dOrdinates(); + toUpdate.X = x; + toUpdate.Y = y; + return toUpdate; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] public (double, double) GetVec2dOrdinates() { - int i = I - K; - int j = J - K; + var i = I - K; + var j = J - K; return (i - 0.5 * j, j * M_SQRT3_2); } @@ -459,8 +478,16 @@ public static implicit operator CoordIJK((int, int, int) coordinates) => /// Determines the H3 digit corresponding to a unit vector in ijk coordinates. /// /// + #if NETSTANDARD2_0 + public static implicit operator Direction(CoordIJK h) { + var unitVector = Normalize(h); + if (!LookupTables.UnitVectorToDirection.ContainsKey(unitVector)) return Direction.Invalid; + return LookupTables.UnitVectorToDirection[unitVector]; + } + #else public static implicit operator Direction(CoordIJK h) => LookupTables.UnitVectorToDirection.GetValueOrDefault(Normalize(h), Direction.Invalid); + #endif /// /// Returns a new ijk coordinate containing the sum of two ijk diff --git a/src/H3/Model/Enums.cs b/src/H3/Model/Enums.cs index d416d8a..9dbb653 100644 --- a/src/H3/Model/Enums.cs +++ b/src/H3/Model/Enums.cs @@ -1,5 +1,7 @@ #nullable enable +using System.Runtime.CompilerServices; + namespace H3.Model { public enum Direction { @@ -14,21 +16,56 @@ public enum Direction { } public static class DirectionExtensions { + /// + /// Clockwise rotation steps, by and number of rotations from 0-5. + /// + private static readonly Direction[,] Clockwise = { + { Direction.Center, Direction.Center, Direction.Center, Direction.Center, Direction.Center, Direction.Center }, + { Direction.K, Direction.JK, Direction.J, Direction.IJ, Direction.I, Direction.IK }, + { Direction.J, Direction.IJ, Direction.I, Direction.IK, Direction.K, Direction.JK }, + { Direction.JK, Direction.J, Direction.IJ, Direction.I, Direction.IK, Direction.K }, + { Direction.I, Direction.IK, Direction.K, Direction.JK, Direction.J, Direction.IJ }, + { Direction.IK, Direction.K, Direction.JK, Direction.J, Direction.IJ, Direction.I }, + { Direction.IJ, Direction.I, Direction.IK, Direction.K, Direction.JK, Direction.J }, + { Direction.Invalid, Direction.Invalid, Direction.Invalid, Direction.Invalid, Direction.Invalid, Direction.Invalid } + }; + + /// + /// Counter-clockwise rotation steps, by and number of rotations from 0-5. + /// + private static readonly Direction[,] CounterClockwise = { + { Direction.Center, Direction.Center, Direction.Center, Direction.Center, Direction.Center, Direction.Center }, + { Direction.K, Direction.IK, Direction.I, Direction.IJ, Direction.J, Direction.JK }, + { Direction.J , Direction.JK, Direction.K, Direction.IK, Direction.I, Direction.IJ }, + { Direction.JK, Direction.K, Direction.IK, Direction.I, Direction.IJ, Direction.J }, + { Direction.I, Direction.IJ, Direction.J, Direction.JK, Direction.K, Direction.IK }, + { Direction.IK, Direction.I, Direction.IJ, Direction.J, Direction.JK, Direction.K }, + { Direction.IJ, Direction.J, Direction.JK, Direction.K, Direction.IK, Direction.I }, + { Direction.Invalid, Direction.Invalid, Direction.Invalid, Direction.Invalid, Direction.Invalid, Direction.Invalid } + }; + /// /// Returns the Direction that is 60 degrees clockwise to the current /// direction. /// /// /// - public static Direction RotateClockwise(this Direction direction) => direction switch { - Direction.K => Direction.JK, - Direction.JK => Direction.J, - Direction.J => Direction.IJ, - Direction.IJ => Direction.I, - Direction.I => Direction.IK, - Direction.IK => Direction.K, - _ => direction - }; + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Direction RotateClockwise(this Direction direction) { + return Clockwise[(int)direction, 1]; + } + + /// + /// Returns the Direction that is 60 degrees clockwise to the current + /// direction. + /// + /// + /// number of rotations to perform + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Direction RotateClockwise(this Direction direction, int rotations) { + return Clockwise[(int)direction, rotations % 6]; + } /// /// Returns the Direction that is 60 degrees counter-clockwise to the current @@ -36,15 +73,22 @@ public static class DirectionExtensions { /// /// /// - public static Direction RotateCounterClockwise(this Direction direction) => direction switch { - Direction.K => Direction.IK, - Direction.IK => Direction.I, - Direction.I => Direction.IJ, - Direction.IJ => Direction.J, - Direction.J => Direction.JK, - Direction.JK => Direction.K, - _ => direction - }; + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Direction RotateCounterClockwise(this Direction direction) { + return CounterClockwise[(int)direction, 1]; + } + + /// + /// Returns the Direction that is 60 degrees counter-clockwise to the current + /// direction. + /// + /// + /// number of rotations to perform + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Direction RotateCounterClockwise(this Direction direction, int rotations) { + return CounterClockwise[(int)direction, rotations % 6]; + } } public enum Mode { diff --git a/src/H3/Model/FaceIJK.cs b/src/H3/Model/FaceIJK.cs index 1ab6361..f9edb51 100644 --- a/src/H3/Model/FaceIJK.cs +++ b/src/H3/Model/FaceIJK.cs @@ -9,6 +9,8 @@ namespace H3.Model { public sealed class FaceIJK { + private const double THREE_M_SQRT32 = 3.0 * M_SQRT3_2; + public int Face { get; set; } public CoordIJK Coord { get; set; } = new(0, 0, 0); @@ -16,10 +18,8 @@ public sealed class FaceIJK { public const int KI = 2; public const int JK = 3; - public BaseCellRotation? BaseCellRotation - { - get - { + public BaseCellRotation? BaseCellRotation { + get { if (Coord.I > MAX_FACE_COORD || Coord.J > MAX_FACE_COORD || Coord.K > MAX_FACE_COORD) return null; return LookupTables.FaceIjkBaseCells[Face, Coord.I, Coord.J, Coord.K]; } @@ -38,17 +38,17 @@ public FaceIJK(int face, CoordIJK coord) { } public static FaceIJK FromGeoCoord(double longitudeRadians, double latitudeRadians, int resolution, FaceIJK? toUpdate = default, Vec3d? workVec3d = default) { - Vec3d v3d = Vec3d.FromLonLat(longitudeRadians, latitudeRadians, workVec3d); - FaceIJK result = toUpdate ?? new FaceIJK(); + var v3d = Vec3d.FromLonLat(longitudeRadians, latitudeRadians, workVec3d); + var result = toUpdate ?? new FaceIJK(); result.Face = 0; result.Coord.I = 0; result.Coord.J = 0; result.Coord.K = 0; - double sqd = v3d.PointSquareDistance(LookupTables.FaceCenters[0]); + var sqd = v3d.PointSquareDistance(LookupTables.FaceCenters[0]); for (var f = 1; f < NUM_ICOSA_FACES; f += 1) { - double sqdT = v3d.PointSquareDistance(LookupTables.FaceCenters[f]); + var sqdT = v3d.PointSquareDistance(LookupTables.FaceCenters[f]); if (!(sqdT < sqd)) continue; @@ -62,8 +62,8 @@ public static FaceIJK FromGeoCoord(double longitudeRadians, double latitudeRadia if (r >= EPSILON) { var center = LookupTables.GeoFaceCenters[result.Face]; - double az = NormalizeAngle(AzimuthInRadians(center.Longitude, center.Latitude, longitudeRadians, latitudeRadians)); - double theta = NormalizeAngle(LookupTables.AxisAzimuths[result.Face] - az); + var az = NormalizeAngle(AzimuthInRadians(center.Longitude, center.Latitude, longitudeRadians, latitudeRadians)); + var theta = NormalizeAngle(LookupTables.AxisAzimuths[result.Face] - az); if (IsResolutionClass3(resolution)) theta = NormalizeAngle(theta - M_AP7_ROT_RADS); @@ -78,6 +78,7 @@ public static FaceIJK FromGeoCoord(double longitudeRadians, double latitudeRadia return result; } + // TODO provide version that reuses result array private FaceIJK[] GetVertices(CoordIJK[] class3Verts, CoordIJK[] class2Verts, ref int resolution) { var verts = IsResolutionClass3(resolution) ? class3Verts : class2Verts; Coord.DownAperture3CounterClockwise(); @@ -128,12 +129,12 @@ public FaceIJK[] GetPentagonVertices(ref int resolution) => /// Whether or not the cell is on a substrate grid /// public Overage AdjustOverageClass2(int resolution, bool pentagonLeading4, bool isSubstrate) { - Overage overage = Overage.None; + var overage = Overage.None; - int maxDist = LookupTables.MaxDistanceByClass2Res[resolution]; + var maxDist = LookupTables.MaxDistanceByClass2Res[resolution]; if (isSubstrate) maxDist *= 3; - int sum = Coord.I + Coord.J + Coord.K; + var sum = Coord.I + Coord.J + Coord.K; if (isSubstrate && sum == maxDist) { overage = Overage.FaceEdge; } else if (sum > maxDist) { @@ -162,11 +163,11 @@ public Overage AdjustOverageClass2(int resolution, bool pentagonLeading4, bool i Face = orientedFace.Face; // rotate and translate for adjacent face - for (int i = 0; i < orientedFace.CounterClockwiseRotations; i += 1) { + for (var i = 0; i < orientedFace.CounterClockwiseRotations; i += 1) { Coord.RotateCounterClockwise(); } - int unitScale = LookupTables.UnitScaleByClass2Res[resolution]; + var unitScale = LookupTables.UnitScaleByClass2Res[resolution]; if (isSubstrate) unitScale *= 3; Coord.I += orientedFace.Translate.I * unitScale; Coord.J += orientedFace.Translate.J * unitScale; @@ -208,23 +209,33 @@ public Overage AdjustPentagonVertexOverage(int resolution) { /// The number of topological vertexes to return /// The spherical coordinates of the cell boundary public IEnumerable GetPentagonBoundary(int resolution, int start, int length) { - int adjustedResolution = resolution; - FaceIJK centerIJK = new(this); - FaceIJK[] verts = centerIJK.GetPentagonVertices(ref adjustedResolution); + var adjustedResolution = resolution; + FaceIJK centerIjk = new(this); + var verts = centerIjk.GetPentagonVertices(ref adjustedResolution); // If we're returning the entire loop, we need one more iteration in case // of a distortion vertex on the last edge - int additionalIteration = length == NUM_PENT_VERTS ? 1 : 0; + var additionalIteration = length == NUM_PENT_VERTS ? 1 : 0; // convert each vertex to lat/lon // adjust the face of each vertex as appropriate and introduce // edge-crossing vertices as needed + Vec2d v0 = new(); + Vec2d v1 = new(); + Vec2d v2 = new(); + Vec2d orig2d0 = new(); + Vec2d orig2d1 = new(); + + var fijk = new FaceIJK(); FaceIJK lastFijk = new(); - for (int vert = start; vert < start + length + additionalIteration; vert += 1) { - int v = vert % NUM_PENT_VERTS; + for (var vert = start; vert < start + length + additionalIteration; vert += 1) { + var v = vert % NUM_PENT_VERTS; - FaceIJK fijk = new(verts[v]); + fijk.Face = verts[v].Face; + fijk.Coord.I = verts[v].Coord.I; + fijk.Coord.J = verts[v].Coord.J; + fijk.Coord.K = verts[v].Coord.K; fijk.AdjustPentagonVertexOverage(adjustedResolution); // all Class III pentagon edges cross icosa edges @@ -233,16 +244,16 @@ public IEnumerable GetPentagonBoundary(int resolution, int start, int if (IsResolutionClass3(resolution) && vert > start) { // find hex2d of the two vertexes on the last face FaceIJK tmpFijk = new(fijk); - Vec2d orig2d0 = lastFijk.Coord.ToVec2d(); + lastFijk.Coord.ToVec2d(orig2d0); - int currentToLastDir = LookupTables.AdjacentFaceDirections[tmpFijk.Face, lastFijk.Face]; + var currentToLastDir = LookupTables.AdjacentFaceDirections[tmpFijk.Face, lastFijk.Face]; - FaceOrientIJK fijkOrient = LookupTables.OrientedFaceNeighbours[tmpFijk.Face, currentToLastDir]; + var fijkOrient = LookupTables.OrientedFaceNeighbours[tmpFijk.Face, currentToLastDir]; tmpFijk.Face = fijkOrient.Face; CoordIJK ijk = new(tmpFijk.Coord); // rotate and translate for adjacent face - for (int i = 0; i < fijkOrient.CounterClockwiseRotations; i += 1) ijk.RotateCounterClockwise(); + for (var i = 0; i < fijkOrient.CounterClockwiseRotations; i += 1) ijk.RotateCounterClockwise(); var scale = LookupTables.UnitScaleByClass2Res[adjustedResolution] * 3; ijk.I += fijkOrient.Translate.I * scale; @@ -250,32 +261,40 @@ public IEnumerable GetPentagonBoundary(int resolution, int start, int ijk.K += fijkOrient.Translate.K * scale; ijk.Normalize(); - Vec2d orig2d1 = ijk.ToVec2d(); + ijk.ToVec2d(orig2d1); // find the appropriate icosa face edge vertexes - int maxDim = LookupTables.MaxDistanceByClass2Res[adjustedResolution]; - Vec2d v0 = new(3.0 * maxDim, 0.0); - Vec2d v1 = new(-1.5 * maxDim, 3.0 * M_SQRT3_2 * maxDim); - Vec2d v2 = new(-1.5 * maxDim, -3.0 * M_SQRT3_2 * maxDim); + var maxDist = LookupTables.MaxDistanceByClass2Res[adjustedResolution]; + v0.X = 3 * maxDist; + v0.Y = 0; + v1.X = -1.5 * maxDist; + v1.Y = THREE_M_SQRT32 * maxDist; + v2.X = v1.X; + v2.Y = -THREE_M_SQRT32 * maxDist; + + Vec2d intersection; Vec2d edge0; Vec2d edge1; - int adjacentFace = LookupTables.AdjacentFaceDirections[tmpFijk.Face, fijk.Face]; + var adjacentFace = LookupTables.AdjacentFaceDirections[tmpFijk.Face, fijk.Face]; switch (adjacentFace) { case IJ: edge0 = v0; edge1 = v1; + intersection = v2; break; case JK: edge0 = v1; edge1 = v2; + intersection = v0; break; case KI: edge0 = v2; edge1 = v0; + intersection = v1; break; default: @@ -283,7 +302,7 @@ public IEnumerable GetPentagonBoundary(int resolution, int start, int } // find the intersection and add the lat/lon point to the result - Vec2d intersection = Vec2d.Intersect(orig2d0, orig2d1, edge0, edge1); + Vec2d.Intersect(orig2d0, orig2d1, edge0, edge1, intersection); yield return intersection.ToFaceGeoCoord(tmpFijk.Face, adjustedResolution, true); } @@ -291,7 +310,10 @@ public IEnumerable GetPentagonBoundary(int resolution, int start, int yield return fijk.ToFaceGeoCoord(adjustedResolution, true); } - lastFijk = fijk; + lastFijk.Face = fijk.Face; + lastFijk.Coord.I = fijk.Coord.I; + lastFijk.Coord.J = fijk.Coord.J; + lastFijk.Coord.K = fijk.Coord.K; } } @@ -304,20 +326,32 @@ public IEnumerable GetPentagonBoundary(int resolution, int start, int /// The number of topological vertexes to return /// The spherical coordinates of the cell boundary public IEnumerable GetHexagonBoundary(int resolution, int start, int length) { - int adjustedResolution = resolution; - FaceIJK centerIJK = new(this); - FaceIJK[] verts = centerIJK.GetHexVertices(ref adjustedResolution); + var adjustedResolution = resolution; + FaceIJK centerIjk = new(this); + var verts = centerIjk.GetHexVertices(ref adjustedResolution); + + var additionalIteration = length == NUM_HEX_VERTS ? 1 : 0; - int additionalIteration = length == NUM_HEX_VERTS ? 1 : 0; + var lastFace = -1; + var lastOverage = Overage.None; - int lastFace = -1; - Overage lastOverage = Overage.None; + Vec2d v0 = new(); + Vec2d v1 = new(); + Vec2d v2 = new(); + Vec2d orig2d0 = new(); + Vec2d orig2d1 = new(); - for (int vert = start; vert < start + length + additionalIteration; vert += 1) { - int v = vert % NUM_HEX_VERTS; + var fijk = new FaceIJK(); - FaceIJK fijk = new(verts[v]); - Overage overage = fijk.AdjustOverageClass2(adjustedResolution, false, true); + for (var vert = start; vert < start + length + additionalIteration; vert += 1) { + var v = vert % NUM_HEX_VERTS; + + fijk.Face = verts[v].Face; + fijk.Coord.I = verts[v].Coord.I; + fijk.Coord.J = verts[v].Coord.J; + fijk.Coord.K = verts[v].Coord.K; + + var overage = fijk.AdjustOverageClass2(adjustedResolution, false, true); /* Check for edge-crossing. Each face of the underlying icosahedron is a @@ -330,45 +364,52 @@ projection. Note that Class II cell edges have vertices on the face */ if (IsResolutionClass3(resolution) && vert > start && fijk.Face != lastFace && lastOverage != Overage.FaceEdge) { // find hex2d of the two vertexes on original face - int lastV = (v + 5) % NUM_HEX_VERTS; - Vec2d orig2d0 = verts[lastV].Coord.ToVec2d(); - Vec2d orig2d1 = verts[v].Coord.ToVec2d(); + var lastV = (v + 5) % NUM_HEX_VERTS; + verts[lastV].Coord.ToVec2d(orig2d0); + verts[v].Coord.ToVec2d(orig2d1); // find the appropriate icosa face edge vertexes - int maxDist = LookupTables.MaxDistanceByClass2Res[adjustedResolution]; - Vec2d v0 = new(3 * maxDist, 0); - Vec2d v1 = new(-1.5 * maxDist, 3.0 * M_SQRT3_2 * maxDist); - Vec2d v2 = new(-1.5 * maxDist, -3.0 * M_SQRT3_2 * maxDist); + var maxDist = LookupTables.MaxDistanceByClass2Res[adjustedResolution]; + v0.X = 3 * maxDist; + v0.Y = 0; + v1.X = -1.5 * maxDist; + v1.Y = THREE_M_SQRT32 * maxDist; + v2.X = v1.X; + v2.Y = -THREE_M_SQRT32 * maxDist; + var face2 = lastFace == centerIjk.Face ? fijk.Face : lastFace; + + Vec2d intersection; Vec2d edge0; Vec2d edge1; - int face2 = lastFace == centerIJK.Face ? fijk.Face : lastFace; - - switch (LookupTables.AdjacentFaceDirections[centerIJK.Face, face2]) { + switch (LookupTables.AdjacentFaceDirections[centerIjk.Face, face2]) { case IJ: edge0 = v0; edge1 = v1; + intersection = v2; break; case JK: edge0 = v1; edge1 = v2; + intersection = v0; break; case KI: edge0 = v2; edge1 = v0; + intersection = v1; break; default: throw new Exception("Unsupported direction"); } - Vec2d intersection = Vec2d.Intersect(orig2d0, orig2d1, edge0, edge1); - bool atVertex = orig2d0 == intersection || orig2d1 == intersection; + Vec2d.Intersect(orig2d0, orig2d1, edge0, edge1, intersection); + var atVertex = orig2d0 == intersection || orig2d1 == intersection; if (!atVertex) { - yield return intersection.ToFaceGeoCoord(centerIJK.Face, adjustedResolution, true); + yield return intersection.ToFaceGeoCoord(centerIjk.Face, adjustedResolution, true); } } diff --git a/src/H3/Model/GeoCoord.cs b/src/H3/Model/GeoCoord.cs index 62dca1b..121583c 100644 --- a/src/H3/Model/GeoCoord.cs +++ b/src/H3/Model/GeoCoord.cs @@ -79,11 +79,15 @@ public static GeoCoord ForAzimuthDistanceInRadians(GeoCoord p1, double azimuth, } } else { // not due north or south - double sinP1Lat = Math.Sin(p1.Latitude); - double cosP1Lat = Math.Cos(p1.Latitude); - double cosDist = Math.Cos(distance); - double sinDist = Math.Sin(distance); - double sinLat = Math.Clamp(sinP1Lat * cosDist + cosP1Lat * sinDist * Math.Cos(az), -1.0, 1.0); + var sinP1Lat = Math.Sin(p1.Latitude); + var cosP1Lat = Math.Cos(p1.Latitude); + var cosDist = Math.Cos(distance); + var sinDist = Math.Sin(distance); + #if NETSTANDARD2_0 + var sinLat = Clamp(sinP1Lat * cosDist + cosP1Lat * sinDist * Math.Cos(az), -1.0, 1.0); + #else + var sinLat = Math.Clamp(sinP1Lat * cosDist + cosP1Lat * sinDist * Math.Cos(az), -1.0, 1.0); + #endif p2.Latitude = Math.Asin(sinLat); if (Math.Abs(p2.Latitude - M_PI_2) < EPSILON) { @@ -95,9 +99,14 @@ public static GeoCoord ForAzimuthDistanceInRadians(GeoCoord p1, double azimuth, p2.Latitude = -M_PI_2; p2.Longitude = 0; } else { - double cosP2Lat = Math.Cos(p2.Latitude); - double sinLon = Math.Clamp(Math.Sin(az) * sinDist / cosP2Lat, -1.0, 1.0); - double cosLon = Math.Clamp((cosDist - sinP1Lat * Math.Sin(p2.Latitude)) / cosP1Lat / cosP2Lat, -1.0, 1.0); + var cosP2Lat = Math.Cos(p2.Latitude); + #if NETSTANDARD2_0 + var sinLon = Clamp(Math.Sin(az) * sinDist / cosP2Lat, -1.0, 1.0); + var cosLon = Clamp((cosDist - sinP1Lat * Math.Sin(p2.Latitude)) / cosP1Lat / cosP2Lat, -1.0, 1.0); + #else + var sinLon = Math.Clamp(Math.Sin(az) * sinDist / cosP2Lat, -1.0, 1.0); + var cosLon = Math.Clamp((cosDist - sinP1Lat * Math.Sin(p2.Latitude)) / cosP1Lat / cosP2Lat, -1.0, 1.0); + #endif p2.Longitude = ConstrainLongitude(p1.Longitude + Math.Atan2(sinLon, cosLon)); } } @@ -171,7 +180,7 @@ public double GetPointDistanceInRadians(GeoCoord p2) { public double GetPointDistanceInKm(GeoCoord p2) => GetPointDistanceInRadians(p2) * EARTH_RADIUS_KM; /// - /// The great circle disance in meters between two spherical coordiantes. + /// The great circle distance in meters between two spherical coordinates. /// /// Destination coordinate /// The great circle distance in meters between this coordinate @@ -187,10 +196,10 @@ public double GetPointDistanceInRadians(GeoCoord p2) { /// Estimated number of cells required to trace the line public int LineHexEstimate(GeoCoord other, int resolution) { // Get the area of the pentagon as the maximally-distorted area possible - H3Index firstPentagon = LookupTables.PentagonIndexesPerResolution[resolution][0]; - double pentagonRadiusKm = firstPentagon.GetRadiusInKm(); - double dist = GetPointDistanceInKm(other); - int estimate = (int)Math.Ceiling(dist / (2 * pentagonRadiusKm)); + var firstPentagon = LookupTables.PentagonIndexesPerResolution[resolution][0]; + var pentagonRadiusKm = firstPentagon.GetRadiusInKm(); + var dist = GetPointDistanceInKm(other); + var estimate = (int)Math.Ceiling(dist / (2 * pentagonRadiusKm)); return estimate == 0 ? 1 : estimate; } @@ -201,12 +210,12 @@ public bool AlmostEqualsThreshold(GeoCoord p2, double threshold) => public static implicit operator GeoCoord((double, double) c) => new(c.Item1, c.Item2); - public static bool operator ==(GeoCoord a, GeoCoord b) => a.Latitude == b.Latitude && a.Longitude == b.Longitude; + public static bool operator ==(GeoCoord a, GeoCoord b) => Math.Abs(a.Latitude - b.Latitude) < EPSILON_RAD && Math.Abs(a.Longitude - b.Longitude) < EPSILON_RAD; - public static bool operator !=(GeoCoord a, GeoCoord b) => a.Latitude != b.Latitude || a.Longitude != b.Longitude; + public static bool operator !=(GeoCoord a, GeoCoord b) => Math.Abs(a.Latitude - b.Latitude) >= EPSILON_RAD || Math.Abs(a.Longitude - b.Longitude) >= EPSILON_RAD; public override bool Equals(object? other) { - return other is GeoCoord c && Latitude == c.Latitude && Longitude == c.Longitude; + return other is GeoCoord c && Math.Abs(Latitude - c.Latitude) < EPSILON_RAD && Math.Abs(Longitude - c.Longitude) < EPSILON_RAD; } public override int GetHashCode() { diff --git a/src/H3/Model/LookupTables.cs b/src/H3/Model/LookupTables.cs index 0805d59..31feb6a 100644 --- a/src/H3/Model/LookupTables.cs +++ b/src/H3/Model/LookupTables.cs @@ -8,382 +8,11 @@ namespace H3.Model { public static class LookupTables { #region basecells - public static readonly BaseCell[] BaseCells = { - (0, (1, (1, 0, 0)), 0, (0, 0)), // base cell 0 - (1, (2, (1, 1, 0)), 0, (0, 0)), // base cell 1 - (2, (1, (0, 0, 0)), 0, (0, 0)), // base cell 2 - (3, (2, (1, 0, 0)), 0, (0, 0)), // base cell 3 - (4, (0, (2, 0, 0)), 1, (-1, -1)), // base cell 4 - (5, (1, (1, 1, 0)), 0, (0, 0)), // base cell 5 - (6, (1, (0, 0, 1)), 0, (0, 0)), // base cell 6 - (7, (2, (0, 0, 0)), 0, (0, 0)), // base cell 7 - (8, (0, (1, 0, 0)), 0, (0, 0)), // base cell 8 - (9, (2, (0, 1, 0)), 0, (0, 0)), // base cell 9 - (10, (1, (0, 1, 0)), 0, (0, 0)), // base cell 10 - (11, (1, (0, 1, 1)), 0, (0, 0)), // base cell 11 - (12, (3, (1, 0, 0)), 0, (0, 0)), // base cell 12 - (13, (3, (1, 1, 0)), 0, (0, 0)), // base cell 13 - (14, (11, (2, 0, 0)), 1, (2, 6)), // base cell 14 - (15, (4, (1, 0, 0)), 0, (0, 0)), // base cell 15 - (16, (0, (0, 0, 0)), 0, (0, 0)), // base cell 16 - (17, (6, (0, 1, 0)), 0, (0, 0)), // base cell 17 - (18, (0, (0, 0, 1)), 0, (0, 0)), // base cell 18 - (19, (2, (0, 1, 1)), 0, (0, 0)), // base cell 19 - (20, (7, (0, 0, 1)), 0, (0, 0)), // base cell 20 - (21, (2, (0, 0, 1)), 0, (0, 0)), // base cell 21 - (22, (0, (1, 1, 0)), 0, (0, 0)), // base cell 22 - (23, (6, (0, 0, 1)), 0, (0, 0)), // base cell 23 - (24, (10, (2, 0, 0)), 1, (1, 5)), // base cell 24 - (25, (6, (0, 0, 0)), 0, (0, 0)), // base cell 25 - (26, (3, (0, 0, 0)), 0, (0, 0)), // base cell 26 - (27, (11, (1, 0, 0)), 0, (0, 0)), // base cell 27 - (28, (4, (1, 1, 0)), 0, (0, 0)), // base cell 28 - (29, (3, (0, 1, 0)), 0, (0, 0)), // base cell 29 - (30, (0, (0, 1, 1)), 0, (0, 0)), // base cell 30 - (31, (4, (0, 0, 0)), 0, (0, 0)), // base cell 31 - (32, (5, (0, 1, 0)), 0, (0, 0)), // base cell 32 - (33, (0, (0, 1, 0)), 0, (0, 0)), // base cell 33 - (34, (7, (0, 1, 0)), 0, (0, 0)), // base cell 34 - (35, (11, (1, 1, 0)), 0, (0, 0)), // base cell 35 - (36, (7, (0, 0, 0)), 0, (0, 0)), // base cell 36 - (37, (10, (1, 0, 0)), 0, (0, 0)), // base cell 37 - (38, (12, (2, 0, 0)), 1, (3, 7)), // base cell 38 - (39, (6, (1, 0, 1)), 0, (0, 0)), // base cell 39 - (40, (7, (1, 0, 1)), 0, (0, 0)), // base cell 40 - (41, (4, (0, 0, 1)), 0, (0, 0)), // base cell 41 - (42, (3, (0, 0, 1)), 0, (0, 0)), // base cell 42 - (43, (3, (0, 1, 1)), 0, (0, 0)), // base cell 43 - (44, (4, (0, 1, 0)), 0, (0, 0)), // base cell 44 - (45, (6, (1, 0, 0)), 0, (0, 0)), // base cell 45 - (46, (11, (0, 0, 0)), 0, (0, 0)), // base cell 46 - (47, (8, (0, 0, 1)), 0, (0, 0)), // base cell 47 - (48, (5, (0, 0, 1)), 0, (0, 0)), // base cell 48 - (49, (14, (2, 0, 0)), 1, (0, 9)), // base cell 49 - (50, (5, (0, 0, 0)), 0, (0, 0)), // base cell 50 - (51, (12, (1, 0, 0)), 0, (0, 0)), // base cell 51 - (52, (10, (1, 1, 0)), 0, (0, 0)), // base cell 52 - (53, (4, (0, 1, 1)), 0, (0, 0)), // base cell 53 - (54, (12, (1, 1, 0)), 0, (0, 0)), // base cell 54 - (55, (7, (1, 0, 0)), 0, (0, 0)), // base cell 55 - (56, (11, (0, 1, 0)), 0, (0, 0)), // base cell 56 - (57, (10, (0, 0, 0)), 0, (0, 0)), // base cell 57 - (58, (13, (2, 0, 0)), 1, (4, 8)), // base cell 58 - (59, (10, (0, 0, 1)), 0, (0, 0)), // base cell 59 - (60, (11, (0, 0, 1)), 0, (0, 0)), // base cell 60 - (61, (9, (0, 1, 0)), 0, (0, 0)), // base cell 61 - (62, (8, (0, 1, 0)), 0, (0, 0)), // base cell 62 - (63, (6, (2, 0, 0)), 1, (11, 15)), // base cell 63 - (64, (8, (0, 0, 0)), 0, (0, 0)), // base cell 64 - (65, (9, (0, 0, 1)), 0, (0, 0)), // base cell 65 - (66, (14, (1, 0, 0)), 0, (0, 0)), // base cell 66 - (67, (5, (1, 0, 1)), 0, (0, 0)), // base cell 67 - (68, (16, (0, 1, 1)), 0, (0, 0)), // base cell 68 - (69, (8, (1, 0, 1)), 0, (0, 0)), // base cell 69 - (70, (5, (1, 0, 0)), 0, (0, 0)), // base cell 70 - (71, (12, (0, 0, 0)), 0, (0, 0)), // base cell 71 - (72, (7, (2, 0, 0)), 1, (12, 16)), // base cell 72 - (73, (12, (0, 1, 0)), 0, (0, 0)), // base cell 73 - (74, (10, (0, 1, 0)), 0, (0, 0)), // base cell 74 - (75, (9, (0, 0, 0)), 0, (0, 0)), // base cell 75 - (76, (13, (1, 0, 0)), 0, (0, 0)), // base cell 76 - (77, (16, (0, 0, 1)), 0, (0, 0)), // base cell 77 - (78, (15, (0, 1, 1)), 0, (0, 0)), // base cell 78 - (79, (15, (0, 1, 0)), 0, (0, 0)), // base cell 79 - (80, (16, (0, 1, 0)), 0, (0, 0)), // base cell 80 - (81, (14, (1, 1, 0)), 0, (0, 0)), // base cell 81 - (82, (13, (1, 1, 0)), 0, (0, 0)), // base cell 82 - (83, (5, (2, 0, 0)), 1, (10, 19)), // base cell 83 - (84, (8, (1, 0, 0)), 0, (0, 0)), // base cell 84 - (85, (14, (0, 0, 0)), 0, (0, 0)), // base cell 85 - (86, (9, (1, 0, 1)), 0, (0, 0)), // base cell 86 - (87, (14, (0, 0, 1)), 0, (0, 0)), // base cell 87 - (88, (17, (0, 0, 1)), 0, (0, 0)), // base cell 88 - (89, (12, (0, 0, 1)), 0, (0, 0)), // base cell 89 - (90, (16, (0, 0, 0)), 0, (0, 0)), // base cell 90 - (91, (17, (0, 1, 1)), 0, (0, 0)), // base cell 91 - (92, (15, (0, 0, 1)), 0, (0, 0)), // base cell 92 - (93, (16, (1, 0, 1)), 0, (0, 0)), // base cell 93 - (94, (9, (1, 0, 0)), 0, (0, 0)), // base cell 94 - (95, (15, (0, 0, 0)), 0, (0, 0)), // base cell 95 - (96, (13, (0, 0, 0)), 0, (0, 0)), // base cell 96 - (97, (8, (2, 0, 0)), 1, (13, 17)), // base cell 97 - (98, (13, (0, 1, 0)), 0, (0, 0)), // base cell 98 - (99, (17, (1, 0, 1)), 0, (0, 0)), // base cell 99 - (100, (19, (0, 1, 0)), 0, (0, 0)), // base cell 100 - (101, (14, (0, 1, 0)), 0, (0, 0)), // base cell 101 - (102, (19, (0, 1, 1)), 0, (0, 0)), // base cell 102 - (103, (17, (0, 1, 0)), 0, (0, 0)), // base cell 103 - (104, (13, (0, 0, 1)), 0, (0, 0)), // base cell 104 - (105, (17, (0, 0, 0)), 0, (0, 0)), // base cell 105 - (106, (16, (1, 0, 0)), 0, (0, 0)), // base cell 106 - (107, (9, (2, 0, 0)), 1, (14, 18)), // base cell 107 - (108, (15, (1, 0, 1)), 0, (0, 0)), // base cell 108 - (109, (15, (1, 0, 0)), 0, (0, 0)), // base cell 109 - (110, (18, (0, 1, 1)), 0, (0, 0)), // base cell 110 - (111, (18, (0, 0, 1)), 0, (0, 0)), // base cell 111 - (112, (19, (0, 0, 1)), 0, (0, 0)), // base cell 112 - (113, (17, (1, 0, 0)), 0, (0, 0)), // base cell 113 - (114, (19, (0, 0, 0)), 0, (0, 0)), // base cell 114 - (115, (18, (0, 1, 0)), 0, (0, 0)), // base cell 115 - (116, (18, (1, 0, 1)), 0, (0, 0)), // base cell 116 - (117, (19, (2, 0, 0)), 1, (-1, -1)), // base cell 117 - (118, (19, (1, 0, 0)), 0, (0, 0)), // base cell 118 - (119, (18, (0, 0, 0)), 0, (0, 0)), // base cell 119 - (120, (19, (1, 0, 1)), 0, (0, 0)), // base cell 120 - (121, (18, (1, 0, 0)), 0, (0, 0)) // base cell 121 - }; - - public static readonly int[,] NeighbourCounterClockwiseRotations = { - { 0, 5, 0, 0, 1, 5, 1}, // base cell 0 - { 0, 0, 1, 0, 1, 0, 1}, // base cell 1 - { 0, 0, 0, 0, 0, 5, 0}, // base cell 2 - { 0, 5, 0, 0, 2, 5, 1}, // base cell 3 - { 0, -1, 1, 0, 3, 4, 2}, // base cell 4 (pentagon) - { 0, 0, 1, 0, 1, 0, 1}, // base cell 5 - { 0, 0, 0, 3, 5, 5, 0}, // base cell 6 - { 0, 0, 0, 0, 0, 5, 0}, // base cell 7 - { 0, 5, 0, 0, 0, 5, 1}, // base cell 8 - { 0, 0, 1, 3, 0, 0, 1}, // base cell 9 - { 0, 0, 1, 3, 0, 0, 1}, // base cell 10 - { 0, 3, 3, 3, 0, 0, 0}, // base cell 11 - { 0, 5, 0, 0, 3, 5, 1}, // base cell 12 - { 0, 0, 1, 0, 1, 0, 1}, // base cell 13 - { 0, -1, 3, 0, 5, 2, 0}, // base cell 14 (pentagon) - { 0, 5, 0, 0, 4, 5, 1}, // base cell 15 - { 0, 0, 0, 0, 0, 5, 0}, // base cell 16 - { 0, 3, 3, 3, 3, 0, 3}, // base cell 17 - { 0, 0, 0, 3, 5, 5, 0}, // base cell 18 - { 0, 3, 3, 3, 0, 0, 0}, // base cell 19 - { 0, 3, 3, 3, 0, 3, 0}, // base cell 20 - { 0, 0, 0, 3, 5, 5, 0}, // base cell 21 - { 0, 0, 1, 0, 1, 0, 1}, // base cell 22 - { 0, 3, 3, 3, 0, 3, 0}, // base cell 23 - { 0, -1, 3, 0, 5, 2, 0}, // base cell 24 (pentagon) - { 0, 0, 0, 3, 0, 0, 3}, // base cell 25 - { 0, 0, 0, 0, 0, 5, 0}, // base cell 26 - { 0, 3, 0, 0, 0, 3, 3}, // base cell 27 - { 0, 0, 1, 0, 1, 0, 1}, // base cell 28 - { 0, 0, 1, 3, 0, 0, 1}, // base cell 29 - { 0, 3, 3, 3, 0, 0, 0}, // base cell 30 - { 0, 0, 0, 0, 0, 5, 0}, // base cell 31 - { 0, 3, 3, 3, 3, 0, 3}, // base cell 32 - { 0, 0, 1, 3, 0, 0, 1}, // base cell 33 - { 0, 3, 3, 3, 3, 0, 3}, // base cell 34 - { 0, 0, 3, 0, 3, 0, 3}, // base cell 35 - { 0, 0, 0, 3, 0, 0, 3}, // base cell 36 - { 0, 3, 0, 0, 0, 3, 3}, // base cell 37 - { 0, -1, 3, 0, 5, 2, 0}, // base cell 38 (pentagon) - { 0, 3, 0, 0, 3, 3, 0}, // base cell 39 - { 0, 3, 0, 0, 3, 3, 0}, // base cell 40 - { 0, 0, 0, 3, 5, 5, 0}, // base cell 41 - { 0, 0, 0, 3, 5, 5, 0}, // base cell 42 - { 0, 3, 3, 3, 0, 0, 0}, // base cell 43 - { 0, 0, 1, 3, 0, 0, 1}, // base cell 44 - { 0, 0, 3, 0, 0, 3, 3}, // base cell 45 - { 0, 0, 0, 3, 0, 3, 0}, // base cell 46 - { 0, 3, 3, 3, 0, 3, 0}, // base cell 47 - { 0, 3, 3, 3, 0, 3, 0}, // base cell 48 - { 0, -1, 3, 0, 5, 2, 0}, // base cell 49 (pentagon) - { 0, 0, 0, 3, 0, 0, 3}, // base cell 50 - { 0, 3, 0, 0, 0, 3, 3}, // base cell 51 - { 0, 0, 3, 0, 3, 0, 3}, // base cell 52 - { 0, 3, 3, 3, 0, 0, 0}, // base cell 53 - { 0, 0, 3, 0, 3, 0, 3}, // base cell 54 - { 0, 0, 3, 0, 0, 3, 3}, // base cell 55 - { 0, 3, 3, 3, 0, 0, 3}, // base cell 56 - { 0, 0, 0, 3, 0, 3, 0}, // base cell 57 - { 0, -1, 3, 0, 5, 2, 0}, // base cell 58 (pentagon) - { 0, 3, 3, 3, 3, 3, 0}, // base cell 59 - { 0, 3, 3, 3, 3, 3, 0}, // base cell 60 - { 0, 3, 3, 3, 3, 0, 3}, // base cell 61 - { 0, 3, 3, 3, 3, 0, 3}, // base cell 62 - { 0, -1, 3, 0, 5, 2, 0}, // base cell 63 (pentagon) - { 0, 0, 0, 3, 0, 0, 3}, // base cell 64 - { 0, 3, 3, 3, 0, 3, 0}, // base cell 65 - { 0, 3, 0, 0, 0, 3, 3}, // base cell 66 - { 0, 3, 0, 0, 3, 3, 0}, // base cell 67 - { 0, 3, 3, 3, 0, 0, 0}, // base cell 68 - { 0, 3, 0, 0, 3, 3, 0}, // base cell 69 - { 0, 0, 3, 0, 0, 3, 3}, // base cell 70 - { 0, 0, 0, 3, 0, 3, 0}, // base cell 71 - { 0, -1, 3, 0, 5, 2, 0}, // base cell 72 (pentagon) - { 0, 3, 3, 3, 0, 0, 3}, // base cell 73 - { 0, 3, 3, 3, 0, 0, 3}, // base cell 74 - { 0, 0, 0, 3, 0, 0, 3}, // base cell 75 - { 0, 3, 0, 0, 0, 3, 3}, // base cell 76 - { 0, 0, 0, 3, 0, 5, 0}, // base cell 77 - { 0, 3, 3, 3, 0, 0, 0}, // base cell 78 - { 0, 0, 1, 3, 1, 0, 1}, // base cell 79 - { 0, 0, 1, 3, 1, 0, 1}, // base cell 80 - { 0, 0, 3, 0, 3, 0, 3}, // base cell 81 - { 0, 0, 3, 0, 3, 0, 3}, // base cell 82 - { 0, -1, 3, 0, 5, 2, 0}, // base cell 83 (pentagon) - { 0, 0, 3, 0, 0, 3, 3}, // base cell 84 - { 0, 0, 0, 3, 0, 3, 0}, // base cell 85 - { 0, 3, 0, 0, 3, 3, 0}, // base cell 86 - { 0, 3, 3, 3, 3, 3, 0}, // base cell 87 - { 0, 0, 0, 3, 0, 5, 0}, // base cell 88 - { 0, 3, 3, 3, 3, 3, 0}, // base cell 89 - { 0, 0, 0, 0, 0, 0, 1}, // base cell 90 - { 0, 3, 3, 3, 0, 0, 0}, // base cell 91 - { 0, 0, 0, 3, 0, 5, 0}, // base cell 92 - { 0, 5, 0, 0, 5, 5, 0}, // base cell 93 - { 0, 0, 3, 0, 0, 3, 3}, // base cell 94 - { 0, 0, 0, 0, 0, 0, 1}, // base cell 95 - { 0, 0, 0, 3, 0, 3, 0}, // base cell 96 - { 0, -1, 3, 0, 5, 2, 0}, // base cell 97 (pentagon) - { 0, 3, 3, 3, 0, 0, 3}, // base cell 98 - { 0, 5, 0, 0, 5, 5, 0}, // base cell 99 - { 0, 0, 1, 3, 1, 0, 1}, // base cell 100 - { 0, 3, 3, 3, 0, 0, 3}, // base cell 101 - { 0, 3, 3, 3, 0, 0, 0}, // base cell 102 - { 0, 0, 1, 3, 1, 0, 1}, // base cell 103 - { 0, 3, 3, 3, 3, 3, 0}, // base cell 104 - { 0, 0, 0, 0, 0, 0, 1}, // base cell 105 - { 0, 0, 1, 0, 3, 5, 1}, // base cell 106 - { 0, -1, 3, 0, 5, 2, 0}, // base cell 107 (pentagon) - { 0, 5, 0, 0, 5, 5, 0}, // base cell 108 - { 0, 0, 1, 0, 4, 5, 1}, // base cell 109 - { 0, 3, 3, 3, 0, 0, 0}, // base cell 110 - { 0, 0, 0, 3, 0, 5, 0}, // base cell 111 - { 0, 0, 0, 3, 0, 5, 0}, // base cell 112 - { 0, 0, 1, 0, 2, 5, 1}, // base cell 113 - { 0, 0, 0, 0, 0, 0, 1}, // base cell 114 - { 0, 0, 1, 3, 1, 0, 1}, // base cell 115 - { 0, 5, 0, 0, 5, 5, 0}, // base cell 116 - { 0, -1, 1, 0, 3, 4, 2}, // base cell 117 (pentagon) - { 0, 0, 1, 0, 0, 5, 1}, // base cell 118 - { 0, 0, 0, 0, 0, 0, 1}, // base cell 119 - { 0, 5, 0, 0, 5, 5, 0}, // base cell 120 - { 0, 0, 1, 0, 1, 5, 1}, // base cell 121 - }; public const int INVALID_BASE_CELL = 127; - public static readonly int[,] Neighbours = { - {0, 1, 5, 2, 4, 3, 8}, // base cell 0 - {1, 7, 6, 9, 0, 3, 2}, // base cell 1 - {2, 6, 10, 11, 0, 1, 5}, // base cell 2 - {3, 13, 1, 7, 4, 12, 0}, // base cell 3 - {4, INVALID_BASE_CELL, 15, 8, 3, 0, 12}, // base cell 4 (pentagon) - {5, 2, 18, 10, 8, 0, 16}, // base cell 5 - {6, 14, 11, 17, 1, 9, 2}, // base cell 6 - {7, 21, 9, 19, 3, 13, 1}, // base cell 7 - {8, 5, 22, 16, 4, 0, 15}, // base cell 8 - {9, 19, 14, 20, 1, 7, 6}, // base cell 9 - {10, 11, 24, 23, 5, 2, 18}, // base cell 10 - {11, 17, 23, 25, 2, 6, 10}, // base cell 11 - {12, 28, 13, 26, 4, 15, 3}, // base cell 12 - {13, 26, 21, 29, 3, 12, 7}, // base cell 13 - {14, INVALID_BASE_CELL, 17, 27, 9, 20, 6}, // base cell 14 (pentagon) - {15, 22, 28, 31, 4, 8, 12}, // base cell 15 - {16, 18, 33, 30, 8, 5, 22}, // base cell 16 - {17, 11, 14, 6, 35, 25, 27}, // base cell 17 - {18, 24, 30, 32, 5, 10, 16}, // base cell 18 - {19, 34, 20, 36, 7, 21, 9}, // base cell 19 - {20, 14, 19, 9, 40, 27, 36}, // base cell 20 - {21, 38, 19, 34, 13, 29, 7}, // base cell 21 - {22, 16, 41, 33, 15, 8, 31}, // base cell 22 - {23, 24, 11, 10, 39, 37, 25}, // base cell 23 - {24, INVALID_BASE_CELL, 32, 37, 10, 23, 18}, // base cell 24 (pentagon) - {25, 23, 17, 11, 45, 39, 35}, // base cell 25 - {26, 42, 29, 43, 12, 28, 13}, // base cell 26 - {27, 40, 35, 46, 14, 20, 17}, // base cell 27 - {28, 31, 42, 44, 12, 15, 26}, // base cell 28 - {29, 43, 38, 47, 13, 26, 21}, // base cell 29 - {30, 32, 48, 50, 16, 18, 33}, // base cell 30 - {31, 41, 44, 53, 15, 22, 28}, // base cell 31 - {32, 30, 24, 18, 52, 50, 37}, // base cell 32 - {33, 30, 49, 48, 22, 16, 41}, // base cell 33 - {34, 19, 38, 21, 54, 36, 51}, // base cell 34 - {35, 46, 45, 56, 17, 27, 25}, // base cell 35 - {36, 20, 34, 19, 55, 40, 54}, // base cell 36 - {37, 39, 52, 57, 24, 23, 32}, // base cell 37 - {38, INVALID_BASE_CELL, 34, 51, 29, 47, 21}, // base cell 38 (pentagon) - {39, 37, 25, 23, 59, 57, 45}, // base cell 39 - {40, 27, 36, 20, 60, 46, 55}, // base cell 40 - {41, 49, 53, 61, 22, 33, 31}, // base cell 41 - {42, 58, 43, 62, 28, 44, 26}, // base cell 42 - {43, 62, 47, 64, 26, 42, 29}, // base cell 43 - {44, 53, 58, 65, 28, 31, 42}, // base cell 44 - {45, 39, 35, 25, 63, 59, 56}, // base cell 45 - {46, 60, 56, 68, 27, 40, 35}, // base cell 46 - {47, 38, 43, 29, 69, 51, 64}, // base cell 47 - {48, 49, 30, 33, 67, 66, 50}, // base cell 48 - {49, INVALID_BASE_CELL, 61, 66, 33, 48, 41}, // base cell 49 (pentagon) - {50, 48, 32, 30, 70, 67, 52}, // base cell 50 - {51, 69, 54, 71, 38, 47, 34}, // base cell 51 - {52, 57, 70, 74, 32, 37, 50}, // base cell 52 - {53, 61, 65, 75, 31, 41, 44}, // base cell 53 - {54, 71, 55, 73, 34, 51, 36}, // base cell 54 - {55, 40, 54, 36, 72, 60, 73}, // base cell 55 - {56, 68, 63, 77, 35, 46, 45}, // base cell 56 - {57, 59, 74, 78, 37, 39, 52}, // base cell 57 - {58, INVALID_BASE_CELL, 62, 76, 44, 65, 42}, // base cell 58 (pentagon) - {59, 63, 78, 79, 39, 45, 57}, // base cell 59 - {60, 72, 68, 80, 40, 55, 46}, // base cell 60 - {61, 53, 49, 41, 81, 75, 66}, // base cell 61 - {62, 43, 58, 42, 82, 64, 76}, // base cell 62 - {63, INVALID_BASE_CELL, 56, 45, 79, 59, 77}, // base cell 63 (pentagon) - {64, 47, 62, 43, 84, 69, 82}, // base cell 64 - {65, 58, 53, 44, 86, 76, 75}, // base cell 65 - {66, 67, 81, 85, 49, 48, 61}, // base cell 66 - {67, 66, 50, 48, 87, 85, 70}, // base cell 67 - {68, 56, 60, 46, 90, 77, 80}, // base cell 68 - {69, 51, 64, 47, 89, 71, 84}, // base cell 69 - {70, 67, 52, 50, 83, 87, 74}, // base cell 70 - {71, 89, 73, 91, 51, 69, 54}, // base cell 71 - {72, INVALID_BASE_CELL, 73, 55, 80, 60, 88}, // base cell 72 (pentagon) - {73, 91, 72, 88, 54, 71, 55}, // base cell 73 - {74, 78, 83, 92, 52, 57, 70}, // base cell 74 - {75, 65, 61, 53, 94, 86, 81}, // base cell 75 - {76, 86, 82, 96, 58, 65, 62}, // base cell 76 - {77, 63, 68, 56, 93, 79, 90}, // base cell 77 - {78, 74, 59, 57, 95, 92, 79}, // base cell 78 - {79, 78, 63, 59, 93, 95, 77}, // base cell 79 - {80, 68, 72, 60, 99, 90, 88}, // base cell 80 - {81, 85, 94, 101, 61, 66, 75}, // base cell 81 - {82, 96, 84, 98, 62, 76, 64}, // base cell 82 - {83, INVALID_BASE_CELL, 74, 70, 100, 87, 92}, // base cell 83 (pentagon) - {84, 69, 82, 64, 97, 89, 98}, // base cell 84 - {85, 87, 101, 102, 66, 67, 81}, // base cell 85 - {86, 76, 75, 65, 104, 96, 94}, // base cell 86 - {87, 83, 102, 100, 67, 70, 85}, // base cell 87 - {88, 72, 91, 73, 99, 80, 105}, // base cell 88 - {89, 97, 91, 103, 69, 84, 71}, // base cell 89 - {90, 77, 80, 68, 106, 93, 99}, // base cell 90 - {91, 73, 89, 71, 105, 88, 103}, // base cell 91 - {92, 83, 78, 74, 108, 100, 95}, // base cell 92 - {93, 79, 90, 77, 109, 95, 106}, // base cell 93 - {94, 86, 81, 75, 107, 104, 101}, // base cell 94 - {95, 92, 79, 78, 109, 108, 93}, // base cell 95 - {96, 104, 98, 110, 76, 86, 82}, // base cell 96 - {97, INVALID_BASE_CELL, 98, 84, 103, 89, 111}, // base cell 97 (pentagon) - {98, 110, 97, 111, 82, 96, 84}, // base cell 98 - {99, 80, 105, 88, 106, 90, 113}, // base cell 99 - {100, 102, 83, 87, 108, 114, 92}, // base cell 100 - {101, 102, 107, 112, 81, 85, 94}, // base cell 101 - {102, 101, 87, 85, 114, 112, 100}, // base cell 102 - {103, 91, 97, 89, 116, 105, 111}, // base cell 103 - {104, 107, 110, 115, 86, 94, 96}, // base cell 104 - {105, 88, 103, 91, 113, 99, 116}, // base cell 105 - {106, 93, 99, 90, 117, 109, 113}, // base cell 106 - {107, INVALID_BASE_CELL, 101, 94, 115, 104, 112}, // base cell 107 (pentagon) - {108, 100, 95, 92, 118, 114, 109}, // base cell 108 - {109, 108, 93, 95, 117, 118, 106}, // base cell 109 - {110, 98, 104, 96, 119, 111, 115}, // base cell 110 - {111, 97, 110, 98, 116, 103, 119}, // base cell 111 - {112, 107, 102, 101, 120, 115, 114}, // base cell 112 - {113, 99, 116, 105, 117, 106, 121}, // base cell 113 - {114, 112, 100, 102, 118, 120, 108}, // base cell 114 - {115, 110, 107, 104, 120, 119, 112}, // base cell 115 - {116, 103, 119, 111, 113, 105, 121}, // base cell 116 - {117, INVALID_BASE_CELL, 109, 118, 113, 121, 106}, // base cell 117 (pentagon) - {118, 120, 108, 114, 117, 121, 109}, // base cell 118 - {119, 111, 115, 110, 121, 116, 120}, // base cell 119 - {120, 115, 114, 112, 121, 119, 118}, // base cell 120 - {121, 116, 120, 119, 117, 113, 118}, // base cell 121 - }; + // TODO build BaseFace or something; anyway, it should have rotations etc + // TODO link basecell to its BaseFace /// /// Resolution 0 base cell lookup table for each face. @@ -890,7 +519,7 @@ public static class LookupTables { }; public static readonly Dictionary DirectionToUnitVector = - Enum.GetValues().ToDictionary(e => e, e => e switch { + Enum.GetValues(typeof(Direction)).Cast().ToDictionary(e => e, e => e switch { Direction.Invalid => CoordIJK.InvalidIJKCoordinate, _ => UnitVectors[(int)e] }); @@ -1487,7 +1116,7 @@ public static class LookupTables { public static readonly Dictionary PentagonIndexesPerResolution = Enumerable.Range(0, MAX_H3_RES + 1) .ToDictionary(resolution => resolution, resolution => Enumerable.Range(0, NUM_BASE_CELLS - 1) - .Where(baseCellNumber => BaseCells[baseCellNumber].IsPentagon) + .Where(baseCellNumber => BaseCells.Cells[baseCellNumber].IsPentagon) .Select(baseCellNumber => H3Index.Create(resolution, baseCellNumber, Direction.Center)).ToArray() ); diff --git a/src/H3/Model/Vec2d.cs b/src/H3/Model/Vec2d.cs index 8df026b..afc1aeb 100644 --- a/src/H3/Model/Vec2d.cs +++ b/src/H3/Model/Vec2d.cs @@ -1,4 +1,6 @@ using System; +using System.Runtime.CompilerServices; +using static H3.Constants; #nullable enable @@ -8,6 +10,9 @@ public sealed class Vec2d { public double X { get; set; } public double Y { get; set; } + /// + /// Returns the magnitude of the vector... ohhh yessss! Un-pre-dictable! + /// public double Magitude => Math.Sqrt(X * X + Y * Y); public Vec2d() { } @@ -27,11 +32,14 @@ public Vec2d((double, double) components) { Y = components.Item2; } - public static Vec2d Intersect(Vec2d p0, Vec2d p1, Vec2d p2, Vec2d p3) { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vec2d Intersect(Vec2d p0, Vec2d p1, Vec2d p2, Vec2d p3, Vec2d? toUpdate = null) { Vec2d s1 = new(p1.X - p0.X, p1.Y - p0.Y); - Vec2d s2 = new(p3.X - p2.X, p3.Y - p2.Y); - float t = (float)(s2.X * (p0.Y - p2.Y) - s2.Y * (p0.X - p2.X)) / - (float)(-s2.X * s1.Y + s1.X * s2.Y); + var s2 = toUpdate ?? new Vec2d(); + s2.X = p3.X - p2.X; + s2.Y = p3.Y - p2.Y; + var t = (float)(s2.X * (p0.Y - p2.Y) - s2.Y * (p0.X - p2.X)) / + (float)(-s2.X * s1.Y + s1.X * s2.Y); s2.X = p0.X + t * s1.X; s2.Y = p0.Y + t * s1.Y; @@ -41,11 +49,11 @@ public static Vec2d Intersect(Vec2d p0, Vec2d p1, Vec2d p2, Vec2d p3) { public GeoCoord ToFaceGeoCoord(int face, int resolution, bool isSubstrate) => FaceIJK.ToFaceGeoCoord(X, Y, face, resolution, isSubstrate); - public static bool operator ==(Vec2d a, Vec2d b) => a.X == b.X && a.Y == b.Y; + public static bool operator ==(Vec2d a, Vec2d b) => Math.Abs(a.X - b.X) < EPSILON && Math.Abs(a.Y - b.Y) < EPSILON; - public static bool operator !=(Vec2d a, Vec2d b) => a.X != b.X || a.Y != b.Y; + public static bool operator !=(Vec2d a, Vec2d b) => Math.Abs(a.X - b.X) > EPSILON || Math.Abs(a.Y - b.Y) > EPSILON; - public override bool Equals(object? other) => other is Vec2d v && X == v.X && Y == v.Y; + public override bool Equals(object? other) => other is Vec2d v && this == v; public override int GetHashCode() => HashCode.Combine(X, Y); } diff --git a/src/H3/Model/Vertex.cs b/src/H3/Model/Vertex.cs index 85b770f..47f98a0 100644 --- a/src/H3/Model/Vertex.cs +++ b/src/H3/Model/Vertex.cs @@ -9,10 +9,10 @@ public class PentagonDirectionToFaceMapping { public int BaseCellNumber { get; init; } public int[] Faces { get; init; } = new int[NUM_PENT_VERTS]; - public BaseCell BaseCell => LookupTables.BaseCells[BaseCellNumber]; + public BaseCell BaseCell => BaseCells.Cells[BaseCellNumber]; public static PentagonDirectionToFaceMapping ForBaseCell(int baseCellNumber) => - LookupTables.PentagonDirectionFaces.Where(df => df.BaseCellNumber == baseCellNumber).First(); + LookupTables.PentagonDirectionFaces.First(df => df.BaseCellNumber == baseCellNumber); public static implicit operator PentagonDirectionToFaceMapping((int, (int, int, int, int, int)) data) { return new PentagonDirectionToFaceMapping { diff --git a/src/H3/Utils.cs b/src/H3/Utils.cs index 5e41521..83e4d27 100644 --- a/src/H3/Utils.cs +++ b/src/H3/Utils.cs @@ -4,7 +4,9 @@ using static H3.Constants; namespace H3 { + public static class Utils { + public static readonly GeometryFactory DefaultGeometryFactory = new(new PrecisionModel(1 / EPSILON), 4326); @@ -33,7 +35,7 @@ public static class Utils { /// azimuth, ...in radians! [MethodImpl(MethodImplOptions.AggressiveInlining)] public static double AzimuthInRadians(double p1Lon, double p1Lat, double p2Lon, double p2Lat) { - double cosP2Lat = Math.Cos(p2Lat); + var cosP2Lat = Math.Cos(p2Lat); return Math.Atan2( cosP2Lat * Math.Sin(p2Lon - p1Lon), Math.Cos(p1Lat) * Math.Sin(p2Lat) - @@ -54,15 +56,15 @@ public static double AzimuthInRadians(double p1Lon, double p1Lat, double p2Lon, /// and the destination coordinate. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static double PointDistanceInRadians(double p1Lon, double p1Lat, double p2Lon, double p2Lat) { - double sinLat = Math.Sin((p2Lat - p1Lat) / 2.0); - double sinLon = Math.Sin((p2Lon - p1Lon) / 2.0); - double a = sinLat * sinLat + Math.Cos(p1Lat) * Math.Cos(p2Lat) * sinLon * sinLon; + var sinLat = Math.Sin((p2Lat - p1Lat) / 2.0); + var sinLon = Math.Sin((p2Lon - p1Lon) / 2.0); + var a = sinLat * sinLat + Math.Cos(p1Lat) * Math.Cos(p2Lat) * sinLon * sinLon; return 2 * Math.Atan2(Math.Sqrt(a), Math.Sqrt(1 - a)); } [MethodImpl(MethodImplOptions.AggressiveInlining)] public static double NormalizeAngle(double radians) { - double tmp = radians < 0 ? radians + M_2PI : radians; + var tmp = radians < 0 ? radians + M_2PI : radians; if (radians >= M_2PI) tmp -= M_2PI; return tmp; } @@ -76,7 +78,7 @@ public static double ConstrainLongitude(double longitude) { [MethodImpl(MethodImplOptions.AggressiveInlining)] public static double TriangleEdgeLengthsToArea(double a, double b, double c) { - double s = (a + b + c) / 2; + var s = (a + b + c) / 2; a = (s - a) / 2; b = (s - b) / 2; @@ -105,5 +107,24 @@ public static double TriangleEdgeLengthsToArea(double a, double b, double c) { public static bool IsValidChildResolution(int parentResolution, int childResolution) => childResolution >= parentResolution && childResolution <= MAX_H3_RES; + #if NETSTANDARD2_0 + /// + /// Clamps the specified value between min + /// and max + /// + /// value to clamp + /// minimum value + /// maximum value + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static T Clamp(T value, T min, T max) where T : IComparable { + var result = value; + if (value.CompareTo(min) < 0) result = min; + if (value.CompareTo(max) > 0) result = max; + return result; + } + #endif + } -} + +} \ No newline at end of file diff --git a/test/H3.Benchmarks/Algorithms/LineBenchmarks.cs b/test/H3.Benchmarks/Algorithms/LineBenchmarks.cs index f3ca823..d9c3b30 100644 --- a/test/H3.Benchmarks/Algorithms/LineBenchmarks.cs +++ b/test/H3.Benchmarks/Algorithms/LineBenchmarks.cs @@ -10,7 +10,10 @@ namespace H3.Benchmarks.Algorithms { + [SimpleJob(RuntimeMoniker.Net60)] [SimpleJob(RuntimeMoniker.Net50)] + [SimpleJob(RuntimeMoniker.NetCoreApp31)] + [SimpleJob(RuntimeMoniker.Net48)] [MemoryDiagnoser] public class LineBenchmarks { diff --git a/test/H3.Benchmarks/Algorithms/PolyfillBenchmarks.cs b/test/H3.Benchmarks/Algorithms/PolyfillBenchmarks.cs index 015d662..b6c00ea 100644 --- a/test/H3.Benchmarks/Algorithms/PolyfillBenchmarks.cs +++ b/test/H3.Benchmarks/Algorithms/PolyfillBenchmarks.cs @@ -1,10 +1,7 @@ using BenchmarkDotNet.Attributes; using BenchmarkDotNet.Jobs; -using System; using System.Linq; using H3.Algorithms; -using H3Lib; -using H3Lib.Extensions; using NetTopologySuite.Geometries; using static H3.Constants; using static H3.Utils; @@ -12,7 +9,10 @@ namespace H3.Benchmarks.Algorithms { + [SimpleJob(RuntimeMoniker.Net60)] [SimpleJob(RuntimeMoniker.Net50)] + [SimpleJob(RuntimeMoniker.NetCoreApp31)] + [SimpleJob(RuntimeMoniker.Net48)] [MemoryDiagnoser] public class PolyfillBenchmarks { @@ -39,32 +39,55 @@ public class PolyfillBenchmarks { private static readonly Polygon SfGeometry = DefaultGeometryFactory.CreatePolygon(UberSfTestPoly.Select(g => g.ToCoordinate()).Reverse().ToArray()); - private static readonly GeoPolygon EntireWorldGeoPolygon = new() { - GeoFence = new GeoFence { - NumVerts = EntireWorld.Length - 1, - Verts = EntireWorld.SkipLast(1).Select(g => new H3Lib.GeoCoord(Convert.ToDecimal(g.Latitude), Convert.ToDecimal(g.Longitude))).ToArray() - } - }; + //private static readonly GeoPolygon EntireWorldGeoPolygon = new() { + // GeoFence = new GeoFence { + // NumVerts = EntireWorld.Length - 1, + // Verts = EntireWorld.SkipLast(1).Select(g => new H3Lib.GeoCoord(Convert.ToDecimal(g.Latitude), Convert.ToDecimal(g.Longitude))).ToArray() + // } + //}; - private static readonly GeoPolygon SfGeoPolygon = new() { - GeoFence = new GeoFence { - NumVerts = UberSfTestPoly.Length - 1, - Verts = UberSfTestPoly.SkipLast(1).Select(g => new H3Lib.GeoCoord(Convert.ToDecimal(g.Latitude), Convert.ToDecimal(g.Longitude))).ToArray() - } - }; + //private static readonly GeoPolygon SfGeoPolygon = new() { + // GeoFence = new GeoFence { + // NumVerts = UberSfTestPoly.Length - 1, + // Verts = UberSfTestPoly.SkipLast(1).Select(g => new H3Lib.GeoCoord(Convert.ToDecimal(g.Latitude), Convert.ToDecimal(g.Longitude))).ToArray() + // } + //}; [Benchmark(Description = "pocketken.H3.Fill(worldPolygon, 4)")] - public int PolyfillWorld() => EntireWorldGeometry.Fill(4).Count(); + public int PolyfillWorld4() => EntireWorldGeometry.Fill(4).Count(); + + [Benchmark(Description = "pocketken.H3.Fill(worldPolygon, 5)")] + public int PolyfillWorld5() => EntireWorldGeometry.Fill(5).Count(); + + //[Benchmark(Description = "H3Lib.Polyfill(worldPolygon, 4)")] + //public int H3LibPolyfillWorld() => EntireWorldGeoPolygon.Polyfill(4).Count; [Benchmark(Description = "pocketken.H3.Fill(sfPolygon, 10)")] - public int PolyfillSf() => SfGeometry.Fill(10).Count(); + public int PolyfillSf10() => SfGeometry.Fill(10).Count(); + + //[Benchmark(Description = "H3Lib.Polyfill(sfPolygon, 10)")] + //public int H3LibPolyfillSf() => SfGeoPolygon.Polyfill(10).Count; + + [Benchmark(Description = "pocketken.H3.Fill(sfPolygon, 11)")] + public int PolyfillSf11() => SfGeometry.Fill(11).Count(); + + //[Benchmark(Description = "H3Lib.Polyfill(sfPolygon, 11)")] + //public int H3LibPolyfillSf11() => SfGeoPolygon.Polyfill(11).Count; + + [Benchmark(Description = "pocketken.H3.Fill(sfPolygon, 12)")] + public int PolyfillSf12() => SfGeometry.Fill(12).Count(); + + //[Benchmark(Description = "H3Lib.Polyfill(sfPolygon, 12)")] + //public int H3LibPolyfillSf12() => SfGeoPolygon.Polyfill(12).Count; - [Benchmark(Description = "H3Lib.Polyfill(sfPolygon, 10)")] - public int H3LibPolyfillSf() => SfGeoPolygon.Polyfill(10).Count; + //[Benchmark(Description = "pocketken.H3.Fill(sfPolygon, 13)")] + //public int PolyfillSf13() => SfGeometry.Fill(13).Count(); - [Benchmark(Description = "pocketken.H3.Fill(sfPolygon, 14)")] - public int PolyfillSf14() => SfGeometry.Fill(14).Count(); + //[Benchmark(Description = "pocketken.H3.Fill(sfPolygon, 14)")] + //public int PolyfillSf14() => SfGeometry.Fill(14).Count(); + //[Benchmark(Description = "pocketken.H3.Fill(sfPolygon, 15)")] + //public int PolyfillSf15() => SfGeometry.Fill(15).Count(); } diff --git a/test/H3.Benchmarks/Algorithms/RingBenchmarks.cs b/test/H3.Benchmarks/Algorithms/RingBenchmarks.cs index ec83bf6..77c5cc7 100644 --- a/test/H3.Benchmarks/Algorithms/RingBenchmarks.cs +++ b/test/H3.Benchmarks/Algorithms/RingBenchmarks.cs @@ -5,13 +5,16 @@ using H3.Algorithms; using H3.Extensions; using H3.Test; -using H3Lib.Extensions; using BenchmarkDotNet.Jobs; using H3.Model; +using H3Lib.Extensions; namespace H3.Benchmarks.Algorithms { + [SimpleJob(RuntimeMoniker.Net60)] [SimpleJob(RuntimeMoniker.Net50)] + [SimpleJob(RuntimeMoniker.NetCoreApp31)] + [SimpleJob(RuntimeMoniker.Net48)] [MemoryDiagnoser] public class RingBenchmarks { @@ -46,8 +49,9 @@ public void Setup() { [Benchmark(Description = "H3Lib.KRingDistances(hex, k = 50)")] public Dictionary H3Lib_KRingDistancesHex() => TestH3LibIndex.KRingDistances(K); - [Benchmark(Description = "H3Lib.KRingDistances(pent, k = 50)")] - public Dictionary H3Lib_KRingDistancesPent() => TestH3LibPentagonIndex.KRingDistances(K); + //[Benchmark(Description = "H3Lib.KRingDistances(pent, k = 50)")] + //public Dictionary H3Lib_KRingDistancesPent() => TestH3LibPentagonIndex.KRingDistances(K); + } } diff --git a/test/H3.Benchmarks/Extensions/GeometryExtensionBenchmarks.cs b/test/H3.Benchmarks/Extensions/GeometryExtensionBenchmarks.cs new file mode 100644 index 0000000..9cf0284 --- /dev/null +++ b/test/H3.Benchmarks/Extensions/GeometryExtensionBenchmarks.cs @@ -0,0 +1,55 @@ +using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Jobs; +using System; +using System.Linq; +using H3.Extensions; +using H3.Model; +using H3.Test; +using H3Lib; +using H3Lib.Extensions; +using NetTopologySuite.Geometries; + +namespace H3.Benchmarks.Extensions { + + [SimpleJob(RuntimeMoniker.Net60)] + [SimpleJob(RuntimeMoniker.Net50)] + [SimpleJob(RuntimeMoniker.NetCoreApp31)] + [SimpleJob(RuntimeMoniker.Net48)] + [MemoryDiagnoser] + public class GeometryExtensionBenchmarks { + + private static readonly H3Index PentIndex = LookupTables.PentagonIndexesPerResolution[14].First(); + private static readonly H3Lib.H3Index H3LibTestIndex = new(TestHelpers.SfIndex); + private static readonly H3Lib.H3Index H3LibPentIndex = new(PentIndex); + private static readonly H3Index[] TestIndexChildren = TestHelpers.SfIndex.GetChildrenForResolution(15).ToArray(); + private static readonly H3Index[] PentIndexChildren = PentIndex.GetChildrenForResolution(15).ToArray(); + + [GlobalSetup] + public void Setup() { + Console.WriteLine($"Hex = {TestHelpers.SfIndex}"); + Console.WriteLine($"Hex Children @ 15 = {TestIndexChildren.Length} cells"); + Console.WriteLine($"Pent = {PentIndex}"); + Console.WriteLine($"Pent Children @ 15 = {PentIndexChildren.Length} cells"); + } + + [Benchmark(Description = "pocketken.H3.H3GeometryExtensions.GetCellBoundary(hex)")] + public Polygon PocketkenGetCellBoundaryHex() => TestHelpers.SfIndex.GetCellBoundary(); + + [Benchmark(Description = "H3Lib.Extensions.H3IndexExtensions.ToGeoBoundary(hex)")] + public GeoBoundary H3LibToGeoBoundaryHex() => H3LibTestIndex.ToGeoBoundary(); + + [Benchmark(Description = "pocketken.H3.H3GeometryExtensions.GetCellBoundary(pent)")] + public Polygon PocketkenGetCellBoundaryPent() => PentIndex.GetCellBoundary(); + + [Benchmark(Description = "H3Lib.Extensions.H3IndexExtensions.ToGeoBoundary(pent)")] + public GeoBoundary H3LibToGeoBoundaryPent() => H3LibPentIndex.ToGeoBoundary(); + + [Benchmark(Description = "pocketken.H3.H3GeometryExtensions.GetCellBoundaries(hexChildren)")] + public MultiPolygon PocketkenGetCellBoundariesHex() => TestIndexChildren.GetCellBoundaries(); + + [Benchmark(Description = "pocketken.H3.H3GeometryExtensions.GetCellBoundaries(pentChildren)")] + public MultiPolygon PocketkenGetCellBoundariesPent() => PentIndexChildren.GetCellBoundaries(); + + } + +} diff --git a/test/H3.Benchmarks/Extensions/HierarchyExtensionBenchmarks.cs b/test/H3.Benchmarks/Extensions/HierarchyExtensionBenchmarks.cs index 94b9805..55c4495 100644 --- a/test/H3.Benchmarks/Extensions/HierarchyExtensionBenchmarks.cs +++ b/test/H3.Benchmarks/Extensions/HierarchyExtensionBenchmarks.cs @@ -4,23 +4,54 @@ using System.Collections.Generic; using System.Linq; using H3.Extensions; +using H3.Model; using H3.Test; using H3Lib.Extensions; namespace H3.Benchmarks.Extensions { + [SimpleJob(RuntimeMoniker.Net60)] [SimpleJob(RuntimeMoniker.Net50)] + [SimpleJob(RuntimeMoniker.NetCoreApp31)] + [SimpleJob(RuntimeMoniker.Net48)] [MemoryDiagnoser] public class HierarchyExtensionBenchmarks { private const int resolution = 15; + private static readonly H3Index PentIndex = LookupTables.PentagonIndexesPerResolution[14].First(); private static readonly H3Lib.H3Index H3LibTestIndex = new(TestHelpers.SfIndex); + private static readonly H3Lib.H3Index H3LibPentIndex = new(PentIndex); [GlobalSetup] public void Setup() { - Console.WriteLine($"SfIndex = {TestHelpers.SfIndex}"); + Console.WriteLine($"Hex = {TestHelpers.SfIndex}"); + Console.WriteLine($"Pent = {PentIndex}"); } + [Benchmark(Description = "pocketken.H3.GetDirectNeighbour(hex, I)")] + public (H3Index, int) GetDirectNeighbourHexI() => TestHelpers.SfIndex.GetDirectNeighbour(Direction.I); + + [Benchmark(Description = "pocketken.H3.GetDirectNeighbour(hex, IJ)")] + public (H3Index, int) GetDirectNeighbourHexIJ() => TestHelpers.SfIndex.GetDirectNeighbour(Direction.IJ); + + [Benchmark(Description = "H3Lib.NeighborRotations(hex, I)")] + public (H3Lib.H3Index, int) H3LibNeighborRotationsHexI() => H3LibTestIndex.NeighborRotations(H3Lib.Direction.I_AXES_DIGIT, 0); + + [Benchmark(Description = "H3Lib.NeighborRotations(hex, IJ)")] + public (H3Lib.H3Index, int) H3LibNeighborRotationsHexIJ() => H3LibTestIndex.NeighborRotations(H3Lib.Direction.IJ_AXES_DIGIT, 0); + + [Benchmark(Description = "pocketken.H3.GetDirectNeighbour(pent, I)")] + public (H3Index, int) GetDirectNeighbourPentI() => PentIndex.GetDirectNeighbour(Direction.I); + + [Benchmark(Description = "pocketken.H3.GetDirectNeighbour(pent, IJ)")] + public (H3Index, int) GetDirectNeighbourPentIJ() => PentIndex.GetDirectNeighbour(Direction.IJ); + + [Benchmark(Description = "H3Lib.NeighborRotations(pent, I)")] + public (H3Lib.H3Index, int) H3LibNeighborRotationsPentI() => H3LibPentIndex.NeighborRotations(H3Lib.Direction.I_AXES_DIGIT, 0); + + [Benchmark(Description = "H3Lib.NeighborRotations(pent, IJ)")] + public (H3Lib.H3Index, int) H3LibNeighborRotationsPentIJ() => H3LibPentIndex.NeighborRotations(H3Lib.Direction.IJ_AXES_DIGIT, 0); + [Benchmark(Description = "pocketken.H3.GetChildrenForResolution")] public List GetChildrenForResolution() => TestHelpers.SfIndex.GetChildrenForResolution(resolution).ToList(); diff --git a/test/H3.Benchmarks/Extensions/SetExtensionBenchmarks.cs b/test/H3.Benchmarks/Extensions/SetExtensionBenchmarks.cs index 79de786..29e236d 100644 --- a/test/H3.Benchmarks/Extensions/SetExtensionBenchmarks.cs +++ b/test/H3.Benchmarks/Extensions/SetExtensionBenchmarks.cs @@ -8,7 +8,10 @@ namespace H3.Benchmarks.Extensions { + [SimpleJob(RuntimeMoniker.Net60)] [SimpleJob(RuntimeMoniker.Net50)] + [SimpleJob(RuntimeMoniker.NetCoreApp31)] + [SimpleJob(RuntimeMoniker.Net48)] [MemoryDiagnoser] public class CompactBenchmarks { @@ -27,7 +30,10 @@ public class CompactBenchmarks { public List H3LibCompact() => H3LibTestCompactList.Compact().Item2; } + [SimpleJob(RuntimeMoniker.Net60)] [SimpleJob(RuntimeMoniker.Net50)] + [SimpleJob(RuntimeMoniker.NetCoreApp31)] + [SimpleJob(RuntimeMoniker.Net48)] [MemoryDiagnoser] public class UncompactBenchmarks { diff --git a/test/H3.Benchmarks/H3.Benchmarks.csproj b/test/H3.Benchmarks/H3.Benchmarks.csproj index 581a109..5da0552 100644 --- a/test/H3.Benchmarks/H3.Benchmarks.csproj +++ b/test/H3.Benchmarks/H3.Benchmarks.csproj @@ -2,7 +2,9 @@ Exe - net5.0 + net6.0;net5.0;netcoreapp3.1;net48 + net6.0;net5.0;netcoreapp3.1 + 9.0 false diff --git a/test/H3.Benchmarks/H3IndexBenchmarks.cs b/test/H3.Benchmarks/H3IndexBenchmarks.cs index 4d2508e..d485708 100644 --- a/test/H3.Benchmarks/H3IndexBenchmarks.cs +++ b/test/H3.Benchmarks/H3IndexBenchmarks.cs @@ -3,15 +3,54 @@ using System.Linq; using H3.Test; using H3.Model; +using H3Lib.Extensions; namespace H3.Benchmarks { + [SimpleJob(RuntimeMoniker.Net60)] [SimpleJob(RuntimeMoniker.Net50)] + [SimpleJob(RuntimeMoniker.NetCoreApp31)] + [SimpleJob(RuntimeMoniker.Net48)] [MemoryDiagnoser] public class H3IndexBenchmarks { private static readonly H3Index TestIndex = TestHelpers.TestIndexValue; + private static readonly H3Index InvalidIndex = new(TestIndex); private static readonly H3Index PentagonIndex = LookupTables.PentagonIndexesPerResolution[14].First(); + private static readonly H3Lib.H3Index TestH3LibIndex = new(TestIndex); + private static readonly H3Lib.H3Index PentagonH3LibIndex = new(PentagonIndex); + private static readonly H3Lib.H3Index NullH3LibIndex = new(H3Index.Invalid); + private H3Lib.H3Index _invalidH3LibIndex; + + [GlobalSetup] + public void Setup() { + InvalidIndex.SetDirectionForResolution(10, Direction.Invalid); + _invalidH3LibIndex = new H3Lib.H3Index(InvalidIndex); + } + + [Benchmark(Description = "pocketken.H3.H3Index.IsValid(hex)")] + public bool PocketkenIsValid() => TestIndex.IsValid; + + [Benchmark(Description = "pocketken.H3.H3Index.IsValid(pent)")] + public bool PocketkenIsValidPent() => PentagonIndex.IsValid; + + [Benchmark(Description = "pocketken.H3.H3Index.IsValid(null)")] + public bool PocketkenIsValidNull() => H3Index.Invalid.IsValid; + + [Benchmark(Description = "pocketken.H3.H3Index.IsValid(invalid)")] + public bool PocketkenIsValidInvalid() => InvalidIndex.IsValid; + + [Benchmark(Description = "H3Lib.Extensions.H3IndexExtensions.IsValid(hex)")] + public bool H3LibIsValid() => TestH3LibIndex.IsValid(); + + [Benchmark(Description = "H3Lib.Extensions.H3IndexExtensions.IsValid(pent)")] + public bool H3LibIsValidPent() => PentagonH3LibIndex.IsValid(); + + [Benchmark(Description = "H3Lib.Extensions.H3IndexExtensions.IsValid(null)")] + public bool H3LibIsValidNull() => NullH3LibIndex.IsValid(); + + [Benchmark(Description = "H3Lib.Extensions.H3IndexExtensions.IsValid(invalid)")] + public bool H3LibIsValidInvalid() => _invalidH3LibIndex.IsValid(); [Benchmark(Description = "pocketken.H3.H3Index.RotateClockwise(hex)")] public void PocketkenRotateHexClockwise() => TestIndex.RotateClockwise(); @@ -20,10 +59,19 @@ public class H3IndexBenchmarks { public void PocketkenRotateHexCounterClockwise() => TestIndex.RotateCounterClockwise(); [Benchmark(Description = "pocketken.H3.H3Index.RotateClockwise(pent)")] - public void PocketkenRotatePentagonClockwise() => PentagonIndex.RotateClockwise(); + public void PocketkenRotatePentagonClockwise() => PentagonIndex.RotatePentagonClockwise(); [Benchmark(Description = "pocketken.H3.H3Index.RotateCounterClockwise(pent)")] - public void PocketkenRotatePentagonCounterClockwise() => PentagonIndex.RotateCounterClockwise(); + public void PocketkenRotatePentagonCounterClockwise() => PentagonIndex.RotatePentagonCounterClockwise(); + + [Benchmark(Description = "pocketken.H3.H3Index.RotateClockwise(hex, 4)")] + public void PocketkenRotateHexClockwise4() => TestIndex.RotateClockwise(4); + + [Benchmark(Description = "pocketken.H3.H3Index.RotateCounterClockwise(hex, 4)")] + public void PocketkenRotateHexCounterClockwise4() => TestIndex.RotateCounterClockwise(4); + + [Benchmark(Description = "pocketken.H3.H3Index.LeadingNonZeroDirection")] + public Direction LeadingNonZeroDirection() => TestIndex.LeadingNonZeroDirection; } } diff --git a/test/H3.Benchmarks/MathBenchmarks.cs b/test/H3.Benchmarks/MathBenchmarks.cs new file mode 100644 index 0000000..2674ba8 --- /dev/null +++ b/test/H3.Benchmarks/MathBenchmarks.cs @@ -0,0 +1,27 @@ +using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Jobs; +using System; + +namespace H3.Benchmarks { + + [SimpleJob(RuntimeMoniker.Net60)] + [SimpleJob(RuntimeMoniker.Net50)] + [SimpleJob(RuntimeMoniker.NetCoreApp31)] + [SimpleJob(RuntimeMoniker.Net48)] + [MemoryDiagnoser] + public class MathBenchmarks { + + private const double Value = 0.2387273897282832; + + [Benchmark(Description="Math.Round", Baseline = true)] + public static int MathRound() => (int)Math.Round(Value); + + [Benchmark(Description = "Math.Round with midpoint away from zero")] + public static int MathRoundMidpoint() => (int)Math.Round(Value, MidpointRounding.AwayFromZero); + + [Benchmark(Description = "Math.Floor + 0.5")] + public static int MathFloor() => (int)Math.Floor(Value + 0.5); + + } + +} \ No newline at end of file diff --git a/test/H3.Benchmarks/Model/CoordIjkBenchmarks.cs b/test/H3.Benchmarks/Model/CoordIjkBenchmarks.cs new file mode 100644 index 0000000..559d025 --- /dev/null +++ b/test/H3.Benchmarks/Model/CoordIjkBenchmarks.cs @@ -0,0 +1,24 @@ +using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Jobs; +using H3.Model; + +namespace H3.Benchmarks.Model { + + [SimpleJob(RuntimeMoniker.Net60)] + [SimpleJob(RuntimeMoniker.Net50)] + [SimpleJob(RuntimeMoniker.NetCoreApp31)] + [SimpleJob(RuntimeMoniker.Net48)] + [MemoryDiagnoser] + public class CoordIjkBenchmarks { + + private static readonly CoordIJK TestIjk = new (1, 4, -2); + + [Benchmark(Baseline = true)] + public static CoordIJK NormalizeTest() => TestIjk.Normalize(); + + [Benchmark] + public static CoordIJK StaticNormalizeTest() => CoordIJK.Normalize(TestIjk); + + } + +} \ No newline at end of file diff --git a/test/H3.Benchmarks/Program.cs b/test/H3.Benchmarks/Program.cs index 37c693d..8ab0041 100644 --- a/test/H3.Benchmarks/Program.cs +++ b/test/H3.Benchmarks/Program.cs @@ -5,7 +5,7 @@ namespace H3.Benchmarks { [MemoryDiagnoser] public class Program { - static void Main(string[] args) => BenchmarkSwitcher.FromAssemblies(new[] { typeof(Program).Assembly }).Run(args); + public static void Main(string[] args) => BenchmarkSwitcher.FromAssemblies(new[] { typeof(Program).Assembly }).Run(args); } } diff --git a/test/H3.Test/Algorithms/LineTests.cs b/test/H3.Test/Algorithms/LineTests.cs index 06d8a68..8786fa0 100644 --- a/test/H3.Test/Algorithms/LineTests.cs +++ b/test/H3.Test/Algorithms/LineTests.cs @@ -40,8 +40,8 @@ public class LineTests { [Test] public void Test_LineTo_ReturnsExpectedIndicies() { // Arrange - H3Index start = H3Index.FromPoint(new Point(-110, 30), 14); - H3Index end = H3Index.FromPoint(new Point(-110, 30.0005), 14); + var start = H3Index.FromPoint(new Point(-110, 30), 14); + var end = H3Index.FromPoint(new Point(-110, 30.0005), 14); // Act var line = start.LineTo(end).ToArray(); @@ -83,7 +83,7 @@ public void Test_Upstream_LineTo_KRing_Assertions(int resolution, int k) { // Assert foreach (var (Start, End, Distance, Line) in lines) { if (Distance >= 0) { - int i = 0; + var i = 0; H3Index lastIndex = null; H3Index previousLastIndex = null; diff --git a/test/H3.Test/Algorithms/PolyfillTests.cs b/test/H3.Test/Algorithms/PolyfillTests.cs index aa8fea4..22a7e2d 100644 --- a/test/H3.Test/Algorithms/PolyfillTests.cs +++ b/test/H3.Test/Algorithms/PolyfillTests.cs @@ -108,7 +108,7 @@ public void Test_Polyfill_FillKnownValueBoundary() { // Assert Assert.AreEqual(KnownValuePolyfillAtRes15.Length, filled.Length, "should be same length"); - for (int i = 0; i < KnownValuePolyfillAtRes15.Length; i += 1) { + for (var i = 0; i < KnownValuePolyfillAtRes15.Length; i += 1) { Assert.Contains(KnownValuePolyfillAtRes15[i], filled, $"missing {KnownValuePolyfillAtRes15[i]}"); } } @@ -125,6 +125,30 @@ public void Test_Polyfill_UberSfTestPoly() { Assert.AreEqual(1253, filledCount, "should return 1253 indicies"); } + [Test] + public void Test_Polyfill_VertexNodeAny_UberSfTestPoly() { + // Arrange + var polygon = CreatePolygon(UberSfTestPoly); + + // Act + var filledCount = polygon.Fill(9, VertexTestMode.Any).Count(); + + // Assert + Assert.AreEqual(1334, filledCount, "should return 1334 indicies"); + } + + [Test] + public void Test_Polyfill_VertexNodeAll_UberSfTestPoly() { + // Arrange + var polygon = CreatePolygon(UberSfTestPoly); + + // Act + var filledCount = polygon.Fill(9, VertexTestMode.All).Count(); + + // Assert + Assert.AreEqual(1175, filledCount, "should return 1175 indicies"); + } + [Test] public void Test_Polyfill_PrimeMeridian() { // Arrange @@ -179,7 +203,7 @@ public void Test_Polyfill_TransMeridianComplex() { public void Test_Polyfill_Pentagon() { // Arrange var index = H3Index.Create(9, 24, 0); - GeoCoord coord = index.ToGeoCoord(); + var coord = index.ToGeoCoord(); GeoCoord topRight = new() { Latitude = coord.Latitude + EdgeLength2, Longitude = coord.Longitude + EdgeLength2 @@ -226,10 +250,10 @@ public void Test_Polyfill_EntireWorldMuhahaha(int resolution, int expectedCount) [Test] public void Test_Upstream_H3jsIssue67_One() { // Arrange - double east = -56.25 * M_PI_180; - double north = -33.13755119234615 * M_PI_180; - double south = -34.30714385628804 * M_PI_180; - double west = -57.65625 * M_PI_180; + var east = -56.25 * M_PI_180; + var north = -33.13755119234615 * M_PI_180; + var south = -34.30714385628804 * M_PI_180; + var west = -57.65625 * M_PI_180; var polygon = CreatePolygon(new GeoCoord[] { (north, east), @@ -243,16 +267,16 @@ public void Test_Upstream_H3jsIssue67_One() { var filled = polygon.Fill(7).Count(); // Arrange - Assert.AreEqual(4499, filled, $"should have filled 4499"); + Assert.AreEqual(4499, filled, "should have filled 4499"); } [Test] public void Test_Upstream_H3jsIssue67_Two() { // Arrange - double east = -57.65625 * M_PI_180; - double north = -34.30714385628804 * M_PI_180; - double south = -35.4606699514953 * M_PI_180; - double west = -59.0625 * M_PI_180; + var east = -57.65625 * M_PI_180; + var north = -34.30714385628804 * M_PI_180; + var south = -35.4606699514953 * M_PI_180; + var west = -59.0625 * M_PI_180; var polygon = CreatePolygon(new GeoCoord[] { (north, east), @@ -266,7 +290,7 @@ public void Test_Upstream_H3jsIssue67_Two() { var filled = polygon.Fill(7).Count(); // Arrange - Assert.AreEqual(4609, filled, $"should have filled 4499"); + Assert.AreEqual(4609, filled, "should have filled 4499"); } [Test] @@ -285,7 +309,7 @@ public void Test_Upstream_H3jsIssue136() { var filled = polygon.Fill(13).Count(); // Arrange - Assert.AreEqual(4353, filled, $"should have filled 4353"); + Assert.AreEqual(4353, filled, "should have filled 4353"); } /// diff --git a/test/H3.Test/Algorithms/RingTests.cs b/test/H3.Test/Algorithms/RingTests.cs index 77df303..d9e1892 100644 --- a/test/H3.Test/Algorithms/RingTests.cs +++ b/test/H3.Test/Algorithms/RingTests.cs @@ -126,7 +126,7 @@ public void Test_Upstream_GetHexRing_Ring(int k, H3Index[] expectedRing) { // Assert Assert.AreEqual(expectedRing.Length, actual.Count, "should have same count"); - for (int i = 0; i < expectedRing.Length; i += 1) { + for (var i = 0; i < expectedRing.Length; i += 1) { var expectedIndex = expectedRing[i]; var actualIndex = actual[i]; Assert.AreEqual(expectedIndex, actualIndex, "should be equal"); @@ -147,7 +147,7 @@ public void Test_Upstream_GetHexRing_NearPentagon(int k) { [Test] public void Test_Upstream_GetHexRing_OnPentagon() { // Arrange - H3Index onPentagon = H3Index.Create(0, 4, 0); + var onPentagon = H3Index.Create(0, 4, 0); // Act var exception = Assert.Throws(() => onPentagon.GetHexRing(2).ToList(), "should throw pentagon exception"); @@ -170,7 +170,7 @@ public void Test_Upstream_372_GridDiskInvalidDigit() { private static void AssertRing((H3Index, int)[] expectedRing, RingCell[] actualRing) { Assert.AreEqual(expectedRing.Length, actualRing.Length, "should be same length"); - for (int i = 0; i < expectedRing.Length; i += 1) { + for (var i = 0; i < expectedRing.Length; i += 1) { var expected = expectedRing[i]; Assert.IsNotNull(actualRing.FirstOrDefault(cell => cell.Index == expected.Item1 && cell.Distance == expected.Item2), $"can't find {expected.Item1:x} at k {expected.Item2}"); diff --git a/test/H3.Test/Extensions/H3GeometryExtensionsTests.cs b/test/H3.Test/Extensions/H3GeometryExtensionsTests.cs index 8bc0f2b..5ae614c 100644 --- a/test/H3.Test/Extensions/H3GeometryExtensionsTests.cs +++ b/test/H3.Test/Extensions/H3GeometryExtensionsTests.cs @@ -144,18 +144,22 @@ public void Test_GetCellBoundary_PolygonWktMatchesPg() { [Test] [TestCaseSource(typeof(H3GeometryExtensionsTests), "GetCellBoundaryVerticesTestCases")] public bool Test_Upstream_GetCellBoundaryVertices(string testDataFn, List<(H3Index, GeoCoord[])> expectedData) { + // Arrange + // Act - var vertices = expectedData.Select(t => t.Item1.GetCellBoundaryVertices().ToArray()); + var vertices = expectedData.Select(t => t.Item1.GetCellBoundaryVertices().ToList()).ToList(); // Assert - return expectedData.Zip(vertices).All(e => { - var expectedVerts = e.First.Item2; - var actualVerts = e.Second; - if (expectedVerts.Length != actualVerts.Length) { - Assert.Fail($"{testDataFn}: {e.First.Item1} vertex count mismatch: expected {expectedVerts.Length} got {actualVerts.Length}"); + for (var v = 0; v < expectedData.Count; v += 1) { + var expectedVerts = expectedData[v].Item2; + var actualVerts = vertices[v]; + + if (expectedVerts.Length != actualVerts.Count) { + Assert.Fail($"{testDataFn}: {expectedData[v].Item1} vertex count mismatch: expected {expectedVerts.Length} got {actualVerts.Count}"); return false; } - for (int i = 0; i < expectedVerts.Length; i += 1) { + + for (var i = 0; i < expectedVerts.Length; i += 1) { var ev = expectedVerts[i]; var av = actualVerts[i]; if (Math.Abs(ev.Latitude - av.Latitude) > 0.000001 || @@ -163,8 +167,10 @@ public bool Test_Upstream_GetCellBoundaryVertices(string testDataFn, List<(H3Ind return false; } } - return true; - }); + + } + + return true; } [Test] @@ -177,7 +183,7 @@ public void Test_GetCellAreaInKm2() { var areas = indexes.Select(index => index.CellAreaInKmSquared()).ToArray(); // Assert - for (int i = 0; i < CellAreasInKm2.Length; i += 1) { + for (var i = 0; i < CellAreasInKm2.Length; i += 1) { Assert.IsTrue(Math.Abs(areas[i] - CellAreasInKm2[i]) < 1e-8, $"{indexes[i]} should be {CellAreasInKm2[i]} not {areas[i]}"); } } @@ -229,7 +235,7 @@ private static int CountValidFaces(int[] faces) => faces private static void AssertCellBoundaryVertices(Point[] expected, GeoCoord[] actual) { Assert.AreEqual(expected.Length, actual.Length, "should be same length"); - for (int i = 0; i < expected.Length; i += 1) { + for (var i = 0; i < expected.Length; i += 1) { var p = expected[i]; Assert.IsTrue(Math.Abs(p.X - actual[i].LongitudeDegrees) < EPSILON_DEG, $"longitude {i} should be {p.X} not {actual[i].LongitudeDegrees}"); Assert.IsTrue(Math.Abs(p.Y - actual[i].LatitudeDegrees) < EPSILON_DEG, $"latitude {i} should be {p.X} not {actual[i].LongitudeDegrees}"); diff --git a/test/H3.Test/Extensions/H3HierarchyExtensionsTests.cs b/test/H3.Test/Extensions/H3HierarchyExtensionsTests.cs index ee0c05b..5ec516e 100644 --- a/test/H3.Test/Extensions/H3HierarchyExtensionsTests.cs +++ b/test/H3.Test/Extensions/H3HierarchyExtensionsTests.cs @@ -13,6 +13,50 @@ namespace H3.Test.Extensions { public class H3HierarchyExtensionsTests { private static readonly H3Index BaseCell0 = H3Index.Create(0, 0, 0); + private static readonly H3Index[] ExpectedOneResStepChildren = { + 0x89283080dc3ffff, 0x89283080dc7ffff, + 0x89283080dcbffff, 0x89283080dcfffff, + 0x89283080dd3ffff, 0x89283080dd7ffff, + 0x89283080ddbffff + }; + + private static readonly H3Index[] ExpectedMultipleResStepChildren = { + 0x8a283080dd27fff, 0x8a283080dd37fff, 0x8a283080dc47fff, + 0x8a283080dcdffff, 0x8a283080dc5ffff, 0x8a283080dc27fff, + 0x8a283080ddb7fff, 0x8a283080dc07fff, 0x8a283080dd8ffff, + 0x8a283080dd5ffff, 0x8a283080dc4ffff, 0x8a283080dd47fff, + 0x8a283080dce7fff, 0x8a283080dd1ffff, 0x8a283080dceffff, + 0x8a283080dc6ffff, 0x8a283080dc87fff, 0x8a283080dcaffff, + 0x8a283080dd2ffff, 0x8a283080dcd7fff, 0x8a283080dd9ffff, + 0x8a283080dd6ffff, 0x8a283080dcc7fff, 0x8a283080dca7fff, + 0x8a283080dccffff, 0x8a283080dd77fff, 0x8a283080dc97fff, + 0x8a283080dd4ffff, 0x8a283080dd97fff, 0x8a283080dc37fff, + 0x8a283080dc8ffff, 0x8a283080dcb7fff, 0x8a283080dcf7fff, + 0x8a283080dd87fff, 0x8a283080dda7fff, 0x8a283080dc9ffff, + 0x8a283080dc77fff, 0x8a283080dc67fff, 0x8a283080dc57fff, + 0x8a283080ddaffff, 0x8a283080dd17fff, 0x8a283080dc17fff, + 0x8a283080dd57fff, 0x8a283080dc0ffff, 0x8a283080dd07fff, + 0x8a283080dc1ffff, 0x8a283080dd0ffff, 0x8a283080dc2ffff, + 0x8a283080dd67fff + }; + + private static readonly H3Index[] ExpectedPentagonChildren = { + 0x830800fffffffff, 0x830802fffffffff, 0x830803fffffffff, + 0x830804fffffffff, 0x830805fffffffff, 0x830806fffffffff, + 0x830810fffffffff, 0x830811fffffffff, 0x830812fffffffff, + 0x830813fffffffff, 0x830814fffffffff, 0x830815fffffffff, + 0x830816fffffffff, 0x830818fffffffff, 0x830819fffffffff, + 0x83081afffffffff, 0x83081bfffffffff, 0x83081cfffffffff, + 0x83081dfffffffff, 0x83081efffffffff, 0x830820fffffffff, + 0x830821fffffffff, 0x830822fffffffff, 0x830823fffffffff, + 0x830824fffffffff, 0x830825fffffffff, 0x830826fffffffff, + 0x830828fffffffff, 0x830829fffffffff, 0x83082afffffffff, + 0x83082bfffffffff, 0x83082cfffffffff, 0x83082dfffffffff, + 0x83082efffffffff, 0x830830fffffffff, 0x830831fffffffff, + 0x830832fffffffff, 0x830833fffffffff, 0x830834fffffffff, + 0x830835fffffffff, 0x830836fffffffff + }; + [Test] public void Test_Upstream_GetParentForResolution() { // Arrange @@ -56,23 +100,13 @@ public void Test_Upstream_GetParentForResolution_ReturnsSelfAtSameResolution() { public void Test_Upstream_GetChildrenForResolution_OneResStep() { // Arrange var sfHex8 = H3Index.FromGeoCoord(TestHelpers.SfCoord, 8); - var center = sfHex8.ToGeoCoord(); - var verts = sfHex8.GetCellBoundaryVertices().ToArray(); // Act - var children = sfHex8.GetChildrenForResolution(9).ToArray(); + var children = sfHex8.GetChildrenForResolution(9).ToList(); // Assert - Assert.AreEqual(7, children.Length, "should return 7 children"); - Assert.IsNotEmpty(children.Where(child => child == TestHelpers.SfIndex), "should contain sf @ 9"); - for (int i = 0; i < verts.Length; i += 1) { - GeoCoord avg = ( - (verts[i].Latitude + center.Latitude) / 2, - (verts[i].Longitude + center.Longitude) / 2 - ); - H3Index avgIndex = H3Index.FromGeoCoord(avg, 9); - Assert.IsNotEmpty(children.Where(child => child == avgIndex), $"unable to find expected child {avgIndex:x}"); - } + Assert.AreEqual(ExpectedOneResStepChildren.Length, children.Count, "should return correct child count"); + Assert.IsNotNull(children.Where(ExpectedOneResStepChildren.Contains), "should contain all"); } [Test] @@ -81,22 +115,24 @@ public void Test_Upstream_GetChildrenForResolution_MultipleResStep() { var sfHex8 = H3Index.FromGeoCoord(TestHelpers.SfCoord, 8); // Act - var children = sfHex8.GetChildrenForResolution(10); + var children = sfHex8.GetChildrenForResolution(10).ToList(); // Assert - AssertDistinctChildCount(children, 49); + Assert.AreEqual(ExpectedMultipleResStepChildren.Length, children.Count, "should return correct child count"); + Assert.IsNotNull(children.Where(ExpectedMultipleResStepChildren.Contains), "should contain all"); } [Test] public void Test_Upstream_GetChildrenForResolution_Pentagon() { // Arrange - var index = H3Index.Create(1, 4, 0); + var index = new H3Index(0x81083ffffffffff); // res 1 pentagon // Act - var children = index.GetChildrenForResolution(3); + var children = index.GetChildrenForResolution(3).ToList(); // Assert - AssertDistinctChildCount(children, 5 * 7 + 6); + Assert.AreEqual(ExpectedPentagonChildren.Length, children.Count, "should return correct child count"); + Assert.IsNotNull(children.Where(ExpectedPentagonChildren.Contains), "should contain all"); } [Test] @@ -127,7 +163,7 @@ public void Test_GetChildrenForResolution_TestIndexValue() { H3Index h3 = new(TestHelpers.TestIndexValue); // Act - H3Index[] children = h3.GetChildrenForResolution(15).ToArray(); + var children = h3.GetChildrenForResolution(15).ToArray(); // Assert TestHelpers.AssertAll(TestHelpers.TestIndexChildrenAtRes15, children); @@ -193,7 +229,7 @@ public void Test_Upstream_GetChildCenterForResolution_InvalidInputs(int resoluti [TestCase(Direction.IJ, 8, 1)] public void Test_GetDirectNeighbour_BaseCells(Direction direction, int expectedBaseCell, int baseRotations) { // Arrange - int expectedRotations = LookupTables.BaseCells[expectedBaseCell].IsPentagon ? baseRotations + 1 : baseRotations; + var expectedRotations = BaseCells.Cells[expectedBaseCell].IsPentagon ? baseRotations + 1 : baseRotations; // Act var (actual, rotations) = BaseCell0.GetDirectNeighbour(direction); diff --git a/test/H3.Test/Extensions/H3LocalIJExtensionsTests.cs b/test/H3.Test/Extensions/H3LocalIJExtensionsTests.cs index efd6063..fa0863d 100644 --- a/test/H3.Test/Extensions/H3LocalIJExtensionsTests.cs +++ b/test/H3.Test/Extensions/H3LocalIJExtensionsTests.cs @@ -40,7 +40,7 @@ public class H3LocalIJExtensionsTests { [TestCase(0, 15, Direction.Center)] public void Test_H3IndexToLocalIJK_BaseCell(int resolution, int baseCell, Direction direction) { // Arrange - H3Index index = H3Index.Create(resolution, baseCell, direction); + var index = H3Index.Create(resolution, baseCell, direction); // Act var ijk = LocalCoordIJK.ToLocalIJK(PentagonIndex, index); @@ -160,12 +160,17 @@ public void Test_Upstream_ToLocakIJ_FailsIfNotNeighbours() { public void Test_Upstream_ToLocalIJ_Invalid_ResolutionMismatch() { // Arrange H3Index invalid = 0x7fffffffffffffff; + #if NET48 + const string expectedMessage = "must be same resolution as origin\r\nParameter name: index"; + #else + const string expectedMessage = "must be same resolution as origin (Parameter 'index')"; + #endif // Act var actual = Assert.Throws(() => invalid.ToLocalIJ(BaseCell15)); // Assert - Assert.AreEqual("must be same resolution as origin (Parameter 'index')", actual.Message, "same message"); + Assert.AreEqual(expectedMessage, actual.Message, "same message"); } [Test] @@ -263,7 +268,7 @@ public void Test_Upstream_ToLocalIJ_Neighbours(int resolution) { Enumerable.Range((int)Direction.K, 6) .Where(dir => !(index.IsPentagon && dir == (int)Direction.K)) .Select(dir => { - H3Index offset = index.GetDirectNeighbour((Direction)dir).Item1; + var offset = index.GetDirectNeighbour((Direction)dir).Item1; return ( Origin: index, OriginIJK: index.ToLocalIJK(index), @@ -276,11 +281,11 @@ public void Test_Upstream_ToLocalIJ_Neighbours(int resolution) { // Assert foreach(var (Origin, OriginIJK, Index, LocalCoordIJ, Direction) in coords) { Assert.NotNull(LocalCoordIJ, "should not be null"); - CoordIJK invertedIjk = new CoordIJK(0, 0, 0).ToNeighbour(Direction); - for (int i = 0; i < 3; i += 1) { + var invertedIjk = new CoordIJK(0, 0, 0).ToNeighbour(Direction); + for (var i = 0; i < 3; i += 1) { invertedIjk = invertedIjk.RotateCounterClockwise(); } - CoordIJK ijk = (LocalCoordIJ.ToCoordIJK() + invertedIjk).Normalize(); + var ijk = (LocalCoordIJ.ToCoordIJK() + invertedIjk).Normalize(); Assert.AreEqual(OriginIJK, ijk, $"should be {OriginIJK} not {ijk}"); } } diff --git a/test/H3.Test/Extensions/H3UniEdgeExtensionsTests.cs b/test/H3.Test/Extensions/H3UniEdgeExtensionsTests.cs index f48c931..3604421 100644 --- a/test/H3.Test/Extensions/H3UniEdgeExtensionsTests.cs +++ b/test/H3.Test/Extensions/H3UniEdgeExtensionsTests.cs @@ -32,7 +32,7 @@ public void Test_GetUnidirectionalEdge() { Mode = Mode.UniEdge, ReservedBits = (int)Direction.IJ }; - H3Index destination = origin.GetDirectNeighbour(Direction.IJ).Item1; + var destination = origin.GetDirectNeighbour(Direction.IJ).Item1; // Act var actual = origin.GetUnidirectionalEdge(destination); @@ -144,7 +144,7 @@ public void Test_Upstream_UnidirectionalEdgeIsValid_FalseOnHighBit() { [Test] public void Test_Upstream_GetOriginFromUnidirectionalEdge() { // Arrange - H3Index sf2 = TestHelpers.SfIndex.GetDirectNeighbour(Direction.IJ).Item1; + var sf2 = TestHelpers.SfIndex.GetDirectNeighbour(Direction.IJ).Item1; var edge = TestHelpers.SfIndex.GetUnidirectionalEdge(sf2); // Act @@ -175,7 +175,7 @@ public void Test_Upstream_GetOriginFromUnidirectionalEdge_FailsOnNonEdge() { [Test] public void Test_Upstream_GetDestinationFromUnidirectionalEdge() { // Arrange - H3Index sf2 = TestHelpers.SfIndex.GetDirectNeighbour(Direction.IJ).Item1; + var sf2 = TestHelpers.SfIndex.GetDirectNeighbour(Direction.IJ).Item1; var edge = TestHelpers.SfIndex.GetUnidirectionalEdge(sf2); // Act @@ -206,7 +206,7 @@ public void Test_Upstream_GetDestinationFromUnidirectionalEdge_FailsOnNonEdge() [Test] public void Test_Upstream_GetIndexesFromUnidirectionalEdge() { // Arrange - H3Index sf2 = TestHelpers.SfIndex.GetDirectNeighbour(Direction.IJ).Item1; + var sf2 = TestHelpers.SfIndex.GetDirectNeighbour(Direction.IJ).Item1; var edge = TestHelpers.SfIndex.GetUnidirectionalEdge(sf2); // Act @@ -281,7 +281,7 @@ public void Test_Upstream_GetUnidirectionalEdgeBoundaryVertices() { public void Test_Upstream_GetUnidirectionalEdgeBoundaryVertices_PentagonClass3() { // Arrange var indexes = new List(); - for (int r = 1; r < MAX_H3_RES; r += 2) { + for (var r = 1; r < MAX_H3_RES; r += 2) { indexes.Add(H3Index.Create(r, 24, 0)); } var edgesPerIndex = indexes.Select(index => index.GetUnidirectionalEdges()); @@ -301,7 +301,7 @@ public void Test_Upstream_GetUnidirectionalEdgeBoundaryVertices_PentagonClass3() public void Test_Upstream_GetUnidirectionalEdgeBoundaryVertices_PentagonClass2() { // Arrange var indexes = new List(); - for (int r = 0; r < MAX_H3_RES; r += 2) { + for (var r = 0; r < MAX_H3_RES; r += 2) { indexes.Add(H3Index.Create(r, 24, 0)); } var edgesPerIndex = indexes.Select(index => index.GetUnidirectionalEdges()); @@ -336,7 +336,7 @@ public void Test_Upstream_GetExactEdgeLengthInRadians_ZeroForNonEdge() { } private static void AssertAllEdges(H3Index[] origins, IEnumerable[] rings, IEnumerable[] actualEdges) { - for (int i = 0; i < rings.Length; i += 1) { + for (var i = 0; i < rings.Length; i += 1) { var origin = origins[i]; var neighbours = rings[i]; var edges = actualEdges[i]; @@ -351,12 +351,12 @@ private static void AssertAllEdges(H3Index[] origins, IEnumerable[] rin } private static void AssertAllVertices(GeoCoord[][] expectedVertices, GeoCoord[][][] actualVertices, int[,] vertexMap, int expectedVertexCount, int maxEmpty) { - for (int e = 0; e < actualVertices.Length; e += 1) { - int empty = 0; + for (var e = 0; e < actualVertices.Length; e += 1) { + var empty = 0; var edgeVerts = actualVertices[e]; var expectedVerts = expectedVertices[e]; - for (int i = 0; i < 6; i += 1) { + for (var i = 0; i < 6; i += 1) { var edgeVert = edgeVerts[i]; if (edgeVert.Length == 0) { empty += 1; @@ -368,7 +368,7 @@ private static void AssertAllVertices(GeoCoord[][] expectedVertices, GeoCoord[][ Assert.AreEqual(expectedVertexCount, edgeVert.Length, $"should have {expectedVertexCount} vertices"); - for (int j = 0; j < expectedVertexCount; j += 1) { + for (var j = 0; j < expectedVertexCount; j += 1) { var expectedVert = expectedVerts[vertexMap[i, j]]; Assert.IsTrue( expectedVert.AlmostEquals(edgeVert[j]), diff --git a/test/H3.Test/Extensions/H3VertexExtensionsTests.cs b/test/H3.Test/Extensions/H3VertexExtensionsTests.cs index d60a585..2cb3b45 100644 --- a/test/H3.Test/Extensions/H3VertexExtensionsTests.cs +++ b/test/H3.Test/Extensions/H3VertexExtensionsTests.cs @@ -183,7 +183,7 @@ public void Test_Upstream_GetVertexIndicies_Hex() { public void Test_Upstream_IsValidVertex_InvalidOwner() { // Arrange H3Index origin = 0x823d6ffffffffff; - H3Index vert = origin.GetVertexIndex(0); + var vert = origin.GetVertexIndex(0); // Act vert ^= 1; @@ -196,7 +196,7 @@ public void Test_Upstream_IsValidVertex_InvalidOwner() { public void Test_Upstream_IsValidVertex_OriginDoesNotOwnCanonicalVertex() { // Arrange H3Index origin = 0x823d6ffffffffff; - H3Index vert = origin.GetVertexIndex(0); + var vert = origin.GetVertexIndex(0); // Act H3Index owner = new(vert) { diff --git a/test/H3.Test/H3.Test.csproj b/test/H3.Test/H3.Test.csproj index 4a6f37b..fd11e32 100644 --- a/test/H3.Test/H3.Test.csproj +++ b/test/H3.Test/H3.Test.csproj @@ -1,8 +1,9 @@ - net5.0 - + net6.0;net5.0;netcoreapp3.1;net48 + net6.0;net5.0;netcoreapp3.1 + 9.0 false diff --git a/test/H3.Test/H3IndexTests.cs b/test/H3.Test/H3IndexTests.cs index 19d10cf..331d2b0 100644 --- a/test/H3.Test/H3IndexTests.cs +++ b/test/H3.Test/H3IndexTests.cs @@ -33,7 +33,7 @@ public static IEnumerable ToGeoCoordTestCases { return new TestCaseData(TestHelpers.ReadLines(reader) .Where(s => !string.IsNullOrWhiteSpace(s)) .Select(s => { - var segs = s.Split(" "); + var segs = s.Split(' '); return ( new H3Index(segs[0]), Convert.ToDouble(segs[1]) * M_PI_180, @@ -61,7 +61,7 @@ public static IEnumerable FromGeoCoordTestCases { return new TestCaseData(TestHelpers.ReadLines(reader) .Where(s => !string.IsNullOrWhiteSpace(s)) .Select(s => { - var segs = s.Split(" "); + var segs = s.Split(' '); return ( Convert.ToDouble(segs[1]) * M_PI_180, Convert.ToDouble(segs[2]) * M_PI_180, @@ -98,7 +98,7 @@ public void Test_FromPoint_MatchesKnownIndexValue() { Point point = new(-110, 30); // Act - H3Index h3 = H3Index.FromPoint(point, 14); + var h3 = H3Index.FromPoint(point, 14); // Assert AssertKnownIndexValue(h3); @@ -130,10 +130,10 @@ public void Test_Equality() { [TestCaseSource(typeof(H3IndexTests), "ToGeoCoordTestCases")] public bool Test_Upstream_ToGeoCoord((H3Index, double, double)[] expectedValues) { // Act - GeoCoord[] actualCoords = expectedValues.Select(t => t.Item1.ToGeoCoord()).ToArray(); + var actualCoords = expectedValues.Select(t => t.Item1.ToGeoCoord()).ToArray(); // Assert - for (int i = 0; i < expectedValues.Length; i += 1) { + for (var i = 0; i < expectedValues.Length; i += 1) { var (_, expectedLatitude, expectedLongitude) = expectedValues[i]; var actualCoord = actualCoords[i]; var matches = Math.Abs(expectedLatitude - actualCoord.Latitude) < 0.000001 && @@ -150,10 +150,10 @@ public bool Test_Upstream_ToGeoCoord((H3Index, double, double)[] expectedValues) [TestCaseSource(typeof(H3IndexTests), "FromGeoCoordTestCases")] public bool Test_Upstream_FromGeoCoord((double, double, int, H3Index)[] expectedValues) { // Act - H3Index[] actualIndexes = expectedValues.Select(t => H3Index.FromGeoCoord((t.Item1, t.Item2), t.Item3)).ToArray(); + var actualIndexes = expectedValues.Select(t => H3Index.FromGeoCoord((t.Item1, t.Item2), t.Item3)).ToArray(); // Assert - for (int i = 0; i < expectedValues.Length; i += 1) { + for (var i = 0; i < expectedValues.Length; i += 1) { var expectedIndex = expectedValues[i].Item4; var actualIndex = actualIndexes[i]; if (expectedIndex != actualIndex) { @@ -196,8 +196,13 @@ public void Test_Upstream_IsValid_InvalidBaseCell() { [TestCase("15")] public void Test_Upstream_IsValid_InvalidMode(string modeValue) { // Arrange + #if NET48 + var mode = (Mode)Enum.Parse(typeof(Mode), modeValue, true); + #else + var mode = Enum.Parse(modeValue); + #endif var index = new H3Index { - Mode = Enum.Parse(modeValue) + Mode = mode }; // Act @@ -341,7 +346,7 @@ private static void AssertKnownIndexValue(H3Index h3) { Assert.AreEqual(0, h3.ReservedBits, "should have reserved bits of 0"); Assert.AreEqual(0, h3.HighBit, "should have high bit of 0"); - for (int r = 1; r <= 14; r += 1) { + for (var r = 1; r <= 14; r += 1) { Assert.AreEqual( TestHelpers.TestIndexDirectionPerResolution[r-1], h3.GetDirectionForResolution(r), diff --git a/test/H3.Test/TestHelpers.cs b/test/H3.Test/TestHelpers.cs index 324a3a9..b57a6ec 100644 --- a/test/H3.Test/TestHelpers.cs +++ b/test/H3.Test/TestHelpers.cs @@ -91,21 +91,21 @@ public static IEnumerable GetAllCellsForResolution(int resolution) { public static void AssertAll(H3Index[] expected, H3Index[] actual) { Assert.AreEqual(expected.Length, actual.Length, "should have same Length"); - for (int i = 0; i < expected.Length; i+= 1) { + for (var i = 0; i < expected.Length; i+= 1) { Assert.IsTrue(actual.Contains(expected[i]), $"index {expected[i]} should be found"); } } public static void AssertAll(ulong[] expected, H3Index[] actual) { Assert.AreEqual(expected.Length, actual.Length, "should have same Length"); - for (int i = 0; i < expected.Length; i += 1) { + for (var i = 0; i < expected.Length; i += 1) { Assert.IsTrue(expected[i] == actual[i], $"index {i} should be {expected[i]} not {actual[i]}"); } } public static IEnumerable GetTestData(Func matches) { var executingAssembly = Assembly.GetExecutingAssembly(); - string basePath = $"{executingAssembly.GetName().Name}.TestData"; + var basePath = $"{executingAssembly.GetName().Name}.TestData"; return executingAssembly.GetManifestResourceNames().Where(res => res.StartsWith(basePath) && matches(res)); }