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 / basemap / basemap_test.go
basemap_test.go 5.4K
1package basemap23import (4 "bytes"5 "image"6 "testing"7)89// A square with a square hole, big enough to land across several tiles at10// the deepest level of a small pyramid.11func fixtureFeatures() []Feature {12 outer := [][2]float64{{1000, 1000}, {5000, 1000}, {5000, 5000}, {1000, 5000}}13 hole := [][2]float64{{2000, 2000}, {2000, 3000}, {3000, 3000}, {3000, 2000}}14 street := [][2]float64{{500, 4000}, {7500, 4200}}15 trail := [][2]float64{{500, 6000}, {7500, 6100}}16 return []Feature{17 {Role: RolePark, Rings: [][][2]float64{outer, hole}},18 {Role: RoleStreet, Lines: [][][2]float64{street}, Emphasis: 1.8},19 {Role: RoleTrail, Lines: [][][2]float64{trail}},20 {Role: RoleBoundary, Lines: [][][2]float64{{{800, 800}, {7400, 800}, {7400, 7400}, {800, 7400}, {800, 800}}}},21 }22}2324// The same features must render and encode to the same bytes, twice over --25// the property the crawler's hash-compare writes stand on.26func TestRenderIsDeterministic(t *testing.T) {27 first := NewRenderer(fixtureFeatures(), 3)28 second := NewRenderer(fixtureFeatures(), 3)29 for _, tile := range [][3]int{{3, 1, 1}, {3, 2, 2}, {3, 0, 0}} {30 a := first.Tile(tile[0], tile[1], tile[2])31 b := second.Tile(tile[0], tile[1], tile[2])32 if !bytes.Equal(a.Pix, b.Pix) {33 t.Fatalf("tile %v pixels differ between renders", tile)34 }35 pngA, err := EncodePNG(a)36 if err != nil {37 t.Fatal(err)38 }39 pngB, err := EncodePNG(b)40 if err != nil {41 t.Fatal(err)42 }43 if !bytes.Equal(pngA, pngB) {44 t.Fatalf("tile %v encodes differently between renders", tile)45 }46 }47}4849// A hole in a polygon must keep the background, however the source wound50// its rings.51func TestHolesStayGround(t *testing.T) {52 // Both rings wound the same way: normalization must fix the hole.53 outer := [][2]float64{{1000, 1000}, {5000, 1000}, {5000, 5000}, {1000, 5000}}54 hole := [][2]float64{{2000, 2000}, {3000, 2000}, {3000, 3000}, {2000, 3000}}55 renderer := NewRenderer([]Feature{{Role: RolePark, Rings: [][][2]float64{outer, hole}}}, 1)5657 // Zoom 1: the world is 2x2 tiles of 4096 world px, so tile (0,0) shows58 // the square and its hole. World px 2500,2500 (hole middle) is tile px59 // 156; world px 1500,1500 (park ground) is tile px 93.60 tile := renderer.Tile(1, 0, 0)61 background := tile.NRGBAAt(156, 156)62 if background != Background {63 t.Fatalf("hole middle is %v, not background", background)64 }65 park := tile.NRGBAAt(93, 93)66 if park != styles[RolePark].Fill {67 t.Fatalf("park ground is %v", park)68 }69}7071// Adjacent tiles must continue a shape exactly along their shared edge:72// the same column of pixels, whichever tile drew it.73func TestNeighbouringTilesAgreeAtTheSeam(t *testing.T) {74 renderer := NewRenderer(fixtureFeatures(), 3)75 // At zoom 3 the world is 8 tiles of 1024 world px. The street crosses76 // the boundary between tiles (1,3) and (2,3); compare the last column77 // of the left tile with the first column of the right one. Antialiased78 // coverage at a shared edge comes from the same geometry through the79 // same transform, so the columns must match exactly.80 left := renderer.Tile(3, 1, 3)81 right := renderer.Tile(3, 2, 3)82 for y := range tileSize {83 l := left.NRGBAAt(tileSize-1, y)84 r := right.NRGBAAt(0, y)85 // The seam columns sit one world pixel apart, so demand agreement86 // in kind, not in exact antialias: both background or both not.87 lGround := l == Background88 rGround := r == Background89 if lGround != rGround {90 t.Fatalf("seam disagrees at y=%d: %v vs %v", y, l, r)91 }92 }93}9495// A tile nothing touches is the flat background, and every tile exists.96// Tile (6,2) sits east of the park and street, north of the trail, and97// inside the boundary rectangle's ring without touching its lines.98func TestEmptyTileIsBackground(t *testing.T) {99 renderer := NewRenderer(fixtureFeatures(), 3)100 tile := renderer.Tile(3, 6, 2)101 for at := 0; at < len(tile.Pix); at += 4 {102 if tile.Pix[at] != Background.R || tile.Pix[at+1] != Background.G || tile.Pix[at+2] != Background.B {103 t.Fatalf("empty tile carries paint at offset %d", at)104 }105 }106}107108// Two renders of one empty tile share bytes with each other -- the property109// the placeholder machinery downstream leans on to fold background tiles110// into one hash.111func TestEmptyTilesShareBytes(t *testing.T) {112 renderer := NewRenderer(fixtureFeatures(), 3)113 a, err := EncodePNG(renderer.Tile(3, 6, 2))114 if err != nil {115 t.Fatal(err)116 }117 b, err := EncodePNG(renderer.Tile(3, 5, 2))118 if err != nil {119 t.Fatal(err)120 }121 if !bytes.Equal(a, b) {122 t.Fatal("two empty tiles encode differently")123 }124}125126// The index must hand a tile every feature that bleeds into it, not only127// those whose geometry starts there: a stroke near an edge shows in the128// neighbour.129func TestStrokesBleedAcrossTileEdges(t *testing.T) {130 // At zoom 6 a tile is 128 world px; a street whose centerline sits half131 // a world pixel into tile (8,0) still paints the last column of tile132 // (7,0), because its width reaches across the seam.133 street := []Feature{{Role: RoleStreet, Lines: [][][2]float64{{{1024.5, 100}, {1024.5, 900}}}}}134 renderer := NewRenderer(street, 6)135 tile := renderer.Tile(6, 7, 1)136 found := false137 for y := 0; y < tileSize && !found; y++ {138 if tile.NRGBAAt(tileSize-1, y) != Background {139 found = true140 }141 }142 if !found {143 t.Fatal("a street on the far side of the seam leaves no paint in this tile")144 }145}146147func TestTileSizeIsThePipelines(t *testing.T) {148 renderer := NewRenderer(nil, 1)149 tile := renderer.Tile(1, 0, 0)150 if tile.Bounds() != image.Rect(0, 0, 256, 256) {151 t.Fatalf("tile bounds are %v", tile.Bounds())152 }153}