package plates import ( "math" "testing" ) // A painted layer and its legend, built by hand: two plates split at a quarter and three quarters of the way // round, driven into each other along X with no spin. func paintedStripes(w, h int) ([]uint8, *PaintLegend) { lg := &PaintLegend{ WarnDistance: 60, Plates: []PaintPlate{ {Name: "west", RGB: [3]int{200, 60, 60}, SpeedCmYr: 2, HeadingDeg: 90}, // due east {Name: "east", RGB: [3]int{60, 60, 200}, SpeedCmYr: 2, HeadingDeg: 270}, // due west }, } px := make([]uint8, w*h*3) for y := range h { for x := range w { id := 0 if x >= w/4 && x < 3*w/4 { id = 1 } o := (y*w + x) * 3 c := lg.Plates[id].RGB px[o], px[o+1], px[o+2] = uint8(c[0]), uint8(c[1]), uint8(c[2]) } } return px, lg } func TestAPaintedLayerBecomesAPlanet(t *testing.T) { p := testPlanet(t) px, lg := paintedStripes(400, 200) m, match, err := FromPainting(p, Default(), lg, px, 400, 200, allLandAt) if err != nil { t.Fatalf("from painting: %v", err) } if match.Far != 0 { t.Errorf("%d of %d sampled cells did not match a plate colour", match.Far, match.Cells) } if len(m.Plates) != 2 { t.Fatalf("%d plates from a two-colour legend", len(m.Plates)) } if len(m.Boundaries) != 2 { t.Fatalf("%d boundaries; two stripes on a cylinder make two contacts", len(m.Boundaries)) } // The same invariant the generated path has: with a pure translation one margin closes and the other // opens, by the same amount. Two plates at 2 cm/yr closing head-on give 4 cm/yr. means := sortedMeans(m.Boundaries) if means[0] >= 0 || means[1] <= 0 { t.Fatalf("closing rates %.4g and %.4g; one of each is the only arrangement possible", means[0], means[1]) } if got := math.Abs(means[1]); math.Abs(got-0.04) > 1e-3 { t.Errorf("painted plates at 2 cm/yr each close at %.4g m/yr, want 0.04", got) } // Both painted as land, so both are continental and the closing margin is a collision. if got, want := kinds(m.Boundaries), []string{"collision", "rift"}; !sameStrings(got, want) { t.Errorf("got %v, want %v", got, want) } } func allLandAt(xM, yM float64) bool { return true } func TestAHeadingIsACompassBearing(t *testing.T) { p := testPlanet(t) px, lg := paintedStripes(400, 200) // 0 points at the top of the image, which is -Y; 90 to the right, which is +X. lg.Plates[0].HeadingDeg = 0 lg.Plates[1].HeadingDeg = 90 m, _, err := FromPainting(p, Default(), lg, px, 400, 200, allLandAt) if err != nil { t.Fatalf("from painting: %v", err) } north, east := m.Plates[0], m.Plates[1] if math.Abs(north.TransXM) > 1e-9 || north.TransYM >= 0 { t.Errorf("heading 0 gives (%.4g, %.4g); it should point at the top of the map", north.TransXM, north.TransYM) } if math.Abs(east.TransYM) > 1e-9 || east.TransXM <= 0 { t.Errorf("heading 90 gives (%.4g, %.4g); it should point to the right of the map", east.TransXM, east.TransYM) } if got := math.Hypot(east.TransXM, east.TransYM); math.Abs(got-0.02) > 1e-9 { t.Errorf("2 cm/yr came out as %.4g m/yr", got) } } func TestThePaintingCanOverruleTheLandMask(t *testing.T) { p := testPlanet(t) px, lg := paintedStripes(400, 200) // Every cell is land, so both plates are continental and the closing margin is a collision. m, _, err := FromPainting(p, Default(), lg, px, 400, 200, allLandAt) if err != nil { t.Fatalf("from painting: %v", err) } if !m.Plates[0].Continental || !m.Plates[1].Continental { t.Fatal("a planet of land has an oceanic plate on it") } // The legend says otherwise about one of them, and a legend that bothers to say so wins. oceanic := false lg.Plates[1].Continental = &oceanic m, _, err = FromPainting(p, Default(), lg, px, 400, 200, allLandAt) if err != nil { t.Fatalf("from painting: %v", err) } if m.Plates[1].Continental { t.Error("the legend called plate 1 oceanic and the land mask overruled it") } // And the consequence is the point of the override: the same margin is now a subduction zone. if got, want := kinds(m.Boundaries), []string{"ridge", "subduction"}; !sameStrings(got, want) { t.Errorf("got %v, want %v", got, want) } } func TestACentroidIsMeasuredTheShortWayRound(t *testing.T) { p := testPlanet(t) const w, h = 400, 200 lg := &PaintLegend{ WarnDistance: 60, Plates: []PaintPlate{ {Name: "seam", RGB: [3]int{200, 60, 60}, SpeedCmYr: 2, HeadingDeg: 90}, {Name: "rest", RGB: [3]int{60, 60, 200}, SpeedCmYr: 2, HeadingDeg: 270}, }, } // Plate 0 is painted across the meridian: the left eighth and the right eighth of the image. Its centre // is the seam, and an arithmetic mean of those columns would put it on the far side of the planet - and // with it the pole it spins about. px := make([]uint8, w*h*3) for y := range h { for x := range w { id := 1 if x < w/8 || x >= 7*w/8 { id = 0 } o := (y*w + x) * 3 c := lg.Plates[id].RGB px[o], px[o+1], px[o+2] = uint8(c[0]), uint8(c[1]), uint8(c[2]) } } m, _, err := FromPainting(p, Default(), lg, px, w, h, allLandAt) if err != nil { t.Fatalf("from painting: %v", err) } circ := p.CircumferenceM() got := m.Plates[0].SiteXM // Near the meridian, measured the short way round: either just above 0 or just below the circumference. if d := math.Abs(wrapDelta(got, circ)); d > circ/16 { t.Errorf("the seam-straddling plate's centre is at %.0f m of %.0f; it should be near the meridian, "+ "and the arithmetic mean would have put it near %.0f", got, circ, circ/2) } } func TestAProposalReadsBackAsTheSamePlanet(t *testing.T) { p := testPlanet(t) cfg := Default() cfg.Count = 6 land := func(xM, yM float64) bool { return yM > 4000 && yM < 14000 } made, err := Build(p, 3630, cfg, land) if err != nil { t.Fatalf("build: %v", err) } // The round trip is what makes Propose worth having: what it writes has to be a layer that comes back as // the planet it was written from, or an author's first edit starts from something that was never true. px, w, h, lg := made.Propose(1600) read, match, err := FromPainting(p, cfg, lg, px, w, h, land) if err != nil { t.Fatalf("read back: %v", err) } if match.Far != 0 { t.Errorf("%d of %d cells of its own proposal did not match its own legend", match.Far, match.Cells) } if len(read.Plates) != len(made.Plates) { t.Fatalf("%d plates written, %d read back", len(made.Plates), len(read.Plates)) } // The motions survive the trip through the legend's cm/yr and degrees. Rounded when written - a tenth of // a cm/yr and a whole degree - so the tolerance is the rounding, not a fudge. for i := range made.Plates { a, b := made.Plates[i], read.Plates[i] if d := math.Hypot(a.TransXM-b.TransXM, a.TransYM-b.TransYM); d > 0.0006 { t.Errorf("plate %d drifts %.5g m/yr differently after the round trip", i, d) } if a.Continental != b.Continental { t.Errorf("plate %d was %v continental and reads back %v", i, a.Continental, b.Continental) } } // And the tectonics: the same margins doing the same things. Not vertex-for-vertex - the proposal is a // raster at 1600 px and the model was traced at the tectonic grid - but the same boundaries by count and // by what each one is. if len(read.Boundaries) != len(made.Boundaries) { t.Errorf("%d boundaries written, %d read back", len(made.Boundaries), len(read.Boundaries)) } if got, want := kinds(read.Boundaries), kinds(made.Boundaries); !sameStrings(got, want) { t.Errorf("margins read back as %v, were %v", got, want) } } func TestALegendThatCannotBeAPlanetIsRefused(t *testing.T) { one := &PaintLegend{Plates: []PaintPlate{{Name: "only", RGB: [3]int{1, 2, 3}}}} if err := one.validate("test"); err == nil { t.Error("a planet in one plate was accepted; it has no boundaries") } dup := &PaintLegend{Plates: []PaintPlate{ {Name: "a", RGB: [3]int{1, 2, 3}}, {Name: "b", RGB: [3]int{1, 2, 3}}, }} if err := dup.validate("test"); err == nil { t.Error("two plates sharing a colour were accepted; a colour is one plate") } }