352 lines
13 KiB
Go
352 lines
13 KiB
Go
|
|
package geodata
|
||
|
|
|
||
|
|
import (
|
||
|
|
"errors"
|
||
|
|
"math"
|
||
|
|
"testing"
|
||
|
|
"time"
|
||
|
|
)
|
||
|
|
|
||
|
|
func TestOpenBoundarySweepContainsIntermediateArcs(t *testing.T) {
|
||
|
|
samples := []OpenBoundarySweepSample{
|
||
|
|
{Boundaries: [][]GeoPoint{{{Longitude: -2, Latitude: 0}, {Longitude: 2, Latitude: 0}}}},
|
||
|
|
{Boundaries: [][]GeoPoint{{
|
||
|
|
{Longitude: -1.5, Latitude: 0.5},
|
||
|
|
{Longitude: 0, Latitude: 3},
|
||
|
|
{Longitude: 1.5, Latitude: 0.5},
|
||
|
|
}}},
|
||
|
|
{Boundaries: [][]GeoPoint{{{Longitude: -1, Latitude: 1}, {Longitude: 1, Latitude: 1}}}},
|
||
|
|
}
|
||
|
|
polygons, err := OpenBoundarySweep(samples)
|
||
|
|
if err != nil {
|
||
|
|
t.Fatalf("OpenBoundarySweep: %v", err)
|
||
|
|
}
|
||
|
|
if len(polygons) != 1 {
|
||
|
|
t.Fatalf("polygon count = %d, want 1", len(polygons))
|
||
|
|
}
|
||
|
|
for _, point := range []GeoPoint{
|
||
|
|
{Longitude: -2, Latitude: 0},
|
||
|
|
{Longitude: 0, Latitude: 3},
|
||
|
|
{Longitude: 2, Latitude: 0},
|
||
|
|
} {
|
||
|
|
if !sweepPolygonContainsOrTouches(polygons[0], point) {
|
||
|
|
t.Fatalf("sweep polygon does not contain boundary point %+v", point)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestOpenBoundarySweepSeparatesGroupsAcrossClosedSample(t *testing.T) {
|
||
|
|
samples := []OpenBoundarySweepSample{
|
||
|
|
{Boundaries: [][]GeoPoint{{{Longitude: -20, Latitude: 0}, {Longitude: -10, Latitude: 0}}}},
|
||
|
|
{Boundaries: [][]GeoPoint{{{Longitude: -19, Latitude: 1}, {Longitude: -9, Latitude: 1}}}},
|
||
|
|
{Closed: true, Boundaries: [][]GeoPoint{{
|
||
|
|
{Longitude: 0, Latitude: 0}, {Longitude: 1, Latitude: 0}, {Longitude: 0, Latitude: 1},
|
||
|
|
}}},
|
||
|
|
{Boundaries: [][]GeoPoint{{{Longitude: 10, Latitude: 0}, {Longitude: 20, Latitude: 0}}}},
|
||
|
|
{Boundaries: [][]GeoPoint{{{Longitude: 11, Latitude: 1}, {Longitude: 21, Latitude: 1}}}},
|
||
|
|
}
|
||
|
|
polygons, err := OpenBoundarySweep(samples)
|
||
|
|
if err != nil {
|
||
|
|
t.Fatalf("OpenBoundarySweep: %v", err)
|
||
|
|
}
|
||
|
|
if len(polygons) != 2 {
|
||
|
|
t.Fatalf("polygon count=%d, want two groups separated by the closed sample", len(polygons))
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestMonotoneOpenBoundarySweepUsesEndpointTracks(t *testing.T) {
|
||
|
|
samples := []OpenBoundarySweepSample{
|
||
|
|
{Boundaries: [][]GeoPoint{{{Longitude: -2, Latitude: 0}, {Longitude: 2, Latitude: 0}}}},
|
||
|
|
{Boundaries: [][]GeoPoint{{{Longitude: -1.5, Latitude: 1}, {Longitude: 1.5, Latitude: 1}}}},
|
||
|
|
{Boundaries: [][]GeoPoint{{{Longitude: -1, Latitude: 2}, {Longitude: 1, Latitude: 2}}}},
|
||
|
|
}
|
||
|
|
polygons, err := MonotoneOpenBoundarySweep(samples)
|
||
|
|
if err != nil {
|
||
|
|
t.Fatalf("MonotoneOpenBoundarySweep: %v", err)
|
||
|
|
}
|
||
|
|
if len(polygons) != 1 {
|
||
|
|
t.Fatalf("polygon count=%d, want one monotone outline", len(polygons))
|
||
|
|
}
|
||
|
|
for _, point := range []GeoPoint{
|
||
|
|
{Longitude: -2, Latitude: 0},
|
||
|
|
{Longitude: 2, Latitude: 0},
|
||
|
|
{Longitude: -1.5, Latitude: 1},
|
||
|
|
{Longitude: 1.5, Latitude: 1},
|
||
|
|
{Longitude: -1, Latitude: 2},
|
||
|
|
{Longitude: 1, Latitude: 2},
|
||
|
|
} {
|
||
|
|
found := false
|
||
|
|
for _, candidate := range polygons[0] {
|
||
|
|
if SameGeoPoint(candidate, point) {
|
||
|
|
found = true
|
||
|
|
break
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if !found {
|
||
|
|
t.Fatalf("monotone outline is missing endpoint-track point %+v", point)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestMonotoneOpenBoundarySweepRejectsIntermediateBulge(t *testing.T) {
|
||
|
|
samples := []OpenBoundarySweepSample{
|
||
|
|
{Boundaries: [][]GeoPoint{{{Longitude: -2, Latitude: 0}, {Longitude: 2, Latitude: 0}}}},
|
||
|
|
{Boundaries: [][]GeoPoint{{
|
||
|
|
{Longitude: -1.5, Latitude: 0.5},
|
||
|
|
{Longitude: 0, Latitude: 3},
|
||
|
|
{Longitude: 1.5, Latitude: 0.5},
|
||
|
|
}}},
|
||
|
|
{Boundaries: [][]GeoPoint{{{Longitude: -1, Latitude: 1}, {Longitude: 1, Latitude: 1}}}},
|
||
|
|
}
|
||
|
|
if _, err := MonotoneOpenBoundarySweep(samples); err == nil {
|
||
|
|
t.Fatal("monotone sweep accepted an intermediate arc outside its endpoint tracks")
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestMonotoneOpenBoundarySweepRejectsEndpointTrackReversal(t *testing.T) {
|
||
|
|
samples := []OpenBoundarySweepSample{
|
||
|
|
{Boundaries: [][]GeoPoint{{{Longitude: 0, Latitude: 0}, {Longitude: 2, Latitude: 0}}}},
|
||
|
|
{Boundaries: [][]GeoPoint{{{Longitude: 1, Latitude: 0.5}, {Longitude: 3, Latitude: 0.5}}}},
|
||
|
|
{Boundaries: [][]GeoPoint{{{Longitude: 0.5, Latitude: 1}, {Longitude: 2.5, Latitude: 1}}}},
|
||
|
|
{Boundaries: [][]GeoPoint{{{Longitude: 0, Latitude: 1.5}, {Longitude: 2, Latitude: 1.5}}}},
|
||
|
|
}
|
||
|
|
if _, err := MonotoneOpenBoundarySweep(samples); !errors.Is(err, ErrOpenBoundaryEndpointTrackReversal) {
|
||
|
|
t.Fatalf("monotone sweep error=%v, want endpoint-track reversal", err)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestMonotoneOpenBoundarySweepAllowsSubKilometreEndpointNoise(t *testing.T) {
|
||
|
|
samples := []OpenBoundarySweepSample{
|
||
|
|
{Boundaries: [][]GeoPoint{{{Longitude: 0, Latitude: 0}, {Longitude: 2, Latitude: 0}}}},
|
||
|
|
{Boundaries: [][]GeoPoint{{{Longitude: 0.005, Latitude: 0.5}, {Longitude: 2.005, Latitude: 0.5}}}},
|
||
|
|
{Boundaries: [][]GeoPoint{{{Longitude: 0.001, Latitude: 1}, {Longitude: 2.001, Latitude: 1}}}},
|
||
|
|
{Boundaries: [][]GeoPoint{{{Longitude: 0.01, Latitude: 1.5}, {Longitude: 2.01, Latitude: 1.5}}}},
|
||
|
|
}
|
||
|
|
if _, err := MonotoneOpenBoundarySweep(samples); err != nil {
|
||
|
|
t.Fatalf("monotone sweep rejected sub-kilometre endpoint noise: %v", err)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestOpenBoundaryEndpointOutlinesRetainTracksAcrossIntermediateBulge(t *testing.T) {
|
||
|
|
samples := []OpenBoundarySweepSample{
|
||
|
|
{Boundaries: [][]GeoPoint{{{Longitude: 0, Latitude: 0}, {Longitude: 4, Latitude: 0}}}},
|
||
|
|
{Boundaries: [][]GeoPoint{{{Longitude: 0, Latitude: 1}, {Longitude: 2, Latitude: 4}, {Longitude: 4, Latitude: 1}}}},
|
||
|
|
{Boundaries: [][]GeoPoint{{{Longitude: 0, Latitude: 2}, {Longitude: 4, Latitude: 2}}}},
|
||
|
|
}
|
||
|
|
|
||
|
|
polygons, err := OpenBoundaryEndpointOutlines(samples)
|
||
|
|
if err != nil {
|
||
|
|
t.Fatalf("OpenBoundaryEndpointOutlines: %v", err)
|
||
|
|
}
|
||
|
|
if len(polygons) != 1 {
|
||
|
|
t.Fatalf("polygon count=%d, want 1", len(polygons))
|
||
|
|
}
|
||
|
|
for _, point := range []GeoPoint{
|
||
|
|
{Longitude: 0, Latitude: 0},
|
||
|
|
{Longitude: 0, Latitude: 1},
|
||
|
|
{Longitude: 0, Latitude: 2},
|
||
|
|
{Longitude: 4, Latitude: 0},
|
||
|
|
{Longitude: 4, Latitude: 1},
|
||
|
|
{Longitude: 4, Latitude: 2},
|
||
|
|
} {
|
||
|
|
if !sweepPolygonContainsOrTouches(polygons[0], point) {
|
||
|
|
t.Fatalf("endpoint outline does not contain track point %+v", point)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if sweepPolygonContainsOrTouches(polygons[0], GeoPoint{Longitude: 2, Latitude: 4}) {
|
||
|
|
t.Fatal("endpoint-only outline unexpectedly contains the intermediate bulge")
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestOpenBoundaryEndpointOutlinesRejectSingleSampleGroups(t *testing.T) {
|
||
|
|
samples := []OpenBoundarySweepSample{
|
||
|
|
{Boundaries: [][]GeoPoint{{{Longitude: 0, Latitude: 0}, {Longitude: 1, Latitude: 1}, {Longitude: 2, Latitude: 0}}}},
|
||
|
|
{Closed: true},
|
||
|
|
{Boundaries: [][]GeoPoint{{{Longitude: 10, Latitude: 0}, {Longitude: 11, Latitude: 1}, {Longitude: 12, Latitude: 0}}}},
|
||
|
|
}
|
||
|
|
if _, err := OpenBoundaryEndpointOutlines(samples); err == nil {
|
||
|
|
t.Fatal("endpoint outlines accepted groups without an endpoint track")
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestDecimateOpenBoundarySweepSamplesPreservesGroupsAndEndpoints(t *testing.T) {
|
||
|
|
var samples []OpenBoundarySweepSample
|
||
|
|
for group := 0; group < 2; group++ {
|
||
|
|
for index := 0; index < 10; index++ {
|
||
|
|
longitude := float64(group*100 + index)
|
||
|
|
samples = append(samples, OpenBoundarySweepSample{Boundaries: [][]GeoPoint{{
|
||
|
|
{Longitude: longitude, Latitude: 0},
|
||
|
|
{Longitude: longitude + 0.1, Latitude: 0.1},
|
||
|
|
{Longitude: longitude + 0.2, Latitude: 0.2},
|
||
|
|
}}})
|
||
|
|
}
|
||
|
|
if group == 0 {
|
||
|
|
samples = append(samples, OpenBoundarySweepSample{Closed: true})
|
||
|
|
}
|
||
|
|
}
|
||
|
|
decimated := DecimateOpenBoundarySweepSamples(samples, 4, 20)
|
||
|
|
if len(decimated) != 9 || !decimated[4].Closed {
|
||
|
|
t.Fatalf("decimated samples=%d separator=%v, want two groups of four", len(decimated), decimated[4].Closed)
|
||
|
|
}
|
||
|
|
for _, index := range []int{0, 3, 5, 8} {
|
||
|
|
if len(decimated[index].Boundaries) != 1 || len(decimated[index].Boundaries[0]) != 2 {
|
||
|
|
t.Fatalf("sample %d boundary was not spatially decimated: %#v", index, decimated[index].Boundaries)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
for index, wantLongitude := range map[int]float64{0: 0, 3: 9, 5: 100, 8: 109} {
|
||
|
|
if got := decimated[index].Boundaries[0][0].Longitude; got != wantLongitude {
|
||
|
|
t.Fatalf("sample %d first longitude=%.1f, want %.1f", index, got, wantLongitude)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestOpenBoundarySweepInnerCapsCloseOnlyNearSeparator(t *testing.T) {
|
||
|
|
samples := []OpenBoundarySweepSample{
|
||
|
|
{Boundaries: [][]GeoPoint{{
|
||
|
|
{Longitude: -2, Latitude: 0}, {Longitude: 0, Latitude: 2}, {Longitude: 2, Latitude: 0},
|
||
|
|
}}},
|
||
|
|
{Closed: true},
|
||
|
|
{Boundaries: [][]GeoPoint{{
|
||
|
|
{Longitude: 10, Latitude: 0}, {Longitude: 12, Latitude: 2}, {Longitude: 14, Latitude: 0},
|
||
|
|
}}},
|
||
|
|
}
|
||
|
|
if caps := OpenBoundarySweepInnerCaps(samples, 500); len(caps) != 2 {
|
||
|
|
t.Fatalf("inner caps=%d, want both sides of the closed separator", len(caps))
|
||
|
|
}
|
||
|
|
if caps := OpenBoundarySweepInnerCaps(samples, 100); len(caps) != 0 {
|
||
|
|
t.Fatalf("inner caps=%d, want large horizon gaps left open", len(caps))
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestOpenBoundarySweepStaysContinuousAcrossAntimeridian(t *testing.T) {
|
||
|
|
samples := []OpenBoundarySweepSample{
|
||
|
|
{Boundaries: [][]GeoPoint{{
|
||
|
|
{Longitude: 170, Latitude: 82},
|
||
|
|
{Longitude: -170, Latitude: 82},
|
||
|
|
}}},
|
||
|
|
{Boundaries: [][]GeoPoint{{
|
||
|
|
{Longitude: 172, Latitude: 84},
|
||
|
|
{Longitude: -168, Latitude: 84},
|
||
|
|
}}},
|
||
|
|
{Boundaries: [][]GeoPoint{{
|
||
|
|
{Longitude: 175, Latitude: 86},
|
||
|
|
{Longitude: -165, Latitude: 86},
|
||
|
|
}}},
|
||
|
|
}
|
||
|
|
polygons, err := OpenBoundarySweep(samples)
|
||
|
|
if err != nil {
|
||
|
|
t.Fatalf("OpenBoundarySweep across antimeridian: %v", err)
|
||
|
|
}
|
||
|
|
if len(polygons) != 1 {
|
||
|
|
t.Fatalf("antimeridian sweep polygon count=%d, want one", len(polygons))
|
||
|
|
}
|
||
|
|
for _, sample := range samples {
|
||
|
|
for _, point := range sample.Boundaries[0] {
|
||
|
|
if !sphericalPolygonContainsOrTouches(polygons[0], point) {
|
||
|
|
t.Fatalf("sweep excludes sampled endpoint %+v", point)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestSweepPointInPolygonIncludesBoundary(t *testing.T) {
|
||
|
|
polygon := []GeoPoint{
|
||
|
|
{Longitude: 120, Latitude: -72},
|
||
|
|
{Longitude: 135, Latitude: -72},
|
||
|
|
{Longitude: 135, Latitude: -68},
|
||
|
|
{Longitude: 120, Latitude: -68},
|
||
|
|
}
|
||
|
|
for _, point := range []GeoPoint{
|
||
|
|
{Longitude: 120, Latitude: -72},
|
||
|
|
{Longitude: 127.5, Latitude: -72},
|
||
|
|
{Longitude: 135, Latitude: -70},
|
||
|
|
{Longitude: 127.5, Latitude: -70},
|
||
|
|
} {
|
||
|
|
if !sweepPointInPolygon(polygon, point) {
|
||
|
|
t.Fatalf("boundary/interior point %#v was rejected", point)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if sweepPointInPolygon(polygon, GeoPoint{Longitude: 127.5, Latitude: -73}) {
|
||
|
|
t.Fatal("outside point was accepted")
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func sweepPolygonContainsOrTouches(polygon []GeoPoint, point GeoPoint) bool {
|
||
|
|
inside := false
|
||
|
|
for current, previous := 0, len(polygon)-1; current < len(polygon); previous, current = current, current+1 {
|
||
|
|
first, second := polygon[previous], polygon[current]
|
||
|
|
cross := (point.Longitude-first.Longitude)*(second.Latitude-first.Latitude) -
|
||
|
|
(point.Latitude-first.Latitude)*(second.Longitude-first.Longitude)
|
||
|
|
if math.Abs(cross) <= 1e-9 && point.Longitude >= math.Min(first.Longitude, second.Longitude)-1e-9 &&
|
||
|
|
point.Longitude <= math.Max(first.Longitude, second.Longitude)+1e-9 &&
|
||
|
|
point.Latitude >= math.Min(first.Latitude, second.Latitude)-1e-9 &&
|
||
|
|
point.Latitude <= math.Max(first.Latitude, second.Latitude)+1e-9 {
|
||
|
|
return true
|
||
|
|
}
|
||
|
|
if (first.Latitude > point.Latitude) != (second.Latitude > point.Latitude) &&
|
||
|
|
point.Longitude < (second.Longitude-first.Longitude)*(point.Latitude-first.Latitude)/
|
||
|
|
(second.Latitude-first.Latitude)+first.Longitude {
|
||
|
|
inside = !inside
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return inside
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestSweepBridgeTouchingPolygonsBridgesNearPair(t *testing.T) {
|
||
|
|
polygons := [][]GeoPoint{
|
||
|
|
{{0, 0}, {1, 0}, {1, 1}, {0, 1}},
|
||
|
|
{{0, 0.00005}, {1, 0.00005}, {1, 1.00005}, {0, 1.00005}},
|
||
|
|
}
|
||
|
|
result, err := sweepBridgeTouchingPolygons(polygons)
|
||
|
|
if err != nil {
|
||
|
|
t.Fatalf("sweepBridgeTouchingPolygons: %v", err)
|
||
|
|
}
|
||
|
|
if len(result) != 1 {
|
||
|
|
t.Fatalf("ring count=%d, want one bridged ring", len(result))
|
||
|
|
}
|
||
|
|
for _, point := range []GeoPoint{{Longitude: 0.5, Latitude: 0.5}, {Longitude: 0.5, Latitude: 0.00002}} {
|
||
|
|
if !sweepPointInPolygon(result[0], point) {
|
||
|
|
t.Fatalf("bridged ring misses %+v", point)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestSweepBridgeTouchingPolygonsSeparatesDistantRings(t *testing.T) {
|
||
|
|
polygons := [][]GeoPoint{
|
||
|
|
{{0, 0}, {1, 0}, {1, 1}, {0, 1}},
|
||
|
|
{{0, 0.001}, {1, 0.001}, {1, 1.001}, {0, 1.001}},
|
||
|
|
}
|
||
|
|
result, err := sweepBridgeTouchingPolygons(polygons)
|
||
|
|
if err != nil {
|
||
|
|
t.Fatalf("sweepBridgeTouchingPolygons: %v", err)
|
||
|
|
}
|
||
|
|
if len(result) != 2 {
|
||
|
|
t.Fatalf("ring count=%d, want two rings a kilometre apart", len(result))
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestSweepBridgeTouchingPolygonsScansManyRingsQuickly(t *testing.T) {
|
||
|
|
rings := make([][]GeoPoint, 0, 120)
|
||
|
|
for row := 0; row < 10; row++ {
|
||
|
|
for col := 0; col < 12; col++ {
|
||
|
|
rings = append(rings, SphericalCircle(GeoPoint{
|
||
|
|
Longitude: 15 * float64(col),
|
||
|
|
Latitude: 10 + 5*float64(row),
|
||
|
|
}, 0.5, 128))
|
||
|
|
}
|
||
|
|
}
|
||
|
|
rings[119] = SphericalCircle(GeoPoint{Longitude: 150.0005, Latitude: 55}, 0.5, 128)
|
||
|
|
start := time.Now()
|
||
|
|
result, err := sweepBridgeTouchingPolygons(rings)
|
||
|
|
elapsed := time.Since(start)
|
||
|
|
if err != nil {
|
||
|
|
t.Fatalf("sweepBridgeTouchingPolygons: %v", err)
|
||
|
|
}
|
||
|
|
if len(result) != len(rings) {
|
||
|
|
t.Fatalf("ring count=%d, want %d", len(result), len(rings))
|
||
|
|
}
|
||
|
|
if elapsed > 2*time.Second {
|
||
|
|
t.Fatalf("scanning 120 rings took %v", elapsed)
|
||
|
|
}
|
||
|
|
}
|