Files
MrAlders0n 0e242574fb feat(meshmapper): import region boundaries from the Zones API
Imported boundaries override borderFile on the border map and for foreign marking, falling back to the manual border when MeshMapper has none.
2026-09-30 12:38:36 -04:00

142 lines
4.0 KiB
Go

// Copyright 2026 Beacon Contributors
// SPDX-License-Identifier: AGPL-3.0-or-later
package borders
import (
"math"
"testing"
"github.com/paulmach/orb"
)
func TestPossiblyForeign(t *testing.T) {
outer := orb.Ring{{10, 10}, {20, 10}, {20, 20}, {10, 20}, {10, 10}}
hole := orb.Ring{{12, 12}, {14, 12}, {14, 14}, {12, 14}, {12, 12}}
other := orb.Ring{{30, 10}, {40, 10}, {40, 20}, {30, 20}, {30, 10}}
local, err := New([]orb.Polygon{{outer, hole}, {other}})
if err != nil {
t.Fatal(err)
}
for _, tc := range []struct {
name string
role int16
lat, lng float64
want *bool
}{
{"inside", 2, 15, 15, new(false)},
{"outside", 2, 25, 25, new(true)},
{"second polygon", 2, 15, 35, new(false)},
{"hole", 2, 13, 13, new(true)},
{"exterior edge", 2, 15, 10, new(false)},
{"hole edge", 2, 13, 12, new(false)},
{"vertex", 2, 10, 10, new(false)},
{"companion", 1, 25, 25, nil},
{"room", 3, 25, 25, nil},
{"sensor", 4, 25, 25, nil},
{"null island is a position", 2, 0, 0, new(true)},
{"equator is valid", 2, 0, 25, new(true)},
{"prime meridian is valid", 2, 25, 0, new(true)},
{"invalid latitude", 2, 91, 25, nil},
{"invalid longitude", 2, 25, -181, nil},
{"NaN", 2, math.NaN(), 25, nil},
{"infinite", 2, 25, math.Inf(1), nil},
} {
t.Run(tc.name, func(t *testing.T) {
got := local.PossiblyForeign(tc.role, &tc.lat, &tc.lng)
if (got == nil) != (tc.want == nil) || got != nil && *got != *tc.want {
t.Fatalf("got %v, want %v", got, tc.want)
}
})
}
if local.PossiblyForeign(2, nil, new(15.0)) != nil || local.PossiblyForeign(2, new(15.0), nil) != nil {
t.Fatal("missing coordinate classified")
}
var disabled *Local
if disabled.PossiblyForeign(2, new(25.0), new(25.0)) != nil {
t.Fatal("disabled classification")
}
// The shared startup classifier owns its geometry; callers cannot mutate it.
outer[0] = orb.Point{99, 99}
if *local.PossiblyForeign(2, new(15.0), new(15.0)) {
t.Fatal("input mutation changed classifier")
}
}
func TestLocalBordersValidation(t *testing.T) {
for _, polys := range [][]orb.Polygon{
nil, {{}}, {{{{0, 0}, {1, 0}, {1, 1}}}},
{{{{0, 0}, {0, 0}, {0, 0}, {0, 0}}}},
{{{{179, 10}, {-179, 10}, {-179, 20}, {179, 20}, {179, 10}}}},
} {
if _, err := New(polys); err == nil {
t.Fatalf("accepted invalid/uncut geometry: %v", polys)
}
}
cut := []orb.Polygon{
{{{179, 10}, {180, 10}, {180, 20}, {179, 20}, {179, 10}}},
{{{-180, 10}, {-179, 10}, {-179, 20}, {-180, 20}, {-180, 10}}},
}
local, err := New(cut)
if err != nil {
t.Fatal(err)
}
for _, lon := range []float64{179.5, -179.5, 180, -180} {
if *local.PossiblyForeign(2, new(15.0), &lon) {
t.Fatalf("dateline island excluded: %v", lon)
}
}
if !*local.PossiblyForeign(2, new(15.0), new(0.0)) {
t.Fatal("uncut global region inferred")
}
}
func BenchmarkPossiblyForeign(b *testing.B) {
ring := make(orb.Ring, 10001)
for i := 0; i < 10000; i++ {
a := 2 * math.Pi * float64(i) / 10000
ring[i] = orb.Point{20 + math.Cos(a), 50 + math.Sin(a)}
}
ring[10000] = ring[0]
local, err := New([]orb.Polygon{{ring}})
if err != nil {
b.Fatal(err)
}
for _, tc := range []struct {
name string
lat, lng float64
}{{"inside_10k_vertices", 50, 20}, {"outside_bounds_10k_vertices", 0, 1}} {
b.Run(tc.name, func(b *testing.B) {
b.ReportAllocs()
for b.Loop() {
local.PossiblyForeign(2, &tc.lat, &tc.lng)
}
})
}
}
func TestLiveSwap(t *testing.T) {
var disabled *Live
if disabled.PossiblyForeign(2, new(15.0), new(15.0)) != nil {
t.Fatal("nil live classified")
}
live := NewLive(nil)
if disabled.Ready() || live.Ready() {
t.Fatal("ready without boundaries")
}
if live.PossiblyForeign(2, new(15.0), new(15.0)) != nil {
t.Fatal("empty live classified")
}
local, err := New([]orb.Polygon{{{{10, 10}, {20, 10}, {20, 20}, {10, 20}, {10, 10}}}})
if err != nil {
t.Fatal(err)
}
live.Store(local)
if !live.Ready() {
t.Fatal("not ready after store")
}
if got := live.PossiblyForeign(2, new(15.0), new(15.0)); got == nil || *got {
t.Fatal("stored classifier not used", got)
}
}