1package compose23import (4 "encoding/json"5 "fmt"6 "testing"78 "github.com/FelineStateMachine/atlas/internal/generate/curation"9 "github.com/FelineStateMachine/atlas/internal/generate/doc"10 "github.com/FelineStateMachine/atlas/internal/generate/tiles"11)1213// splitGrid is the shared corpus window, so a test's world pixels are the ones14// every other part of the lane measures in.15func splitGrid() surfaceGrid {16 return surfaceGrid{SourceZoom: 13, FirstTile: 4064, TileSize: 256, Size: 8192}17}1819// squareAt outlines a box of world pixels as an area feature's geometry, so a20// test can say where a piece of a sheet is drawn without spelling degrees.21func squareAt(x, y, size float64, grid surfaceGrid) []doc.Geometry {22 corners := [][2]float64{23 {x, y}, {x + size, y}, {x + size, y + size}, {x, y + size}, {x, y},24 }25 ring := make([][2]float64, 0, len(corners))26 for _, corner := range corners {27 ring = append(ring, [2]float64{28 unprojectLng(corner[0], grid),29 unprojectLat(corner[1], grid),30 })31 }32 raw, err := json.Marshal([][][2]float64{ring})33 if err != nil {34 panic(err)35 }36 return []doc.Geometry{{Type: "Polygon", Coordinates: raw}}37}3839func pointAt(id int64, member int64, x, y float64, grid surfaceGrid) composedFeature {40 return composedFeature{Feature: doc.Feature{41 ID: id,42 Title: fmt.Sprintf("pin %d", id),43 Member: member,44 At: &doc.Position{45 Lat: unprojectLat(y, grid),46 Lng: unprojectLng(x, grid),47 },48 }}49}5051// sheet is two squares side by side with a gap between them, each with two pins52// on it: the smallest thing that is really two places.53func sheet(grid surfaceGrid) composedWorld {54 return composedWorld{55 ID: 7,56 Slug: "sheet",57 Title: "Sheet",58 Lenses: []lens{{Name: "Default", Tiles: "sheet"}},59 Pyramids: []tiles.Pyramid{{Name: "sheet", Stamp: "stamp"}},60 Collections: []composedCollection{61 {62 ID: 1,63 Title: "Pins",64 Kind: doc.KindPoint,65 Visible: true,66 Features: []composedFeature{67 pointAt(11, 100, 600, 600, grid),68 pointAt(12, 100, 1400, 1400, grid),69 pointAt(21, 200, 4600, 600, grid),70 pointAt(22, 200, 5400, 1400, grid),71 },72 },73 {74 ID: 2,75 Key: "regions",76 Title: "Regions",77 Kind: doc.KindArea,78 Visible: true,79 Features: []composedFeature{80 {Feature: doc.Feature{ID: 100, Title: "West", Geometry: squareAt(512, 512, 1024, grid)}},81 {Feature: doc.Feature{ID: 200, Title: "East", Geometry: squareAt(4096, 512, 2048, grid)}},82 {Feature: doc.Feature{83 ID: 201, Title: "East Quarter", Parent: 200,84 Geometry: squareAt(4096, 512, 512, grid),85 }},86 },87 },88 },89 }90}9192func TestSplitLeavesAnUncuratedSheetWhole(t *testing.T) {93 grid := splitGrid()94 pieces, err := splitWorld(sheet(grid), curation.ShardNone, grid)95 if err != nil {96 t.Fatal(err)97 }98 if len(pieces) != 1 || pieces[0].Slug != "sheet" {99 t.Fatalf("an uncurated sheet became %d worlds", len(pieces))100 }101 if pieces[0].Lenses[0].Bounds != nil {102 t.Errorf("an uncurated sheet had its raster clipped")103 }104}105106func TestSplitIntoWorlds(t *testing.T) {107 grid := splitGrid()108 pieces, err := splitWorld(sheet(grid), curation.ShardIntoWorlds, grid)109 if err != nil {110 t.Fatal(err)111 }112 if len(pieces) != 2 {113 t.Fatalf("two places on one sheet became %d worlds", len(pieces))114 }115116 // The largest piece leads and keeps the sheet's identity; the rest are named117 // after it so they sort together in the picker.118 if pieces[0].Slug != "sheet" || pieces[0].Parent != "" {119 t.Errorf("the leading piece is %s (parent %q), want the sheet itself", pieces[0].Slug, pieces[0].Parent)120 }121 if pieces[1].Slug != "sheet-west" || pieces[1].Parent != "sheet" {122 t.Errorf("the second piece is %s (parent %q), want sheet-west under sheet",123 pieces[1].Slug, pieces[1].Parent)124 }125 if pieces[1].Title != "Sheet — West" {126 t.Errorf("the second piece is titled %q", pieces[1].Title)127 }128 if pieces[1].ID != 100 {129 t.Errorf("a piece carries id %d, want the area it was cut around", pieces[1].ID)130 }131132 // Each piece keeps its own pins and only its own.133 for _, want := range []struct {134 world string135 ids []int64136 }{137 {"sheet", []int64{21, 22}},138 {"sheet-west", []int64{11, 12}},139 } {140 var got []int64141 for _, part := range pieces {142 if part.Slug != want.world {143 continue144 }145 for _, collection := range part.Collections {146 if collection.Kind != doc.KindPoint {147 continue148 }149 for _, feature := range collection.Features {150 got = append(got, feature.ID)151 }152 }153 }154 if fmt.Sprint(got) != fmt.Sprint(want.ids) {155 t.Errorf("%s holds pins %v, want %v", want.world, got, want.ids)156 }157 }158159 // The outline a piece was cut around is dropped; a descendant survives.160 var areas []int64161 for _, collection := range pieces[0].Collections {162 if collection.Kind != doc.KindArea {163 continue164 }165 for _, feature := range collection.Features {166 areas = append(areas, feature.ID)167 }168 }169 if fmt.Sprint(areas) != fmt.Sprint([]int64{201}) {170 t.Errorf("the leading piece keeps areas %v, want only the descendant 201", areas)171 }172173 // The raster is clipped to the piece, and the ground it covers travels174 // beside the window because they are not the same rectangle.175 window := pieces[1].Lenses[0].Bounds176 ground := pieces[1].Lenses[0].Surface177 if window == nil || ground == nil {178 t.Fatalf("a piece's lens carries no window or no ground")179 }180 if window.X > ground.X || window.Y > ground.Y ||181 window.X+window.Width < ground.X+ground.Width ||182 window.Y+window.Height < ground.Y+ground.Height {183 t.Errorf("a piece's ground %+v reaches outside its window %+v", *ground, *window)184 }185}186187func TestSplitIntoLenses(t *testing.T) {188 grid := splitGrid()189 pieces, err := splitWorld(sheet(grid), curation.ShardIntoLenses, grid)190 if err != nil {191 t.Fatal(err)192 }193 if len(pieces) != 1 {194 t.Fatalf("a sheet split into lenses became %d worlds, want one", len(pieces))195 }196 world := pieces[0]197 if len(world.Lenses) != 2 {198 t.Fatalf("a sheet of two places offers %d lenses", len(world.Lenses))199 }200 if len(world.Pyramids) != len(world.Lenses) {201 t.Fatalf("%d lenses draw from %d pyramids", len(world.Lenses), len(world.Pyramids))202 }203 // Lenses read top to bottom, and both pieces sit at the same height here, so204 // the tie breaks on title.205 if world.Lenses[0].Name != "East" || world.Lenses[1].Name != "West" {206 t.Errorf("lenses are named %q then %q", world.Lenses[0].Name, world.Lenses[1].Name)207 }208 if world.Lenses[0].Shard != 200 || world.Lenses[1].Shard != 100 {209 t.Errorf("lenses carry shards %d and %d", world.Lenses[0].Shard, world.Lenses[1].Shard)210 }211 // Every feature says which piece it belongs to, so a reader can show one at212 // a time without the world underneath it changing.213 for _, collection := range world.Collections {214 for _, feature := range collection.Features {215 if feature.Shard == 0 {216 t.Errorf("feature %d belongs to no piece", feature.ID)217 }218 }219 }220}221222func TestSplitRefusesAPointBelongingToNoArea(t *testing.T) {223 grid := splitGrid()224 world := sheet(grid)225 world.Collections[0].Features = append(world.Collections[0].Features,226 pointAt(99, 0, 2000, 2000, grid))227 if _, err := splitWorld(world, curation.ShardIntoWorlds, grid); err == nil {228 t.Fatal("a sheet with a homeless point split anyway, so the point vanished")229 }230}231232func TestSplitRefusesASheetOfOnePlace(t *testing.T) {233 grid := splitGrid()234 world := sheet(grid)235 // One family, so there is nothing to take apart.236 world.Collections[1].Features = world.Collections[1].Features[1:]237 world.Collections[0].Features = world.Collections[0].Features[2:]238 if _, err := splitWorld(world, curation.ShardIntoWorlds, grid); err == nil {239 t.Fatal("a sheet holding one place split anyway")240 }241}242243// A point drawn on a piece that does not claim it moves to the piece it is244// actually on: where it sits is not in doubt, and the claim is the mistake.245func TestSplitRehomesAStrayPoint(t *testing.T) {246 grid := splitGrid()247 world := sheet(grid)248 world.Collections[0].Features[0] = pointAt(11, 200, 600, 600, grid)249250 pieces, err := splitWorld(world, curation.ShardIntoWorlds, grid)251 if err != nil {252 t.Fatal(err)253 }254 for _, part := range pieces {255 if part.Slug != "sheet-west" {256 continue257 }258 for _, collection := range part.Collections {259 for _, feature := range collection.Features {260 if feature.ID == 11 {261 return262 }263 }264 }265 }266 t.Error("a point drawn on the west piece stayed with the east one that claimed it")267}