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 / pbmap / pbmap_test.go
pbmap_test.go 6.8K
1package pbmap23import (4 "bytes"5 "encoding/json"6 "math"7 "strings"8 "testing"910 "github.com/FelineStateMachine/atlas/internal/mgdoc"11)1213// fixture exercises everything Translate decides: a declared category with a14// used and an unused type, district pins under the favorites category, prose15// descriptions, and the night-city transformation.16func fixture() Capture {17 return Capture{18 Source: Source,19 GameSlug: "cyberpunk-2077",20 MapSlug: "night-city",21 GameTitle: "Cyberpunk 2077",22 MapTitle: "Night City",23 Map: MapConfig{24 TileServer: "https://storage-cdn.piggyback.com/images/tiles/cbp/{z}/{x}/{y}.webp",25 MinZoom: 3,26 MaxZoom: 7,27 PremiumMaxZoom: 9,28 Transform: Transform{A: 0.015625, B: 128, C: -0.015625, D: 128},29 },30 Labels: Labels{31 Categories: []Label{{Key: "gigs", Label: "Gigs"}},32 Types: []Label{33 {Key: "agent-saboteur", Label: "Agent Saboteur"},34 {Key: "province", Label: "Province"},35 },36 },37 Categories: []Category{38 {ID: "cat-fav", Key: "favorites", Position: 0},39 {ID: "cat-gigs", Key: "gigs", Position: 30, Types: []Type{40 {ID: "t-as", Key: "agent-saboteur", Position: 10},41 {ID: "t-empty", Key: "thievery", Position: 20},42 }},43 },44 Pins: []Pin{45 {ID: "pin-b", X: "-1992.51", Y: "4097.82", CategoryKey: "gigs", TypeKey: "agent-saboteur",46 Name: "WELCOME TO AMERICA, COMRADE", Description: "Unlock Condition – Street Cred Tier 1"},47 {ID: "pin-a", X: "0", Y: "0", CategoryKey: "gigs", TypeKey: "agent-saboteur", Name: "Centre Gig"},48 {ID: "pin-d", X: "-4000", Y: "5000", CategoryKey: "favorites", TypeKey: "province", Name: "Watson"},49 },50 Levels: []Level{51 {Zoom: 3, MinX: 0, MinY: 0, MaxX: 6, MaxY: 6},52 {Zoom: 7, MinX: 0, MinY: 0, MaxX: 111, MaxY: 111},53 },54 }55}5657func translate(t *testing.T, capture Capture) mgdoc.Map {58 t.Helper()59 doc, err := json.Marshal(capture)60 if err != nil {61 t.Fatal(err)62 }63 out, err := Translate(doc)64 if err != nil {65 t.Fatal(err)66 }67 var m mgdoc.Map68 if err := json.Unmarshal(out, &m); err != nil {69 t.Fatal(err)70 }71 return m72}7374func TestTranslateShape(t *testing.T) {75 m := translate(t, fixture())7677 if m.Slug != "night-city" || m.Game.Slug != "cyberpunk-2077" {78 t.Fatalf("identity: %q / %q", m.Game.Slug, m.Slug)79 }80 set := m.Config.TileSets[0]81 if set.Path != "cbp" || set.Extension != "webp" {82 t.Fatalf("tile set: %+v", set)83 }84 if set.MinZoom != 3 || set.MaxZoom != 7 {85 t.Fatalf("zoom range: %d..%d", set.MinZoom, set.MaxZoom)86 }87 if bound := set.Bounds["7"]; bound.X.Max != 111 || bound.Y.Max != 111 {88 t.Fatalf("deepest bounds: %+v", bound)89 }9091 if len(m.Groups) != 2 {92 t.Fatalf("groups: %+v", m.Groups)93 }94 gigs := m.Groups[0]95 if gigs.Title != "Gigs" || len(gigs.Categories) != 1 {96 t.Fatalf("gigs group: %+v", gigs)97 }98 category := gigs.Categories[0]99 if category.Title != "Agent Saboteur" || category.Icon != "agent-saboteur" ||100 category.DisplayType != "markers" {101 t.Fatalf("gig category: %+v", category)102 }103 if len(category.Locations) != 2 {104 t.Fatalf("gig pins: %+v", category.Locations)105 }106 // Pins sort by id, so pin-a comes first.107 if category.Locations[0].Title != "Centre Gig" {108 t.Fatalf("pin order: %+v", category.Locations)109 }110 if category.Locations[1].Description != "Unlock Condition – Street Cred Tier 1" {111 t.Fatalf("description lost: %+v", category.Locations[1])112 }113114 districts := m.Groups[1]115 if districts.Title != "Districts" || len(districts.Categories) != 1 {116 t.Fatalf("districts group: %+v", districts)117 }118 province := districts.Categories[0]119 if province.Title != "Province" || province.DisplayType != "text" {120 t.Fatalf("province category: %+v", province)121 }122 if province.Locations[0].Title != "Watson" {123 t.Fatalf("district pin: %+v", province.Locations[0])124 }125126 // The empty thievery type keeps no category.127 for _, group := range m.Groups {128 for _, category := range group.Categories {129 if category.Icon == "thievery" {130 t.Fatal("a pinless type kept its category")131 }132 }133 }134}135136// TestCoordinatesRoundTrip runs a pin's synthetic coordinates through the137// viewer's projection and expects the pixel Piggyback draws it at: the game138// coordinate through the map's own transformation, scaled to the world.139func TestCoordinatesRoundTrip(t *testing.T) {140 m := translate(t, fixture())141142 project := func(latitude, longitude float64) (float64, float64) {143 worldTiles := math.Pow(2, mgdoc.SourceZoom)144 x := ((longitude + 180) / 360) * worldTiles * mgdoc.TileSize145 y := (1 - math.Asinh(math.Tan(latitude*math.Pi/180))/math.Pi) / 2 * worldTiles * mgdoc.TileSize146 return x, y147 }148 expected := map[string][2]float64{149 "Centre Gig": {4096, 4096},150 "WELCOME TO AMERICA, COMRADE": {-1992.51/2 + 4096, -4097.82/2 + 4096},151 "Watson": {-4000.0/2 + 4096, -5000.0/2 + 4096},152 }153 seen := 0154 for _, group := range m.Groups {155 for _, category := range group.Categories {156 for _, location := range category.Locations {157 want, ok := expected[location.Title]158 if !ok {159 continue160 }161 seen++162 x, y := project(location.Latitude, location.Longitude)163 if math.Abs(x-want[0]) > 1e-6 || math.Abs(y-want[1]) > 1e-6 {164 t.Fatalf("%s projects to %.6f,%.6f, want %.3f,%.3f",165 location.Title, x, y, want[0], want[1])166 }167 }168 }169 }170 if seen != len(expected) {171 t.Fatalf("saw %d of %d pins", seen, len(expected))172 }173}174175func TestTranslateIsDeterministic(t *testing.T) {176 doc, _ := json.Marshal(fixture())177 first, err := Translate(doc)178 if err != nil {179 t.Fatal(err)180 }181 shuffled := fixture()182 shuffled.Pins[0], shuffled.Pins[2] = shuffled.Pins[2], shuffled.Pins[0]183 shuffled.Categories[0], shuffled.Categories[1] = shuffled.Categories[1], shuffled.Categories[0]184 reordered, _ := json.Marshal(shuffled)185 second, err := Translate(reordered)186 if err != nil {187 t.Fatal(err)188 }189 if !bytes.Equal(first, second) {190 t.Fatal("the same capture translated to different bytes")191 }192}193194func TestTranslateRejectsBadCaptures(t *testing.T) {195 undeclared := fixture()196 undeclared.Pins[0].TypeKey = "never-declared"197 if _, err := translateErr(undeclared); err == nil || !strings.Contains(err.Error(), "no category declares") {198 t.Fatalf("undeclared type: %v", err)199 }200201 unsurveyed := fixture()202 unsurveyed.Levels = nil203 if _, err := translateErr(unsurveyed); err == nil {204 t.Fatal("a capture with no observed levels was accepted")205 }206207 flat := fixture()208 flat.Map.Transform = Transform{}209 if _, err := translateErr(flat); err == nil {210 t.Fatal("a capture with no transformation was accepted")211 }212}213214func translateErr(capture Capture) ([]byte, error) {215 doc, err := json.Marshal(capture)216 if err != nil {217 return nil, err218 }219 return Translate(doc)220}221222func TestMaybeTranslatePassesOthersThrough(t *testing.T) {223 doc := []byte(`{"id": 5}`)224 out, err := MaybeTranslate("ign-map", doc)225 if err != nil {226 t.Fatal(err)227 }228 if !bytes.Equal(out, doc) {229 t.Fatal("another source's snapshot was rewritten")230 }231}