clone

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

feat/atlas-next-horizon
atlas / internal / generate / crawl / bluemarble_test.go
bluemarble_test.go 12.0K
1package crawl23import (4 "bytes"5 "encoding/json"6 "image"7 "image/color"8 "image/jpeg"9 "log/slog"10 "os"11 "path/filepath"12 "strings"13 "testing"1415 "github.com/FelineStateMachine/atlas/internal/generate/archive"16 "github.com/FelineStateMachine/atlas/internal/generate/doc"17)1819// TestBlueMarblePinsTheSpecifiedPublication holds the shipped pin to the facts20// the included volume is specified against. A typo here is not a style problem:21// the digest decides which bytes are accepted, the capture time decides the22// bundle's name, and the credit is what NASA asks a republisher to keep.23func TestBlueMarblePinsTheSpecifiedPublication(t *testing.T) {24 if blueMarble.Asset != "https://assets.science.nasa.gov/content/dam/science/esd/eo/images/bmng/bmng-topography-bathymetry/july/world.topo.bathy.200407.3x21600x10800.jpg" {25 t.Errorf("asset URL %q is not the pinned publication", blueMarble.Asset)26 }27 if blueMarble.SHA256 != "d225f1f35a6448a4d1d8f6de6e48f3433e470085b70a35800e64f384f269a7b0" {28 t.Errorf("digest %q is not the pinned digest", blueMarble.SHA256)29 }30 if blueMarble.CapturedAt != "2026-08-03T16:21:07Z" {31 t.Errorf("capture time %q is not the recorded first capture", blueMarble.CapturedAt)32 }33 if blueMarble.Width != 21600 || blueMarble.Height != 10800 {34 t.Errorf("source size %d×%d is not the published image's", blueMarble.Width, blueMarble.Height)35 }36 if blueMarble.Borders.SHA256 != "6866c877d39cba9c357620878839b336d569f8c662d3cfab4cb1dbe2d39c977f" ||37 blueMarble.Places.SHA256 != "0dbd25c9ad8bd797ddf164b067f563be5c16be2c002254eb594862377963f9dc" {38 t.Error("the Natural Earth digests are not the pinned ones")39 }40 if !strings.Contains(blueMarble.Borders.Asset, "/v5.1.2/") ||41 !strings.Contains(blueMarble.Places.Asset, "/v5.1.2/") ||42 !strings.Contains(blueMarble.FeaturesEdition, "v5.1.2") {43 t.Error("the feature pins do not agree on the tagged edition")44 }45}4647// TestBlueMarbleDistillsTheFeatureFiles: the distillation keeps exactly what48// the capture needs -- names, codes, continents, label points, rings49// normalized to multipolygons, and only the primary capitals -- with every50// coordinate verbatim.51func TestBlueMarbleDistillsTheFeatureFiles(t *testing.T) {52 borders := []byte(`{"features":[53 {"properties":{"NAME":"Vale","ADM0_A3":"VAL","CONTINENT":"Europe","LABEL_X":10.5,"LABEL_Y":50.25},54 "geometry":{"type":"Polygon","coordinates":[[[5,45],[15,45],[15,55],[5,45]]]}},55 {"properties":{"NAME":"Mar","ADM0_A3":"MAR2","CONTINENT":"Oceania","LABEL_X":150,"LABEL_Y":-20},56 "geometry":{"type":"MultiPolygon","coordinates":[[[[145,-25],[155,-25],[155,-15],[145,-25]]]]}}]}`)57 places := []byte(`{"features":[58 {"properties":{"featurecla":"Admin-0 capital","name":"Vale City","adm0name":"Vale","adm0_a3":"VAL","latitude":50.1,"longitude":10.2}},59 {"properties":{"featurecla":"Admin-0 capital alt","name":"Old Seat","adm0name":"Vale","adm0_a3":"VAL","latitude":49,"longitude":9}},60 {"properties":{"featurecla":"Populated place","name":"Just A Town","adm0name":"Vale","adm0_a3":"VAL","latitude":48,"longitude":8}}]}`)61 pin := blueMarblePin{FeaturesEdition: "stated",62 Borders: blueMarbleAsset{SHA256: "bb"}, Places: blueMarbleAsset{SHA256: "pp"}}63 out, err := distillBlueMarbleFeatures(pin, borders, places)64 if err != nil {65 t.Fatal(err)66 }67 if out.Edition != "stated" || out.BordersSHA256 != "bb" || out.PlacesSHA256 != "pp" {68 t.Errorf("the distillation does not carry its provenance: %+v", out)69 }70 if len(out.Countries) != 2 {71 t.Fatalf("%d countries", len(out.Countries))72 }73 vale := out.Countries[0]74 if vale.Name != "Vale" || vale.A3 != "VAL" || vale.Continent != "Europe" ||75 vale.LabelLon != 10.5 || vale.LabelLat != 50.25 {76 t.Errorf("country did not travel verbatim: %+v", vale)77 }78 if len(vale.Polygons) != 1 || len(vale.Polygons[0]) != 1 || vale.Polygons[0][0][0] != [2]float64{5, 45} {79 t.Errorf("a Polygon did not normalize to one multipolygon part: %+v", vale.Polygons)80 }81 if len(out.Countries[1].Polygons) != 1 {82 t.Errorf("a MultiPolygon did not pass through: %+v", out.Countries[1].Polygons)83 }84 if len(out.Capitals) != 1 || out.Capitals[0].Name != "Vale City" {85 t.Fatalf("the distillation kept %+v; only primary capitals ride", out.Capitals)86 }87 if out.Capitals[0].Lat != 50.1 || out.Capitals[0].Lon != 10.2 {88 t.Errorf("a capital's coordinates did not travel verbatim: %+v", out.Capitals[0])89 }9091 if _, err := distillBlueMarbleFeatures(pin, []byte(`{"features":[92 {"properties":{"NAME":"Line","ADM0_A3":"LIN","CONTINENT":"Europe"},93 "geometry":{"type":"LineString","coordinates":[[0,0],[1,1]]}}]}`), places); err == nil {94 t.Error("a line among the borders was distilled anyway")95 }96}9798// TestBlueMarbleRefusesAWrongDigest is the gate the whole capture stands99// behind: bytes that do not hash to the pin are refused by name, and bytes that100// do pass.101func TestBlueMarbleRefusesAWrongDigest(t *testing.T) {102 err := verifyBlueMarble([]byte("not the publication"), blueMarble.SHA256)103 if err == nil {104 t.Fatal("changed upstream bytes were accepted")105 }106 if !strings.Contains(err.Error(), blueMarble.SHA256) || !strings.Contains(err.Error(), "refusing") {107 t.Errorf("the refusal does not name the pin: %v", err)108 }109110 stated := []byte("a stated source")111 if err := verifyBlueMarble(stated, Hash(stated)); err != nil {112 t.Errorf("the pinned bytes themselves were refused: %v", err)113 }114}115116// statedSource paints a small stated source image -- a horizontal gradient117// crossed by bands, so neighbouring tiles differ -- and encodes it the way the118// pin expects.119func statedSource(t *testing.T, width, height int) []byte {120 t.Helper()121 img := image.NewNRGBA(image.Rect(0, 0, width, height))122 for y := range height {123 for x := range width {124 img.SetNRGBA(x, y, color.NRGBA{125 R: uint8(255 * x / width),126 G: uint8(255 * y / height),127 B: uint8((x / 8) % 256),128 A: 255,129 })130 }131 }132 var out bytes.Buffer133 if err := jpeg.Encode(&out, img, &jpeg.Options{Quality: 95}); err != nil {134 t.Fatal(err)135 }136 return out.Bytes()137}138139// statedFeatures is the smallest sound vector half: two countries on two140// continents, a capital in one of them, and a microstate capital no border141// draws.142func statedFeatures() blueMarbleFeatures {143 return blueMarbleFeatures{144 Edition: "stated",145 BordersSHA256: "bb", PlacesSHA256: "pp",146 Countries: []blueMarbleCountry{147 {Name: "Vale", A3: "VAL", Continent: "Europe", LabelLon: 10, LabelLat: 50,148 Polygons: [][][][2]float64{{{{5, 45}, {15, 45}, {15, 55}, {5, 55}, {5, 45}}}}},149 {Name: "Mar", A3: "MAR2", Continent: "Oceania", LabelLon: 150, LabelLat: -20,150 Polygons: [][][][2]float64{{{{145, -25}, {155, -25}, {155, -15}, {145, -15}, {145, -25}}}}},151 },152 Capitals: []blueMarbleCapital{153 {Name: "Vale City", Country: "Vale", A3: "VAL", Lat: 50, Lon: 10},154 {Name: "Atoll", Country: "Atoll", A3: "ATL", Lat: -18, Lon: 152},155 },156 }157}158159// writeStated runs the capture's write path over a stated pin into a fresh160// archive, and answers the world directory it wrote.161func writeStated(t *testing.T, pin blueMarblePin, source []byte) (root, worldDir string) {162 t.Helper()163 root = t.TempDir()164 store, err := Open(root)165 if err != nil {166 t.Fatal(err)167 }168 volumeDir, err := store.RegisterVolume(Volume{ID: 42, Title: "Earth", Source: "nasa-blue-marble"})169 if err != nil {170 t.Fatal(err)171 }172 worldDir, err = store.RegisterWorld(173 Volume{ID: 42, Title: "Earth", Source: "nasa-blue-marble"}, volumeDir,174 World{ID: 43, Slug: "earth", Title: "Earth"})175 if err != nil {176 t.Fatal(err)177 }178 if err := writeBlueMarble(store, worldDir, 43, pin, source, statedFeatures(), slog.New(slog.DiscardHandler)); err != nil {179 t.Fatal(err)180 }181 return root, worldDir182}183184// TestBlueMarbleCutsTheWholeSphereWindow: the write path cuts exactly the185// reference level the deriver expects -- every tile of the half-height window186// at the reference zoom, cached, recorded under the tile set the reader names,187// and readable back through the archive.188func TestBlueMarbleCutsTheWholeSphereWindow(t *testing.T) {189 source := statedSource(t, 432, 216)190 pin := blueMarblePin{191 SHA256: Hash(source), CapturedAt: "2026-08-03T14:30:39Z",192 Width: 432, Height: 216, Quality: 90,193 }194 root, _ := writeStated(t, pin, source)195196 store, err := archive.Open(root)197 if err != nil {198 t.Fatal(err)199 }200 worlds, err := store.Worlds(store.Volumes()[0])201 if err != nil {202 t.Fatal(err)203 }204 captured, err := store.Tiles(worlds[0])205 if err != nil {206 t.Fatal(err)207 }208 levels, held := captured["earth/blue-marble"]209 if !held {210 t.Fatalf("no tiles recorded under the reader's tile set; recorded sets: %v", keysOf(captured))211 }212 maxX, maxY := doc.EquirectLevelExtent(blueMarbleZoom)213 if len(levels[blueMarbleZoom]) != (maxX+1)*(maxY+1) {214 t.Fatalf("%d tiles at the reference level, want the whole %d×%d window",215 len(levels[blueMarbleZoom]), maxX+1, maxY+1)216 }217 for _, ref := range levels[blueMarbleZoom] {218 path, format, err := store.Raster(worlds[0], ref)219 if err != nil || format != "jpg" {220 t.Fatalf("tile %d/%d is not readable as jpg: %v", ref.X, ref.Y, err)221 }222 data, err := os.ReadFile(path)223 if err != nil {224 t.Fatal(err)225 }226 if Hash(data) != ref.ContentHash {227 t.Fatalf("tile %d/%d does not carry the hash its record claims", ref.X, ref.Y)228 }229 }230231 captures, err := store.Captures(worlds[0])232 if err != nil || len(captures) != 1 {233 t.Fatalf("%d captures recorded: %v", len(captures), err)234 }235 if captures[0].Kind != "blue-marble-map" || captures[0].CapturedAt != pin.CapturedAt {236 t.Errorf("capture records %s at %s; the pin decides both",237 captures[0].Kind, captures[0].CapturedAt)238 }239}240241// TestBlueMarbleWritesTheSameCaptureTwice is the determinism the included242// volume stands on: the same pinned source, cut on two runs, is the same243// archive -- every tile byte for byte, and the capture body under the same244// content hash.245func TestBlueMarbleWritesTheSameCaptureTwice(t *testing.T) {246 source := statedSource(t, 432, 216)247 pin := blueMarblePin{248 SHA256: Hash(source), CapturedAt: "2026-08-03T14:30:39Z",249 Width: 432, Height: 216, Quality: 90,250 }251 firstRoot, firstWorld := writeStated(t, pin, source)252 secondRoot, secondWorld := writeStated(t, pin, source)253254 first, err := os.ReadFile(filepath.Join(firstWorld, "tiles", "index.json"))255 if err != nil {256 t.Fatal(err)257 }258 second, err := os.ReadFile(filepath.Join(secondWorld, "tiles", "index.json"))259 if err != nil {260 t.Fatal(err)261 }262 if !bytes.Equal(first, second) {263 t.Fatal("two runs over one pin recorded different tile indexes")264 }265 var records []TileRecord266 if err := json.Unmarshal(first, &records); err != nil {267 t.Fatal(err)268 }269 for _, record := range records {270 a, err := os.ReadFile(filepath.Join(firstRoot, tileRelPath(firstWorld, firstRoot, record)))271 if err != nil {272 t.Fatal(err)273 }274 b, err := os.ReadFile(filepath.Join(secondRoot, tileRelPath(secondWorld, secondRoot, record)))275 if err != nil {276 t.Fatal(err)277 }278 if !bytes.Equal(a, b) {279 t.Fatalf("tile %d/%d differs between two runs", record.X, record.Y)280 }281 }282283 firstIndex, err := os.ReadFile(filepath.Join(firstWorld, "snapshots", "index.json"))284 if err != nil {285 t.Fatal(err)286 }287 secondIndex, err := os.ReadFile(filepath.Join(secondWorld, "snapshots", "index.json"))288 if err != nil {289 t.Fatal(err)290 }291 if !bytes.Equal(firstIndex, secondIndex) {292 t.Fatal("two runs over one pin recorded different captures")293 }294}295296// TestResamplePreservesFlatGround: the frozen weights of every tap window sum297// exactly to one, so a flat picture resamples to exactly itself -- the property298// that keeps the cut from shifting the whole planet's tone.299func TestResamplePreservesFlatGround(t *testing.T) {300 flat := image.NewNRGBA(image.Rect(0, 0, 270, 135))301 for i := range flat.Pix {302 flat.Pix[i] = 137303 }304 out := resample(flat, 128, 64)305 for i, value := range out.Pix {306 if value != 137 {307 t.Fatalf("pixel byte %d resampled 137 to %d", i, value)308 }309 }310}311312func tileRelPath(worldDir, root string, record TileRecord) string {313 rel, _ := filepath.Rel(root, TilePath(worldDir, record.TileSetID, record.Zoom, record.X, record.Y, "jpg"))314 return rel315}316317func keysOf(m map[string]map[int][]archive.TileRef) []string {318 out := make([]string, 0, len(m))319 for key := range m {320 out = append(out, key)321 }322 return out323}