1package main23import (4 "encoding/json"5 "reflect"6 "strings"7 "testing"89 "github.com/FelineStateMachine/atlas/internal/semconv"10)1112// A capture normalizes into the same collections the wire now says: each13// category a point collection carrying its group title and resolved colours,14// the regions folded into the implicit area collection with a claimed id of15// its own, and what never drew never surviving.16func TestNormalizeWorldBuildsTheCollections(t *testing.T) {17 oldTown := int64(9001)18 ring := json.RawMessage(`[[[[0,0],[1,0],[1,1]]]]`)19 raw := rawMap{20 ID: 7,21 Groups: []rawGroup{22 {ID: 1, Title: "Districts", Color: "38344C", IconColor: "ffffff", Categories: []rawCategory{23 {24 ID: 10, Title: "Shops", Icon: "shop", Color: "aabbcc",25 DisplayType: "pin", Visible: true,26 Locations: []rawLocation{{27 ID: 100,28 Title: "Bakery",29 Description: "Fresh bread.",30 Latitude: json.RawMessage(`"1.5"`),31 Longitude: json.RawMessage(`-2.25`),32 RegionID: &oldTown,33 Attrs: map[string]string{"atlas.geo.lat": "39.8"},34 }},35 },36 {ID: 11, Title: "Signs", Icon: "sign", DisplayType: "text", Visible: false},37 }},38 {ID: 2, Title: "Nature", Categories: []rawCategory{39 {ID: 12, Title: "Trees", Icon: "tree", Visible: true},40 }},41 },42 Regions: []rawRegion{43 {44 ID: 9001, Title: "Old Town", Subtitle: "OT", Description: "The old quarter.",45 CenterX: json.RawMessage(`"12.5"`),46 CenterY: json.RawMessage(`3.25`),47 Features: []rawFeature{48 {Geometry: geometry{Type: "MultiPolygon", Coordinates: ring}},49 // An empty part never survived the old build either.50 {Geometry: geometry{}},51 },52 },53 // A region whose geometry all came through empty has nothing to54 // draw, and the build drops it whole.55 {ID: 9002, Title: "Ghost", Features: []rawFeature{{Geometry: geometry{Type: "Polygon"}}}},56 },57 }5859 collections, err := normalizeWorld(raw)60 if err != nil {61 t.Fatal(err)62 }63 if len(collections) != 4 {64 t.Fatalf("normalized into %d collections, want 4: %+v", len(collections), collections)65 }66 shops := collections[0]67 if shops.ID != 10 || shops.Group != "Districts" || shops.Kind != kindPoint ||68 shops.Color != "#AABBCC" || shops.IconColor != "#FFFFFF" || !shops.Visible {69 t.Fatalf("shops normalized as %+v", shops)70 }71 wantBakery := feature{72 ID: 100, Title: "Bakery", Description: "Fresh bread.",73 Lat: 1.5, Lng: -2.25, Member: &oldTown,74 Attrs: map[string]string{"atlas.geo.lat": "39.8"},75 }76 if len(shops.Features) != 1 || !reflect.DeepEqual(shops.Features[0], wantBakery) {77 t.Fatalf("bakery normalized as %+v\nwant %+v", shops.Features, wantBakery)78 }79 if collections[1].ID != 11 || collections[1].DisplayType != "text" ||80 collections[1].Color != "#38344C" || collections[2].Group != "Nature" {81 t.Fatalf("point collections normalized as %+v", collections[1:3])82 }8384 implicit := collections[3]85 if implicit.Key != regionsCollectionKey || implicit.Kind != kindArea || !implicit.Visible {86 t.Fatalf("implicit collection = %+v", implicit)87 }88 if implicit.ID == 0 || implicit.ID != int64(int32(implicit.ID)) {89 t.Fatalf("implicit collection id %d is no positive int31", implicit.ID)90 }91 wantOldTown := feature{92 ID: 9001, Title: "Old Town", Subtitle: "OT", Description: "The old quarter.",93 Center: &coordinate{Latitude: 3.25, Longitude: 12.5},94 Geometry: []geometry{{Type: "MultiPolygon", Coordinates: ring}},95 }96 if len(implicit.Features) != 1 || !reflect.DeepEqual(implicit.Features[0], wantOldTown) {97 t.Fatalf("implicit features = %+v\nwant one Old Town", implicit.Features)98 }99100 counts := catalogWorld{Collections: collections}.featureTally()101 if counts != (featureCounts{Point: 1, Area: 1}) {102 t.Fatalf("tally = %+v, want one point and one area", counts)103 }104}105106107// declaredFixture is a capture that declares its collections: an area108// collection and a path collection, a region claiming each, and one region109// claiming nothing, which still folds into the implicit collection.110func declaredFixture() rawMap {111 ring := json.RawMessage(`[[[[0,0],[1,0],[1,1]]]]`)112 lines := json.RawMessage(`[[[0,0],[1,1]]]`)113 return rawMap{114 Collections: []rawCollectionDecl{115 {Key: "districts", Title: "Districts", Attrs: map[string]string{116 semconv.KeyGeometryKind: "area",117 semconv.KeyLabelPolicy: "quiet",118 }},119 {Key: "creeks", Title: "Creeks", Attrs: map[string]string{120 semconv.KeyGeometryKind: "path",121 semconv.KeyStrokeWidthPx: "10",122 }},123 },124 Regions: []rawRegion{125 {ID: 1, Title: "R-5", Collection: "districts",126 Features: []rawFeature{{Geometry: geometry{Type: "MultiPolygon", Coordinates: ring}}}},127 {ID: 2, Title: "Big Dry Creek", Collection: "creeks",128 Features: []rawFeature{{Geometry: geometry{Type: "MultiLineString", Coordinates: lines}}}},129 {ID: 3, Title: "Old Town",130 Features: []rawFeature{{Geometry: geometry{Type: "MultiPolygon", Coordinates: ring}}}},131 },132 }133}134135// Declared collections bucket the regions that claim them, in declaration136// order after the point collections, the implicit collection last.137func TestNormalizeWorldBucketsDeclaredCollections(t *testing.T) {138 collections, err := normalizeWorld(declaredFixture())139 if err != nil {140 t.Fatal(err)141 }142 if len(collections) != 3 {143 t.Fatalf("normalized into %d collections, want 3: %+v", len(collections), collections)144 }145 districts, creeks, implicit := collections[0], collections[1], collections[2]146 if districts.Key != "districts" || districts.Kind != kindArea || districts.Title != "Districts" {147 t.Fatalf("first collection is %+v", districts)148 }149 if districts.Attrs[semconv.KeyLabelPolicy] != "quiet" {150 t.Fatalf("districts lost its declared attrs: %v", districts.Attrs)151 }152 if creeks.Key != "creeks" || creeks.Kind != kindPath {153 t.Fatalf("second collection is %+v", creeks)154 }155 if implicit.Key != regionsCollectionKey || implicit.Kind != kindArea {156 t.Fatalf("last collection is %+v, want the implicit one", implicit)157 }158 for at, want := range []string{"R-5", "Big Dry Creek", "Old Town"} {159 if len(collections[at].Features) != 1 || collections[at].Features[0].Title != want {160 t.Fatalf("collection %d holds %+v, want one feature %q",161 at, collections[at].Features, want)162 }163 }164}165166func TestNormalizeWorldRefusals(t *testing.T) {167 refuse := func(name, wantWords string, mutate func(*rawMap)) {168 t.Helper()169 raw := declaredFixture()170 mutate(&raw)171 _, err := normalizeWorld(raw)172 if err == nil {173 t.Fatalf("%s: normalized anyway", name)174 }175 if !strings.Contains(err.Error(), wantWords) {176 t.Fatalf("%s: error %q says nothing of %q", name, err, wantWords)177 }178 }179 refuse("undeclared collection", "never declares", func(raw *rawMap) {180 raw.Regions[0].Collection = "ghost"181 })182 // The sniff is dead: an undeclared line is refused, not guessed at.183 refuse("implicit lines", "declared as a path collection", func(raw *rawMap) {184 raw.Regions[1].Collection = ""185 })186 refuse("path without stroke", "stroke", func(raw *rawMap) {187 delete(raw.Collections[1].Attrs, semconv.KeyStrokeWidthPx)188 })189 refuse("kind unspoken", "geometry kind", func(raw *rawMap) {190 delete(raw.Collections[0].Attrs, semconv.KeyGeometryKind)191 })192 refuse("kind foreign", "geometry kind", func(raw *rawMap) {193 raw.Collections[0].Attrs[semconv.KeyGeometryKind] = "point"194 })195 refuse("key doubled", "declared twice", func(raw *rawMap) {196 raw.Collections[1].Key = "districts"197 raw.Regions[1].Collection = "districts"198 })199 refuse("key reserved", "reserves", func(raw *rawMap) {200 raw.Collections[0].Key = regionsCollectionKey201 raw.Regions[0].Collection = regionsCollectionKey202 })203}