clone

an offline map explorer where each world, from a game map to a city's open data, ships as one .atlas file

archive/golden-reference
atlas / internal / blend / blend_test.go
blend_test.go 3.7K
1package blend23import (4 "fmt"5 "math"6 "testing"7)89// truth is the kind of transformation two renderings of one map actually10// differ by: a scale, an offset, and a whisper of shear.11var truth = Affine{12 AX: 1.38, BX: 0.02, CX: -1620,13 AY: 0.005, BY: 1.41, CY: -2050,14}1516// jitter is deterministic pin sloppiness: sources place the same shop a few17// pixels apart, and the fit has to see through that without any randomness18// in the test.19func jitter(seed int) (float64, float64) {20 return float64((seed*37)%17) - 8, float64((seed*53)%19) - 921}2223func anchors() ([]Anchor, []Anchor) {24 var donor, base []Anchor25 for i := range 60 {26 x := float64(500 + (i%10)*700)27 y := float64(400 + (i/10)*1200)28 donor = append(donor, Anchor{Title: fmt.Sprintf("Place %d", i), X: x, Y: y})29 bx, by := truth.Apply(x, y)30 jx, jy := jitter(i)31 base = append(base, Anchor{Title: fmt.Sprintf("place %d", i), X: bx + jx, Y: by + jy})32 }33 return donor, base34}3536func TestFitRecoversTheTransformation(t *testing.T) {37 donor, base := anchors()38 affine, report, err := Fit(donor, base)39 if err != nil {40 t.Fatal(err)41 }42 // The fit sees jittered pins, so it recovers the truth to within the43 // jitter's reach, not exactly.44 if math.Abs(affine.AX-truth.AX) > 0.01 || math.Abs(affine.BY-truth.BY) > 0.01 {45 t.Fatalf("scale drifted: %+v", affine)46 }47 if math.Abs(affine.CX-truth.CX) > 25 || math.Abs(affine.CY-truth.CY) > 25 {48 t.Fatalf("offset drifted: %+v", affine)49 }50 if report.MedianPx > 15 {51 t.Fatalf("fit did not close: %s", report)52 }5354 again, _, err := Fit(donor, base)55 if err != nil || again != affine {56 t.Fatalf("fit is not deterministic: %+v then %+v (%v)", affine, again, err)57 }58}5960func TestFitShedsOutliers(t *testing.T) {61 donor, base := anchors()62 // A handful of pins one source simply misplaced.63 for i := 0; i < 5; i++ {64 base[i*7].X += 90065 base[i*7].Y -= 70066 }67 affine, report, err := Fit(donor, base)68 if err != nil {69 t.Fatal(err)70 }71 if math.Abs(affine.AX-truth.AX) > 0.02 {72 t.Fatalf("outliers pulled the fit: %+v", affine)73 }74 if report.WorstPx > 60 {75 t.Fatalf("an outlier survived the trim: %s", report)76 }77}7879func TestFitRefusesThinEvidence(t *testing.T) {80 donor, base := anchors()81 if _, _, err := Fit(donor[:5], base[:5]); err == nil {82 t.Fatal("five anchors were enough for a transformation")83 }8485 // Ambiguous names carry no evidence: every anchor named alike must drop,86 // leaving too few.87 for i := range donor {88 if i >= 10 {89 donor[i].Title = "Duplicate"90 base[i].Title = "Duplicate"91 }92 }93 if _, _, err := Fit(donor, base); err == nil {94 t.Fatal("ambiguous names were treated as calibration points")95 }96}9798func TestFitRefusesNonsense(t *testing.T) {99 donor, base := anchors()100 // The same names placed with no geometric relationship at all.101 for i := range base {102 base[i].X = float64((i * 811) % 8000)103 base[i].Y = float64((i * 977) % 8000)104 }105 if _, _, err := Fit(donor, base); err == nil {106 t.Fatal("a fit that cannot close was still offered")107 }108}109110func TestNormalizeTitleFoldsFastTravelSpellings(t *testing.T) {111 cases := map[string]string{112 "Kennedy North - Fast Travel Point": "kennedy north",113 "Kennedy North Fast Travel": "kennedy north",114 "KENNEDY NORTH": "kennedy north",115 "Skippy, The Talking Gun ": "skippy the talking gun",116 }117 for given, want := range cases {118 if got := NormalizeTitle(given); got != want {119 t.Errorf("NormalizeTitle(%q) = %q, want %q", given, got, want)120 }121 }122}123124func TestInvertRoundTrips(t *testing.T) {125 inverse, ok := truth.Invert()126 if !ok {127 t.Fatal("the truth is singular")128 }129 x, y := truth.Apply(1234, 5678)130 backX, backY := inverse.Apply(x, y)131 if math.Abs(backX-1234) > 1e-6 || math.Abs(backY-5678) > 1e-6 {132 t.Fatalf("round trip landed at %.6f,%.6f", backX, backY)133 }134}