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 / authoring / representative_test.go
representative_test.go 6.0K
1package authoring23import (4 "bytes"5 "context"6 "encoding/json"7 "image/png"8 "os"9 "path/filepath"10 "strings"11 "testing"1213 "github.com/FelineStateMachine/atlas/format/vnext"14)1516func TestBuildRepresentativeWorldExercisesTheNativeContract(t *testing.T) {17 projectPath := filepath.Join("..", "..", "examples", "sample-region"+ProjectExtension)18 cacheDir := filepath.Join(t.TempDir(), "cache")19 libraryDir := filepath.Join(t.TempDir(), "library")20 result, err := Build(context.Background(), BuildOptions{21 ProjectPath: projectPath,22 CacheDir: cacheDir,23 LibraryDir: libraryDir,24 })25 if err != nil {26 t.Fatal(err)27 }28 reader, err := vnext.OpenFile(result.Path, vnext.StandardSchema())29 if err != nil {30 t.Fatal(err)31 }32 defer reader.Close()33 volume, err := reader.Volume()34 if err != nil {35 t.Fatal(err)36 }37 if len(volume.Worlds) != 1 {38 t.Fatalf("world count = %d, want 1", len(volume.Worlds))39 }40 world := volume.Worlds[0]41 if len(world.FeatureSets) != 3 {42 t.Fatalf("feature set count = %d, want point/path/area", len(world.FeatureSets))43 }44 wantKinds := map[vnext.GeometryKind]bool{45 vnext.GeometryPoint: false,46 vnext.GeometryLineString: false,47 vnext.GeometryPolygon: false,48 }49 wantPropertyKinds := map[vnext.Kind]bool{50 vnext.KindBool: false, vnext.KindInt64: false, vnext.KindFloat64: false,51 vnext.KindString: false, vnext.KindBytes: false, vnext.KindID: false,52 }53 relationships, provenance := 0, 054 featureIDs := make(map[string]bool)55 transformed := make(map[vnext.Position]bool)56 for _, set := range world.FeatureSets {57 if len(set.Features) == 0 {58 t.Fatalf("feature set %s is empty", set.ID)59 }60 kind := set.Features[0].Geometry.Kind61 wantKinds[kind] = true62 if !hasGeometryClaim(set.Claims, familyOf(kind)) {63 t.Errorf("feature set %s omits geometry claim %s", set.ID, familyOf(kind))64 }65 for _, feature := range set.Features {66 featureIDs[feature.ID] = true67 if feature.Geometry.Kind != kind {68 t.Fatalf("feature set %s mixes geometry %d and %d", set.ID, kind, feature.Geometry.Kind)69 }70 if len(feature.Provenance) == 0 {71 t.Fatalf("feature %s has no provenance", feature.ID)72 }73 provenance += len(feature.Provenance)74 relationships += len(feature.Relationships)75 if feature.Geometry.Kind == vnext.GeometryPoint {76 transformed[feature.Geometry.Parts[0].Rings[0][0]] = true77 }78 for _, property := range feature.Properties {79 wantPropertyKinds[property.Value.Kind] = true80 }81 }82 }83 for _, position := range []vnext.Position{{40, 60}, {75, 35}} {84 if !transformed[position] {85 t.Errorf("affine transform omits point %v", position)86 }87 }88 for kind, seen := range wantKinds {89 if !seen {90 t.Errorf("geometry kind %d is not represented", kind)91 }92 }93 for kind, seen := range wantPropertyKinds {94 if !seen {95 t.Errorf("property kind %d is not represented", kind)96 }97 }98 if relationships == 0 || provenance < 4 {99 t.Fatalf("relationships=%d provenance=%d, want a related evidence graph", relationships, provenance)100 }101 for _, set := range world.FeatureSets {102 for _, feature := range set.Features {103 for _, relationship := range feature.Relationships {104 if !featureIDs[relationship.Target] {105 t.Errorf("feature %s targets missing %s", feature.ID, relationship.Target)106 }107 }108 }109 }110 if len(world.RasterPyramids) != 1 {111 t.Fatalf("raster pyramid count = %d, want 1", len(world.RasterPyramids))112 }113 raster := world.RasterPyramids[0]114 if raster.MinZoom != 0 || raster.MaxZoom != 1 || raster.Formats[0] != "png" || raster.Formats[1] != "png" {115 t.Fatalf("raster pyramid = %+v, want canonical PNG zooms 0..1", raster)116 }117 blobNames := reader.BlobNames()118 for _, name := range []string{"tiles/background/0/0/0.png", "tiles/background/1/0/0.png", "tiles/background/1/1/1.png"} {119 if !containsString(blobNames, name) {120 t.Errorf("native Atlas omits raster blob %s", name)121 }122 }123 tile, err := reader.Blob("tiles/background/1/1/1.png")124 if err != nil {125 t.Fatal(err)126 }127 image, err := png.Decode(bytes.NewReader(tile))128 if err != nil || image.Bounds().Dx() != 32 || image.Bounds().Dy() != 32 {129 t.Fatalf("canonical tile = %v, %v", image, err)130 }131 if len(world.Presentation.Styles) != 3 || len(world.Presentation.Layers) != 3 || len(world.Presentation.Legend) != 3 {132 t.Fatalf("presentation = %d styles, %d layers, %d legend entries", len(world.Presentation.Styles), len(world.Presentation.Layers), len(world.Presentation.Legend))133 }134 assets := make(map[string]vnext.Asset, len(volume.Assets))135 for _, asset := range volume.Assets {136 assets[asset.ID] = asset137 }138 if len(assets["sample-symbol"].Data) == 0 || len(assets["build-receipt"].Data) == 0 {139 t.Fatalf("assets = %v, want authored symbol and build receipt", sortedAssetIDs(assets))140 }141 var receipt struct {142 Captures []struct {143 SHA256 string `json:"sha256"`144 } `json:"captures"`145 }146 if err := json.Unmarshal(assets["build-receipt"].Data, &receipt); err != nil || len(receipt.Captures) != 5 {147 t.Fatalf("build receipt captures = %d, %v", len(receipt.Captures), err)148 }149 firstBytes, err := os.ReadFile(result.Path)150 if err != nil {151 t.Fatal(err)152 }153 rebuilt, err := Build(context.Background(), BuildOptions{154 ProjectPath: projectPath, CacheDir: cacheDir, LibraryDir: libraryDir, Offline: true,155 })156 if err != nil {157 t.Fatal(err)158 }159 rebuiltBytes, err := os.ReadFile(rebuilt.Path)160 if err != nil {161 t.Fatal(err)162 }163 if rebuilt.Path != result.Path || !rebuilt.Present || !bytes.Equal(firstBytes, rebuiltBytes) {164 t.Fatalf("offline rebuild differs: first=%s rebuilt=%s present=%v", result.Path, rebuilt.Path, rebuilt.Present)165 }166}167168func hasGeometryClaim(properties []vnext.Property, family string) bool {169 for _, property := range properties {170 if property.Field.Name == "atlas.geometry.kind" && property.Value.String == family {171 return true172 }173 }174 return false175}176177func containsString(values []string, want string) bool {178 for _, value := range values {179 if value == want {180 return true181 }182 }183 return false184}185186func sortedAssetIDs(values map[string]vnext.Asset) string {187 ids := make([]string, 0, len(values))188 for id := range values {189 ids = append(ids, id)190 }191 return strings.Join(ids, ", ")192}