1package national23import (4 "encoding/json"5 "fmt"6 "math"7 "strings"8 "testing"910 "github.com/FelineStateMachine/atlas/format/semconv"11 "github.com/FelineStateMachine/atlas/internal/enrich"12 "github.com/FelineStateMachine/atlas/internal/enrich/curation"13)1415func tables(t *testing.T) enrich.Curation {16 t.Helper()17 loaded, err := curation.LoadFrom([]byte(`{18 "schema": 1,19 "queue": {"order": ["national"]},20 "merge": {"matchRadiusPx": 160, "separateRadiusPx": 320, "nearbyFloorPx": 48,21 "attributes": {"donorFillsEmpty": []}},22 "national": {"evidenceCollections": {"city": ["Subwatersheds"]}}23 }`))24 if err != nil {25 t.Fatal(err)26 }27 return loaded28}2930// box is a square of ground, as a polygon geometry.31func box(west, south, east, north float64) enrich.Geometry {32 coordinates := fmt.Sprintf(`[[[[%g,%g],[%g,%g],[%g,%g],[%g,%g],[%g,%g]]]]`,33 west, south, east, south, east, north, west, north, west, south)34 return enrich.Geometry{Type: "MultiPolygon", Coordinates: json.RawMessage(coordinates)}35}3637func line(positions string) enrich.Geometry {38 return enrich.Geometry{Type: "MultiLineString", Coordinates: json.RawMessage(positions)}39}4041// twoUnits is a survey of two subwatersheds side by side: west of zero and east42// of it.43func twoUnits() Evidence {44 return Evidence{45 Evidence: EvidenceDoc, Version: EvidenceVersion, Kind: EvidenceKind, Space: SpaceWorld,46 Units: []Unit{47 {Code: "170703010801", Name: "West Fork", Rings: ringsOf(box(-10, -10, 0, 10))},48 {Code: "170703010802", Name: "East Fork", Rings: ringsOf(box(0, -10, 10, 10))},49 },50 }51}5253func ringsOf(g enrich.Geometry) [][][][2]float64 { return g.Rings() }5455func city(features ...enrich.Feature) *enrich.Volume {56 return &enrich.Volume{57 Slug: "city",58 Worlds: []enrich.World{{59 Slug: "today",60 Collections: []enrich.Collection{61 {ID: 1, Title: "Zoning", Kind: enrich.KindArea, Features: features},62 {ID: 2, Title: "Subwatersheds", Kind: enrich.KindArea, Features: []enrich.Feature{63 {ID: 900, Title: "West Fork", Geometry: []enrich.Geometry{box(-10, -10, 0, 10)}},64 }},65 },66 }},67 }68}6970func run(t *testing.T, volume *enrich.Volume, evidence Evidence) enrich.Contribution {71 t.Helper()72 data, err := json.Marshal(evidence)73 if err != nil {74 t.Fatal(err)75 }76 contribution, err := New().Enrich(volume, enrich.Context{77 Evidence: staticEvidence{EvidenceName: data},78 Curation: tables(t),79 })80 if err != nil {81 t.Fatalf("national: %v", err)82 }83 return contribution84}8586type staticEvidence map[string][]byte8788func (s staticEvidence) Open(name string) ([]byte, bool, error) {89 data, held := s[name]90 return data, held, nil91}9293func TestTheJoinClaimsOnlyWhatItIsSureOf(t *testing.T) {94 cases := []struct {95 what string96 feature enrich.Feature97 code string98 }{99 {100 what: "ground wholly inside one unit",101 feature: enrich.Feature{ID: 1, Title: "Downtown", Geometry: []enrich.Geometry{box(-8, -8, -2, -2)}},102 code: "170703010801",103 },104 {105 what: "ground wholly inside the other",106 feature: enrich.Feature{ID: 1, Title: "Eastside", Geometry: []enrich.Geometry{box(2, 2, 8, 8)}},107 code: "170703010802",108 },109 {110 what: "ground that straddles the divide says nothing",111 feature: enrich.Feature{ID: 1, Title: "Riverside", Geometry: []enrich.Geometry{box(-4, -4, 4, 4)}},112 },113 {114 what: "ground that leaves the survey says nothing",115 feature: enrich.Feature{ID: 1, Title: "Outskirts", Geometry: []enrich.Geometry{box(-20, -4, -2, 4)}},116 },117 {118 what: "a trail wholly inside one unit",119 feature: enrich.Feature{ID: 1, Title: "River Trail", Geometry: []enrich.Geometry{line(`[[[-8,-8],[-7,-6],[-6,-4]]]`)}},120 code: "170703010801",121 },122 {123 what: "a trail that crosses the divide says nothing",124 feature: enrich.Feature{ID: 1, Title: "Crossing Trail", Geometry: []enrich.Geometry{line(`[[[-8,0],[0.5,0],[8,0]]]`)}},125 },126 {127 what: "a feature made of two pieces, both in one unit",128 feature: enrich.Feature{ID: 1, Title: "Two parks",129 Geometry: []enrich.Geometry{box(-8, -8, -6, -6), box(-5, -5, -3, -3)}},130 code: "170703010801",131 },132 {133 what: "a feature made of two pieces in different units",134 feature: enrich.Feature{ID: 1, Title: "Two parks",135 Geometry: []enrich.Geometry{box(-8, -8, -6, -6), box(3, 3, 5, 5)}},136 },137 }138139 for _, c := range cases {140 t.Run(c.what, func(t *testing.T) {141 contribution := run(t, city(c.feature), twoUnits())142 claimed := ""143 sentence := ""144 for _, op := range contribution.Ops {145 if op.Feature != 1 {146 continue147 }148 switch op.Kind {149 case enrich.OpSetAttr:150 claimed = op.Value151 case enrich.OpSetProse:152 sentence = op.Value153 }154 }155 if claimed != c.code {156 t.Errorf("claimed %q, want %q", claimed, c.code)157 }158 if c.code == "" {159 if sentence != "" {160 t.Errorf("silence was written down as %q", sentence)161 }162 return163 }164 if !strings.Contains(sentence, c.code) || !strings.HasPrefix(sentence, "Lies in the ") {165 t.Errorf("the sentence reads %q", sentence)166 }167 })168 }169}170171func TestTheSurveyMakesNoClaimsAboutItself(t *testing.T) {172 // The subwatershed collection is the evidence; a subwatershed does not need173 // a sentence saying which subwatershed it lies in.174 contribution := run(t, city(enrich.Feature{175 ID: 1, Title: "Downtown", Geometry: []enrich.Geometry{box(-8, -8, -2, -2)},176 }), twoUnits())177 for _, op := range contribution.Ops {178 if op.Feature == 900 {179 t.Errorf("the survey made a claim about itself: %+v", op)180 }181 }182}183184func TestPointFeaturesAreNotJoined(t *testing.T) {185 volume := city()186 volume.Worlds[0].Collections = append(volume.Worlds[0].Collections, enrich.Collection{187 ID: 3, Title: "Historic Resources", Kind: enrich.KindPoint,188 Features: []enrich.Feature{{ID: 5, Title: "A house", At: &enrich.Position{Lat: -5, Lng: -5}}},189 })190 contribution := run(t, volume, twoUnits())191 for _, op := range contribution.Ops {192 if op.Feature == 5 {193 t.Errorf("a point feature was joined: %+v", op)194 }195 }196}197198func TestAClaimAlreadyMadeIsLeftAlone(t *testing.T) {199 feature := enrich.Feature{200 ID: 1, Title: "Downtown", Geometry: []enrich.Geometry{box(-8, -8, -2, -2)},201 Attrs: map[string]string{semconv.KeyHydroHUC12: "170703010801"},202 }203 if contribution := run(t, city(feature), twoUnits()); !contribution.Empty() {204 t.Errorf("a claim that was already made was made again: %+v", contribution.Ops)205 }206}207208func TestProseThatIsAlreadyThereIsNotRewritten(t *testing.T) {209 feature := enrich.Feature{210 ID: 1, Title: "Downtown", Geometry: []enrich.Geometry{box(-8, -8, -2, -2)},211 Description: "The city's oldest zoning district.",212 }213 volume := city(feature)214 contribution := run(t, volume, twoUnits())215 for _, op := range contribution.Ops {216 if op.Kind == enrich.OpSetProse {217 t.Errorf("prose a source wrote was rewritten as %q", op.Value)218 }219 }220 // The membership claim still lands: what the join knows is the key, and the221 // sentence is a courtesy it only offers where there is room for it.222 if err := enrich.Apply(volume, contribution); err != nil {223 t.Fatal(err)224 }225 if _, held := volume.Worlds[0].Feature(1); held.Attrs[semconv.KeyHydroHUC12] == "" {226 t.Error("the claim itself was dropped along with the sentence")227 }228}229230func TestAnEmptyEvidenceBaseIsSilence(t *testing.T) {231 volume := city(enrich.Feature{ID: 1, Title: "Downtown", Geometry: []enrich.Geometry{box(-8, -8, -2, -2)}})232 contribution, err := New().Enrich(volume, enrich.Context{Curation: tables(t)})233 if err != nil {234 t.Fatalf("a volume nobody surveyed is not an error: %v", err)235 }236 if !contribution.Empty() {237 t.Error("something was claimed with no evidence at all")238 }239240 contribution, err = New().Enrich(volume, enrich.Context{241 Evidence: staticEvidence{}, Curation: tables(t),242 })243 if err != nil || !contribution.Empty() {244 t.Errorf("an evidence base without this survey: %v, %d operations", err, len(contribution.Ops))245 }246}247248func TestEvidenceIsHeldToItsSchema(t *testing.T) {249 good := twoUnits()250 cases := []struct {251 what string252 change func(*Evidence)253 refuse string254 }{255 {"a document of another kind", func(e *Evidence) { e.Kind = "hydro.huc8" }, "is of"},256 {"another schema version", func(e *Evidence) { e.Version = 99 }, "version"},257 {"another document entirely", func(e *Evidence) { e.Evidence = "something-else" }, "says"},258 {"coordinates in another space", func(e *Evidence) { e.Space = "geo" }, "published in"},259 {"a code that is not a code", func(e *Evidence) { e.Units[0].Code = "17" }, "twelve digits"},260 }261 for _, c := range cases {262 t.Run(c.what, func(t *testing.T) {263 evidence := good264 evidence.Units = append([]Unit(nil), good.Units...)265 c.change(&evidence)266 data, err := json.Marshal(evidence)267 if err != nil {268 t.Fatal(err)269 }270 if _, err := ReadEvidence(data); err == nil ||271 !strings.Contains(err.Error(), c.refuse) {272 t.Fatalf("error %v, expected something about %q", err, c.refuse)273 }274 })275 }276}277278func TestPositionsAreAnAnchorAndASampleOfTheBoundary(t *testing.T) {279 // A ring with far more vertices than the sample budget still costs the280 // budget, and the anchor is one of them.281 var ring strings.Builder282 ring.WriteString(`[[[`)283 for index := range 200 {284 angle := 2 * math.Pi * float64(index) / 200285 fmt.Fprintf(&ring, "[%f,%f],", 5*math.Cos(angle), 5*math.Sin(angle))286 }287 ring.WriteString(`[5,0]]]]`)288 positions := Positions(enrich.Geometry{Type: "MultiPolygon", Coordinates: json.RawMessage(ring.String())})289 if len(positions) > ClaimSamples+1 {290 t.Errorf("a 200-vertex ring volunteered %d positions; the budget is %d plus an anchor",291 len(positions), ClaimSamples)292 }293 if len(positions) < 2 {294 t.Errorf("a ring volunteered %d positions", len(positions))295 }296 // The anchor of a square is its middle.297 square := Positions(box(-10, -10, 10, 10))298 if len(square) == 0 {299 t.Fatal("a square volunteered nothing")300 }301 if x, y := square[0][0], square[0][1]; x > 1e-9 || x < -1e-9 || y > 1e-9 || y < -1e-9 {302 t.Errorf("the anchor of a centred square is %v", square[0])303 }304}305306func TestTheLargestRingIsTheAnchor(t *testing.T) {307 // Two pieces, one much larger: the anchor comes from the larger.308 small := `[[[-9,-9],[-8,-9],[-8,-8],[-9,-8],[-9,-9]]]`309 large := `[[[1,1],[9,1],[9,9],[1,9],[1,1]]]`310 geometry := enrich.Geometry{Type: "MultiPolygon",311 Coordinates: json.RawMessage("[" + small + "," + large + "]")}312 positions := Positions(geometry)313 if len(positions) == 0 {314 t.Fatal("nothing was volunteered")315 }316 if positions[0][0] < 1 || positions[0][1] < 1 {317 t.Errorf("the anchor is %v, which is not in the larger piece", positions[0])318 }319}