From 1448d72c01d486aa9945fd41c74eba6d19931799 Mon Sep 17 00:00:00 2001 From: Anuragkillswitch <70265851+Anuragkillswitch@users.noreply.github.com> Date: Sat, 6 Jan 2024 13:03:32 +0530 Subject: [PATCH 1/5] feat: add support for cells to multipolygon and tests --- h3.go | 32 +++++++++++++++-- h3_test.go | 101 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 131 insertions(+), 2 deletions(-) diff --git a/h3.go b/h3.go index c0c871a..751b2dd 100644 --- a/h3.go +++ b/h3.go @@ -99,7 +99,10 @@ type ( // LinkedGeoPolygon is a linked-list of GeoPolygons. // TODO: not implemented. - LinkedGeoPolygon struct{} + LinkedGeoPolygon struct { + Data GeoPolygon + Next *LinkedGeoPolygon + } ) func NewLatLng(lat, lng float64) LatLng { @@ -246,8 +249,33 @@ func (p GeoPolygon) Cells(resolution int) []Cell { return PolygonToCells(p, resolution) } +func CellToGeoPolygon(cell Cell) GeoPolygon { + boundary := CellToBoundary(cell) + loop := make(GeoLoop, len(boundary)) + + for i, coord := range boundary { + loop[i] = LatLng{Lat: coord.Lat, Lng: coord.Lng} + } + + return GeoPolygon{GeoLoop: loop} +} + func CellsToMultiPolygon(cells []Cell) *LinkedGeoPolygon { - panic("not implemented") + var head, current *LinkedGeoPolygon + + for _, cell := range cells { + geoPoly := CellToGeoPolygon(cell) + + if head == nil { + head = &LinkedGeoPolygon{Data: geoPoly} + current = head + } else { + current.Next = &LinkedGeoPolygon{Data: geoPoly} + current = current.Next + } + } + + return head } // PointDistRads returns the "great circle" or "haversine" distance between diff --git a/h3_test.go b/h3_test.go index e3b4c58..956b9da 100644 --- a/h3_test.go +++ b/h3_test.go @@ -845,3 +845,104 @@ func copyCells(s []Cell) []Cell { return c } + +func TestCellsToMultiPolygon(t *testing.T) { + t.Parallel() + + // Hypothetical GeoLoops for test cells + validCellGeoLoop := GeoLoop{ + {Lat: 0.1, Lng: 0.1}, + {Lat: 0.1, Lng: 0.2}, + {Lat: 0.2, Lng: 0.2}, + {Lat: 0.2, Lng: 0.1}, + } + lineStartCellGeoLoop := GeoLoop{ + {Lat: 0.3, Lng: 0.3}, + {Lat: 0.3, Lng: 0.4}, + {Lat: 0.4, Lng: 0.4}, + {Lat: 0.4, Lng: 0.3}, + } + lineEndCellGeoLoop := GeoLoop{ + {Lat: 0.5, Lng: 0.5}, + {Lat: 0.5, Lng: 0.6}, + {Lat: 0.6, Lng: 0.6}, + {Lat: 0.6, Lng: 0.5}, + } + + // Test cases + testCases := []struct { + name string + cells []Cell + expected *LinkedGeoPolygon + }{ + { + name: "Single Cell", + cells: []Cell{validCell}, + expected: &LinkedGeoPolygon{ + Data: GeoPolygon{ + GeoLoop: validCellGeoLoop, + }, + Next: nil, + }, + }, + { + name: "Multiple Cells", + cells: []Cell{validCell, lineStartCell, lineEndCell}, + expected: &LinkedGeoPolygon{ + Data: GeoPolygon{ + GeoLoop: validCellGeoLoop, + }, + Next: &LinkedGeoPolygon{ + Data: GeoPolygon{ + GeoLoop: lineStartCellGeoLoop, + }, + Next: &LinkedGeoPolygon{ + Data: GeoPolygon{ + GeoLoop: lineEndCellGeoLoop, + }, + Next: nil, + }, + }, + }, + }, + // Additional test cases can be added here + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + result := CellsToMultiPolygon(tc.cells) + assertLinkedGeoPolygonEqual(t, tc.expected, result) + }) + } +} + +func assertLinkedGeoPolygonEqual(t *testing.T, expected, actual *LinkedGeoPolygon) { + t.Helper() + for expected != nil && actual != nil { + assertEqualGeoPolygon(t, expected.Data, actual.Data) + expected, actual = expected.Next, actual.Next + } + if expected != nil || actual != nil { + t.Errorf("LinkedGeoPolygons length mismatch") + } +} + +func assertEqualGeoPolygon(t *testing.T, expected, actual GeoPolygon) { + t.Helper() + assertEqualGeoLoop(t, expected.GeoLoop, actual.GeoLoop) + // Add checks for holes if necessary +} + +func assertEqualGeoLoop(t *testing.T, expected, actual GeoLoop) { + t.Helper() + if len(expected) != len(actual) { + t.Errorf("GeoLoops length mismatch: expected %d, got %d", len(expected), len(actual)) + return + } + for i, e := range expected { + a := actual[i] + if e.Lat != a.Lat || e.Lng != a.Lng { + t.Errorf("GeoLoop vertex mismatch at index %d: expected %v, got %v", i, e, a) + } + } +} From 38728a248d84e0d0759c695a568d05f43bc41edb Mon Sep 17 00:00:00 2001 From: Anuragkillswitch <70265851+Anuragkillswitch@users.noreply.github.com> Date: Sat, 6 Jan 2024 13:07:41 +0530 Subject: [PATCH 2/5] feat: remove TODO comments --- README.md | 4 ++-- h3.go | 1 - 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 4aa7267..0c4e49e 100644 --- a/README.md +++ b/README.md @@ -83,7 +83,7 @@ func ExampleLatLngToCell() { ## Bindings | C API | Go API | -| ---------------------------- | -------------------------------------------------- | +| ---------------------------- |----------------------------------------------------| | `latLngToCell` | `LatLngToCell`, `LatLng#Cell` | | `cellToLatLng` | `CellToLatLng`, `Cell#LatLng` | | `cellToBoundary` | `CellToBoundary`, `Cell#Boundary` | @@ -91,7 +91,7 @@ func ExampleLatLngToCell() { | `gridDiskDistances` | `GridDiskDistances`, `Cell#GridDiskDistances` | | `gridRingUnsafe` | N/A | | `polygonToCells` | `PolygonToCells`, `GeoPolygon#Cells` | -| `cellsToMultiPolygon` | TODO | +| `cellsToMultiPolygon` | `CellsToMultiPolygon` | | `degsToRads` | `DegsToRads` | | `radsToDegs` | `RadsToDegs` | | `greatCircleDistance` | `GreatCircleDistance* (3/3)` | diff --git a/h3.go b/h3.go index 751b2dd..091d083 100644 --- a/h3.go +++ b/h3.go @@ -98,7 +98,6 @@ type ( } // LinkedGeoPolygon is a linked-list of GeoPolygons. - // TODO: not implemented. LinkedGeoPolygon struct { Data GeoPolygon Next *LinkedGeoPolygon From 2478e3421aac0be4f3856c75fc972a38af0c9b3e Mon Sep 17 00:00:00 2001 From: Anuragkillswitch <70265851+Anuragkillswitch@users.noreply.github.com> Date: Sun, 7 Jan 2024 09:54:22 +0530 Subject: [PATCH 3/5] fix: test case --- h3_test.go | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/h3_test.go b/h3_test.go index 956b9da..5034eca 100644 --- a/h3_test.go +++ b/h3_test.go @@ -849,24 +849,30 @@ func copyCells(s []Cell) []Cell { func TestCellsToMultiPolygon(t *testing.T) { t.Parallel() - // Hypothetical GeoLoops for test cells + // Hypothetical GeoLoops for hexagonal test cells validCellGeoLoop := GeoLoop{ {Lat: 0.1, Lng: 0.1}, {Lat: 0.1, Lng: 0.2}, + {Lat: 0.15, Lng: 0.25}, {Lat: 0.2, Lng: 0.2}, {Lat: 0.2, Lng: 0.1}, + {Lat: 0.15, Lng: 0.05}, } lineStartCellGeoLoop := GeoLoop{ {Lat: 0.3, Lng: 0.3}, {Lat: 0.3, Lng: 0.4}, + {Lat: 0.35, Lng: 0.45}, {Lat: 0.4, Lng: 0.4}, {Lat: 0.4, Lng: 0.3}, + {Lat: 0.35, Lng: 0.25}, } lineEndCellGeoLoop := GeoLoop{ {Lat: 0.5, Lng: 0.5}, {Lat: 0.5, Lng: 0.6}, + {Lat: 0.55, Lng: 0.65}, {Lat: 0.6, Lng: 0.6}, {Lat: 0.6, Lng: 0.5}, + {Lat: 0.55, Lng: 0.45}, } // Test cases @@ -905,7 +911,6 @@ func TestCellsToMultiPolygon(t *testing.T) { }, }, }, - // Additional test cases can be added here } for _, tc := range testCases { From 827b53ed962a936d1fb6f54c1f6373ec11cb1c5a Mon Sep 17 00:00:00 2001 From: Anuragkillswitch <70265851+Anuragkillswitch@users.noreply.github.com> Date: Fri, 12 Jan 2024 11:57:27 +0530 Subject: [PATCH 4/5] feat: fix test --- h3_test.go | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/h3_test.go b/h3_test.go index 5034eca..9912906 100644 --- a/h3_test.go +++ b/h3_test.go @@ -848,8 +848,6 @@ func copyCells(s []Cell) []Cell { func TestCellsToMultiPolygon(t *testing.T) { t.Parallel() - - // Hypothetical GeoLoops for hexagonal test cells validCellGeoLoop := GeoLoop{ {Lat: 0.1, Lng: 0.1}, {Lat: 0.1, Lng: 0.2}, @@ -875,7 +873,6 @@ func TestCellsToMultiPolygon(t *testing.T) { {Lat: 0.55, Lng: 0.45}, } - // Test cases testCases := []struct { name string cells []Cell @@ -912,9 +909,10 @@ func TestCellsToMultiPolygon(t *testing.T) { }, }, } - for _, tc := range testCases { + tc := tc t.Run(tc.name, func(t *testing.T) { + t.Parallel() result := CellsToMultiPolygon(tc.cells) assertLinkedGeoPolygonEqual(t, tc.expected, result) }) From 27f1d3df641974f23d96aa4e37f8d9997acee055 Mon Sep 17 00:00:00 2001 From: Anuragkillswitch <70265851+Anuragkillswitch@users.noreply.github.com> Date: Fri, 12 Jan 2024 13:55:31 +0530 Subject: [PATCH 5/5] fix: test case tolerance for float precision --- h3_test.go | 53 ++++++++++++++++++++++++++++++++--------------------- 1 file changed, 32 insertions(+), 21 deletions(-) diff --git a/h3_test.go b/h3_test.go index 9912906..e6d6a25 100644 --- a/h3_test.go +++ b/h3_test.go @@ -848,29 +848,32 @@ func copyCells(s []Cell) []Cell { func TestCellsToMultiPolygon(t *testing.T) { t.Parallel() + validCellGeoLoop := GeoLoop{ - {Lat: 0.1, Lng: 0.1}, - {Lat: 0.1, Lng: 0.2}, - {Lat: 0.15, Lng: 0.25}, - {Lat: 0.2, Lng: 0.2}, - {Lat: 0.2, Lng: 0.1}, - {Lat: 0.15, Lng: 0.05}, + {Lat: 67.22475, Lng: -168.52301}, + {Lat: 67.14094, Lng: -168.62691}, + {Lat: 67.06725, Lng: -168.49491}, + {Lat: 67.07706, Lng: -168.25970}, + {Lat: 67.16056, Lng: -168.15480}, + {Lat: 67.23456, Lng: -168.28610}, } + lineStartCellGeoLoop := GeoLoop{ - {Lat: 0.3, Lng: 0.3}, - {Lat: 0.3, Lng: 0.4}, - {Lat: 0.35, Lng: 0.45}, - {Lat: 0.4, Lng: 0.4}, - {Lat: 0.4, Lng: 0.3}, - {Lat: 0.35, Lng: 0.25}, + {Lat: 37.77201, Lng: -122.41701}, + {Lat: 37.77369, Lng: -122.41594}, + {Lat: 37.77520, Lng: -122.41720}, + {Lat: 37.77502, Lng: -122.41953}, + {Lat: 37.77334, Lng: -122.42060}, + {Lat: 37.77183, Lng: -122.41934}, } + lineEndCellGeoLoop := GeoLoop{ - {Lat: 0.5, Lng: 0.5}, - {Lat: 0.5, Lng: 0.6}, - {Lat: 0.55, Lng: 0.65}, - {Lat: 0.6, Lng: 0.6}, - {Lat: 0.6, Lng: 0.5}, - {Lat: 0.55, Lng: 0.45}, + {Lat: 33.88098, Lng: -118.35439}, + {Lat: 33.88267, Lng: -118.35327}, + {Lat: 33.88429, Lng: -118.35445}, + {Lat: 33.88421, Lng: -118.35676}, + {Lat: 33.88251, Lng: -118.35788}, + {Lat: 33.88090, Lng: -118.35670}, } testCases := []struct { @@ -909,6 +912,7 @@ func TestCellsToMultiPolygon(t *testing.T) { }, }, } + for _, tc := range testCases { tc := tc t.Run(tc.name, func(t *testing.T) { @@ -933,19 +937,26 @@ func assertLinkedGeoPolygonEqual(t *testing.T, expected, actual *LinkedGeoPolygo func assertEqualGeoPolygon(t *testing.T, expected, actual GeoPolygon) { t.Helper() assertEqualGeoLoop(t, expected.GeoLoop, actual.GeoLoop) - // Add checks for holes if necessary } func assertEqualGeoLoop(t *testing.T, expected, actual GeoLoop) { t.Helper() + const tolerance = 1e-5 + if len(expected) != len(actual) { t.Errorf("GeoLoops length mismatch: expected %d, got %d", len(expected), len(actual)) return } + for i, e := range expected { a := actual[i] - if e.Lat != a.Lat || e.Lng != a.Lng { - t.Errorf("GeoLoop vertex mismatch at index %d: expected %v, got %v", i, e, a) + if !floatsAreClose(e.Lat, a.Lat, tolerance) || !floatsAreClose(e.Lng, a.Lng, tolerance) { + t.Errorf("GeoLoop vertex mismatch at index %d: expected (%.17f, %.17f), got (%.17f, %.17f)", + i, e.Lat, e.Lng, a.Lat, a.Lng) } } } + +func floatsAreClose(a, b, tolerance float64) bool { + return math.Abs(a-b) <= tolerance +}