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 / arcgismap / arcgismap_test.go
arcgismap_test.go 22.3K
1package arcgismap23import (4 "bytes"5 "encoding/json"6 "math"7 "strconv"8 "testing"910 "github.com/FelineStateMachine/atlas/internal/mgdoc"11 "github.com/FelineStateMachine/atlas/internal/semconv"12)1314// The window must be square in Mercator's own terms, or one scale cannot15// serve both axes.16func TestCityWindowIsSquare(t *testing.T) {17 window := CityWindow(bend.BBox)18 u0, v0 := mercator(window.West, window.North)19 u1, v1 := mercator(window.East, window.South)20 du, dv := u1-u0, v1-v021 if math.Abs(du-dv) > 1e-12 {22 t.Fatalf("window is not square: du=%v dv=%v", du, dv)23 }24 if du <= 0 {25 t.Fatalf("window has no ground: du=%v", du)26 }27 // The curated box sits inside its padded window.28 if window.West >= bend.BBox[0] || window.East <= bend.BBox[2] {29 t.Fatalf("window %+v does not contain the bbox longitudes", window)30 }31 if window.South >= bend.BBox[1] || window.North <= bend.BBox[3] {32 t.Fatalf("window %+v does not contain the bbox latitudes", window)33 }34}3536// A feature's true coordinates, through the window and the synthetic37// spelling, must land back on the same world pixel the viewer will project38// them to -- the whole trick, held to a hundredth of a pixel.39func TestWorldPixelRoundTripsThroughSyntheticCoordinates(t *testing.T) {40 window := CityWindow(bend.BBox)41 points := [][2]float64{42 {-121.30810509, 44.05813637}, // a real historic site43 {-121.4180, 43.9500}, // the bbox corners44 {-121.2170, 44.1650},45 }46 for _, point := range points {47 x, y := window.WorldPixel(point[0], point[1])48 lng := mgdoc.SyntheticLongitude(x)49 lat := mgdoc.SyntheticLatitude(y)50 backX := mgdoc.ProjectX(lng, mgdoc.SourceZoom, 0)51 backY := mgdoc.ProjectY(lat, mgdoc.SourceZoom, 0)52 if math.Abs(backX-x) > 0.01 || math.Abs(backY-y) > 0.01 {53 t.Fatalf("pixel (%v,%v) round-trips to (%v,%v)", x, y, backX, backY)54 }55 }56}5758// The declared mercator.deg mapping and WorldPixel must be the same59// transform: a pin's world pixel, inverted through the declared window60// edges, recovers the published coordinates to well under a meter.61func TestDeclaredWindowInvertsToTrueCoordinates(t *testing.T) {62 window := CityWindow(bend.BBox)63 lon, lat := -121.3153, 44.058264 x, y := window.WorldPixel(lon, lat)65 u0, v0 := mercator(window.West, window.North)66 u1, _ := mercator(window.East, window.South)67 side := u1 - u068 backLon, backLat := degrees(u0+x/mgdoc.WorldSize*side, v0+y/mgdoc.WorldSize*side)69 if math.Abs(backLon-lon) > 1e-9 || math.Abs(backLat-lat) > 1e-9 {70 t.Fatalf("(%v,%v) inverts to (%v,%v)", lon, lat, backLon, backLat)71 }72}7374func fixtureCapture() Capture {75 window := CityWindow(bend.BBox)76 point := func(lon, lat float64) Geometry {77 return Geometry{Type: GeometryPoint, Point: []float64{lon, lat}}78 }79 square := func(lon, lat, size float64) Geometry {80 return Geometry{Type: GeometryRings, Rings: [][][][]float64{{{81 {lon, lat}, {lon + size, lat}, {lon + size, lat + size}, {lon, lat + size}, {lon, lat},82 }}}}83 }84 line := func(positions ...[2]float64) Geometry {85 one := make([][]float64, 0, len(positions))86 for _, position := range positions {87 one = append(one, []float64{position[0], position[1]})88 }89 return Geometry{Type: GeometryLines, Lines: [][][]float64{one}}90 }91 return Capture{92 Source: Source,93 City: "bend-or",94 Title: "Bend, Oregon",95 MapSlug: "2026-08-01",96 Window: window,97 Basemap: MapConfig{MaxZoom: 3, Extension: "png"},98 Datasets: []CapturedDataset{99 {Slug: "historic-sites", Features: []Feature{100 {ID: 1, Fields: Fields{"NAME": "A.C. Lucas House", "TAB_NAME": "Residential", "SHORT_DESC": "42 NW Hawthorne Ave", "DESC1": "Constructed: 1910"}, Geometry: point(-121.3081, 44.0581)},101 {ID: 9, Fields: Fields{"NAME": "Drake Park Gazebo", "TAB_NAME": "Civic", "SHORT_DESC": "Drake Park"}, Geometry: point(-121.315, 44.06)},102 }},103 {Slug: "zoning", Features: []Feature{104 {ID: 1, Fields: Fields{"ZONE": "PF"}, Geometry: square(-121.31, 44.05, 0.004)},105 {ID: 2, Fields: Fields{"ZONE": "RS"}, Geometry: square(-121.30, 44.06, 0.002)},106 {ID: 3, Fields: Fields{"ZONE": "PF"}, Geometry: square(-121.28, 44.08, 0.003)},107 // A second RS parcel in the other subwatershed: the RS zone108 // straddles, and must say nothing about membership.109 {ID: 4, Fields: Fields{"ZONE": "RS"}, Geometry: square(-121.36, 44.135, 0.002)},110 }},111 {Slug: "annexations", Features: []Feature{112 {ID: 3, Fields: Fields{"DESCRIPT": "Rockridge Park", "EFF_DATE": "1556866800000", "ORDIN_NO": "NS-2327"}, Geometry: square(-121.29, 44.09, 0.002)},113 }},114 {Slug: "wetlands", Features: []Feature{115 {ID: 1, Fields: Fields{"TYPE": "Significant Identified Wetlands", "MAP_CODE": "R9"}, Geometry: square(-121.32, 44.04, 0.003)},116 }},117 {Slug: "trails", Features: []Feature{118 {ID: 6, Fields: Fields{"Trail_Name": "Riley Ranch", "Park": "Riley Ranch Nature Reserve", "Status": "Existing"}, Geometry: line([2]float64{-121.35, 44.14}, [2]float64{-121.345, 44.145})},119 {ID: 7, Fields: Fields{"Trail_Name": "Planned Spur", "Status": "Proposed"}, Geometry: line([2]float64{-121.33, 44.02}, [2]float64{-121.329, 44.021})},120 }},121 {Slug: "mpo-boundary", Features: []Feature{122 {ID: 21, Fields: Fields{"LABEL": "MPO BOUNDARY"}, Geometry: square(-121.40, 43.97, 0.17)},123 }},124 // The national enrichment, city-sized: two subwatersheds that125 // between them hold most of the city fixtures -- but not the MPO126 // boundary's corners, so that zone's ground lands nowhere and it127 // claims nothing.128 {Slug: "watersheds", Features: []Feature{129 {ID: 1707030101, Fields: Fields{"huc10": "1707030101", "name": "Tumalo Creek"}, Geometry: square(-121.40, 44.00, 0.15)},130 }},131 {Slug: "subwatersheds", Features: []Feature{132 {ID: 170703010101, Fields: Fields{"huc12": "170703010101", "name": "Tumalo Creek"}, Geometry: square(-121.37, 44.03, 0.10)},133 {ID: 170703010102, Fields: Fields{"huc12": "170703010102", "name": "Bull Creek"}, Geometry: square(-121.37, 44.131, 0.03)},134 }},135 {Slug: "streams", Features: []Feature{136 {ID: 51, Fields: Fields{"gnis_name": "Tumalo Creek"}, Geometry: line([2]float64{-121.34, 44.05}, [2]float64{-121.30, 44.07})},137 }},138 {Slug: "waterbodies", Features: []Feature{139 {ID: 61, Fields: Fields{"GNIS_NAME": "Mirror Pond"}, Geometry: square(-121.316, 44.057, 0.002)},140 // An unnamed pond draws into the basemap but earns no zone.141 {ID: 62, Geometry: square(-121.29, 44.05, 0.001)},142 }},143 },144 }145}146147func translated(t *testing.T, capture Capture) mgdoc.Map {148 t.Helper()149 raw, err := json.Marshal(&capture)150 if err != nil {151 t.Fatal(err)152 }153 doc, err := Translate(raw)154 if err != nil {155 t.Fatal(err)156 }157 var out mgdoc.Map158 if err := json.Unmarshal(doc, &out); err != nil {159 t.Fatal(err)160 }161 return out162}163164func TestTranslateShapesTheDocument(t *testing.T) {165 out := translated(t, fixtureCapture())166167 if out.Slug != "2026-08-01" || out.Title != "2026-08-01" {168 t.Fatalf("map is %q titled %q, not the capture day", out.Slug, out.Title)169 }170 if out.Game.Slug != "bend-or" || out.Game.Title != "Bend, Oregon" {171 t.Fatalf("game is %q titled %q", out.Game.Slug, out.Game.Title)172 }173174 if len(out.Config.TileSets) != 1 {175 t.Fatalf("map has %d tile sets", len(out.Config.TileSets))176 }177 set := out.Config.TileSets[0]178 if set.Path != "bend-or/2026-08-01/basemap" || set.Extension != "png" || set.MaxZoom != 3 {179 t.Fatalf("tile set is %+v", set)180 }181 for zoom := 0; zoom <= set.MaxZoom; zoom++ {182 bound, declared := set.Bounds[strconv.Itoa(zoom)]183 if !declared {184 t.Fatalf("level %d declares no bounds", zoom)185 }186 if last := 1<<zoom - 1; bound.X.Max != last || bound.Y.Max != last || bound.X.Min != 0 || bound.Y.Min != 0 {187 t.Fatalf("level %d bounds are %+v", zoom, bound)188 }189 }190191 if err := semconv.Validate(semconv.EntityWorld, out.Attrs); err != nil {192 t.Fatalf("map attrs: %v", err)193 }194 if out.Attrs[semconv.KeyGeometrySurface] != semconv.SurfacePlane {195 t.Fatalf("surface is %q", out.Attrs[semconv.KeyGeometrySurface])196 }197198 // One category per point dataset, gathered under curated group titles.199 if len(out.Groups) != 1 {200 t.Fatalf("document carries %d groups", len(out.Groups))201 }202 group := out.Groups[0]203 if group.Title != "Heritage" || len(group.Categories) != 1 {204 t.Fatalf("group is %q with %d categories", group.Title, len(group.Categories))205 }206 historic := group.Categories[0]207 if historic.Title != "Historic Resources" || len(historic.Locations) != 2 {208 t.Fatalf("category is %q with %d pins", historic.Title, len(historic.Locations))209 }210 if err := semconv.Validate(semconv.EntityCollection, historic.Attrs); err != nil {211 t.Fatalf("category attrs: %v", err)212 }213 pin := historic.Locations[0]214 if pin.Title != "A.C. Lucas House" {215 t.Fatalf("first pin is %q", pin.Title)216 }217 if pin.Description != "Residential · 42 NW Hawthorne Ave · Constructed: 1910" {218 t.Fatalf("pin card reads %q", pin.Description)219 }220 if err := semconv.Validate(semconv.EntityFeature, pin.Attrs); err != nil {221 t.Fatalf("pin attrs: %v", err)222 }223 if pin.Attrs[semconv.KeyGeoLat] != "44.0581" || pin.Attrs[semconv.KeyGeoLon] != "-121.3081" {224 t.Fatalf("pin keeps %q, %q", pin.Attrs[semconv.KeyGeoLat], pin.Attrs[semconv.KeyGeoLon])225 }226227 // Zones: the boundary as one, zoning bucketed by code, annexations by228 // decade, wetlands by type, and only the existing named trail -- in229 // curated dataset order, with the national enrichment after them all.230 titles := make([]string, 0, len(out.Regions))231 for _, region := range out.Regions {232 titles = append(titles, region.Title)233 }234 want := []string{"MPO Boundary", "PF", "RS", "Annexed 2010–2019",235 "Significant Identified Wetlands", "Riley Ranch",236 "Tumalo Creek", "Tumalo Creek", "Bull Creek", "Tumalo Creek", "Mirror Pond"}237 if len(titles) != len(want) {238 t.Fatalf("zones are %v", titles)239 }240 for at, name := range want {241 if titles[at] != name {242 t.Fatalf("zones are %v, want %v", titles, want)243 }244 }245246 // The document declares its zone-making datasets as collections, in the247 // same curated order, each saying its kind -- and its stroke, when its248 // features are lines -- in the registered vocabulary. The hydro areas249 // declare quiet labels: the nation's water is the city's context, not250 // its headline. The stream lines say nothing, because a path is quiet251 // by the registry's own default and label policy is an area's word.252 wantDecls := []struct{ key, title, kind, label string }{253 {"mpo-boundary", "MPO Boundary", "area", ""},254 {"zoning", "Zoning", "area", ""},255 {"annexations", "Annexations", "area", ""},256 {"wetlands", "Wetlands", "area", ""},257 {"trails", "Paths & Trails", "path", ""},258 {"watersheds", "Watersheds", "area", "quiet"},259 {"subwatersheds", "Subwatersheds", "area", "quiet"},260 {"streams", "Streams", "path", ""},261 {"waterbodies", "Waterbodies", "area", "quiet"},262 }263 if len(out.Collections) != len(wantDecls) {264 t.Fatalf("document declares %d collections, want %d: %+v",265 len(out.Collections), len(wantDecls), out.Collections)266 }267 for at, want := range wantDecls {268 decl := out.Collections[at]269 if decl.Key != want.key || decl.Title != want.title {270 t.Fatalf("collection %d is %q titled %q, want %q titled %q",271 at, decl.Key, decl.Title, want.key, want.title)272 }273 if err := semconv.Validate(semconv.EntityCollection, decl.Attrs); err != nil {274 t.Fatalf("collection %q attrs: %v", decl.Key, err)275 }276 if got := decl.Attrs[semconv.KeyGeometryKind]; got != want.kind {277 t.Fatalf("collection %q declares kind %q, want %q", decl.Key, got, want.kind)278 }279 if got := decl.Attrs[semconv.KeyLabelPolicy]; got != want.label {280 t.Fatalf("collection %q declares label policy %q, want %q", decl.Key, got, want.label)281 }282 _, stroked := decl.Attrs[semconv.KeyStrokeWidthPx]283 if stroked != (want.kind == "path") {284 t.Fatalf("collection %q: only path collections declare a stroke, got %v", decl.Key, decl.Attrs)285 }286 }287 if out.Collections[4].Attrs[semconv.KeyStrokeWidthPx] != "12" ||288 out.Collections[7].Attrs[semconv.KeyStrokeWidthPx] != "10" {289 t.Fatalf("path collections declare strokes %q and %q",290 out.Collections[4].Attrs[semconv.KeyStrokeWidthPx],291 out.Collections[7].Attrs[semconv.KeyStrokeWidthPx])292 }293294 // Every region claims the collection its dataset declared.295 wantHomes := []string{"mpo-boundary", "zoning", "zoning", "annexations",296 "wetlands", "trails", "watersheds", "subwatersheds", "subwatersheds",297 "streams", "waterbodies"}298 for at, home := range wantHomes {299 if got := out.Regions[at].Collection; got != home {300 t.Fatalf("zone %q claims collection %q, want %q", out.Regions[at].Title, got, home)301 }302 }303304 // The national zones say which grain of the nation they are.305 if got := out.Regions[6].Subtitle; got != "Watershed · HUC 1707030101" {306 t.Fatalf("watershed subtitle is %q", got)307 }308 if got := out.Regions[7].Subtitle; got != "Subwatershed · HUC 170703010101" {309 t.Fatalf("subwatershed subtitle is %q", got)310 }311 if got := out.Regions[9].Subtitle; got != "Stream" {312 t.Fatalf("stream subtitle is %q", got)313 }314 if got := out.Regions[10].Subtitle; got != "Waterbody" {315 t.Fatalf("waterbody subtitle is %q", got)316 }317318 // Membership: the PF zone's ground lies wholly in one subwatershed, so319 // it says so, once, in both spellings.320 pf := out.Regions[1]321 if pf.Description != "Lies in the Tumalo Creek subwatershed (HUC 170703010101)." {322 t.Fatalf("PF card reads %q", pf.Description)323 }324 if err := semconv.Validate(semconv.EntityFeature, pf.Attrs); err != nil {325 t.Fatalf("PF attrs: %v", err)326 }327 if pf.Attrs[semconv.KeyHydroHUC12] != "170703010101" {328 t.Fatalf("PF claims %q", pf.Attrs[semconv.KeyHydroHUC12])329 }330 // The RS zone straddles the two subwatersheds and says nothing; the MPO331 // boundary's corners land in no unit and it says nothing either; the332 // national zones never describe themselves.333 for _, at := range []int{2, 0, 7} {334 zone := out.Regions[at]335 if zone.Description != "" || zone.Attrs[semconv.KeyHydroHUC12] != "" {336 t.Fatalf("zone %q claims membership: %q %v", zone.Title, zone.Description, zone.Attrs)337 }338 }339 // A line zone in one subwatershed claims it; its stroke is its340 // collection's to declare, never its own.341 riley := out.Regions[5]342 if riley.Attrs[semconv.KeyHydroHUC12] != "170703010102" {343 t.Fatalf("trail attrs are %v", riley.Attrs)344 }345 if out.Regions[1].Subtitle != "Zoning" {346 t.Fatalf("PF subtitle is %q", out.Regions[1].Subtitle)347 }348 if len(out.Regions[1].Features) != 2 {349 t.Fatalf("PF gathers %d features", len(out.Regions[1].Features))350 }351 trail := out.Regions[5]352 if trail.Subtitle != "Riley Ranch Nature Reserve" || len(trail.Features) != 1 {353 t.Fatalf("trail zone is %+v", trail)354 }355 // A line zone stays the line it is. Its width lives on the collection356 // declaration alone, so the zone's own attributes validate as the357 // feature they attach to, with nothing borrowed.358 if err := semconv.Validate(semconv.EntityFeature, trail.Attrs); err != nil {359 t.Fatalf("trail attrs: %v", err)360 }361 if _, stroked := trail.Attrs[semconv.KeyStrokeWidthPx]; stroked {362 t.Fatalf("the trail zone spells a stroke of its own: %v", trail.Attrs)363 }364 if trail.Features[0].Geometry.Type != "MultiLineString" {365 t.Fatalf("trail geometry is %q", trail.Features[0].Geometry.Type)366 }367 var lineCoords [][][]float64368 if err := json.Unmarshal(trail.Features[0].Geometry.Coordinates, &lineCoords); err != nil {369 t.Fatal(err)370 }371 if len(lineCoords) != 1 || len(lineCoords[0]) != 2 {372 t.Fatalf("the two-point trail should stay one two-point line, got %v", lineCoords)373 }374 if _, stroked := out.Regions[1].Attrs[semconv.KeyStrokeWidthPx]; stroked {375 t.Fatalf("a polygon zone declares no stroke, got %v", out.Regions[1].Attrs)376 }377378 // Every zone position must project back inside the world square.379 positions := func(geometry mgdoc.Geometry) [][]float64 {380 var out [][]float64381 if geometry.Type == "MultiLineString" {382 var lines [][][]float64383 if err := json.Unmarshal(geometry.Coordinates, &lines); err != nil {384 t.Fatal(err)385 }386 for _, line := range lines {387 out = append(out, line...)388 }389 return out390 }391 var polygons [][][][]float64392 if err := json.Unmarshal(geometry.Coordinates, &polygons); err != nil {393 t.Fatal(err)394 }395 for _, polygon := range polygons {396 for _, ring := range polygon {397 out = append(out, ring...)398 }399 }400 return out401 }402 for _, region := range out.Regions {403 for _, feature := range region.Features {404 for _, position := range positions(feature.Geometry) {405 x := mgdoc.ProjectX(position[0], mgdoc.SourceZoom, 0)406 y := mgdoc.ProjectY(position[1], mgdoc.SourceZoom, 0)407 if x < -1 || x > mgdoc.WorldSize+1 || y < -1 || y > mgdoc.WorldSize+1 {408 t.Fatalf("zone %q position projects to (%v,%v)", region.Title, x, y)409 }410 }411 }412 }413}414415// The same data in any order makes byte-identical documents, which is what416// content-addressed snapshots require.417func TestTranslateIsDeterministic(t *testing.T) {418 capture := fixtureCapture()419 first, err := json.Marshal(&capture)420 if err != nil {421 t.Fatal(err)422 }423 shuffled := fixtureCapture()424 for at := range shuffled.Datasets {425 features := shuffled.Datasets[at].Features426 for i, j := 0, len(features)-1; i < j; i, j = i+1, j-1 {427 features[i], features[j] = features[j], features[i]428 }429 }430 shuffled.Datasets[0], shuffled.Datasets[len(shuffled.Datasets)-1] =431 shuffled.Datasets[len(shuffled.Datasets)-1], shuffled.Datasets[0]432 shuffled.Normalize()433 capture.Normalize()434 second, err := json.Marshal(&capture)435 if err != nil {436 t.Fatal(err)437 }438 normalizedShuffled, err := json.Marshal(&shuffled)439 if err != nil {440 t.Fatal(err)441 }442 if !bytes.Equal(second, normalizedShuffled) {443 t.Fatal("normalized captures differ by input order")444 }445 docA, err := Translate(first)446 if err != nil {447 t.Fatal(err)448 }449 docB, err := Translate(normalizedShuffled)450 if err != nil {451 t.Fatal(err)452 }453 if !bytes.Equal(docA, docB) {454 t.Fatal("translation differs by input order")455 }456}457458func TestTranslateRefusals(t *testing.T) {459 refuse := func(name string, mutate func(*Capture)) {460 t.Helper()461 capture := fixtureCapture()462 mutate(&capture)463 raw, err := json.Marshal(&capture)464 if err != nil {465 t.Fatal(err)466 }467 if _, err := Translate(raw); err == nil {468 t.Fatalf("%s: translated anyway", name)469 }470 }471 refuse("wrong source", func(c *Capture) { c.Source = "mapgenie" })472 refuse("uncurated city", func(c *Capture) { c.City = "gotham" })473 refuse("slug not a day", func(c *Capture) { c.MapSlug = "latest" })474 refuse("no pyramid", func(c *Capture) { c.Basemap.MaxZoom = 0 })475 refuse("uncurated dataset", func(c *Capture) { c.Datasets[0].Slug = "parcels" })476 refuse("dataset doubled", func(c *Capture) { c.Datasets = append(c.Datasets, c.Datasets[0]) })477 refuse("every pin titleless", func(c *Capture) {478 for at := range c.Datasets {479 if c.Datasets[at].Slug == "historic-sites" {480 for f := range c.Datasets[at].Features {481 c.Datasets[at].Features[f].Fields["NAME"] = ""482 }483 }484 }485 })486 refuse("every pin astray", func(c *Capture) {487 for at := range c.Datasets {488 if c.Datasets[at].Slug == "historic-sites" {489 for f := range c.Datasets[at].Features {490 c.Datasets[at].Features[f].Geometry.Point = []float64{0, 0}491 }492 }493 }494 })495}496497// One nameless row is the source's hygiene: the pin is left out and the498// rest of the dataset stands.499func TestTranslateSkipsLoneUntitledPin(t *testing.T) {500 capture := fixtureCapture()501 for at := range capture.Datasets {502 if capture.Datasets[at].Slug == "historic-sites" {503 capture.Datasets[at].Features[0].Fields["NAME"] = ""504 }505 }506 out := translated(t, capture)507 pins := out.Groups[0].Categories[0].Locations508 if len(pins) != 1 || pins[0].Title != "Drake Park Gazebo" {509 t.Fatalf("pins are %+v, want only the titled one", pins)510 }511}512513func TestScrubDropsLiveURLTokens(t *testing.T) {514 cases := map[string]string{515 "plain prose stays": "plain prose stays",516 "see https://example.com for it": "see for it",517 "HTTP://LOUD.example": "",518 "": "",519 }520 for in, want := range cases {521 if got := scrub(in); got != want {522 t.Fatalf("scrub(%q) = %q, want %q", in, got, want)523 }524 }525}526527func TestFieldString(t *testing.T) {528 cases := []struct {529 in any530 want string531 }{532 {nil, ""},533 {" A.C. Lucas House ", "A.C. Lucas House"},534 {float64(97702), "97702"},535 {float64(-2208988800000), "-2208988800000"},536 {0.0202956, "0.0202956"},537 {true, "true"},538 }539 for _, tc := range cases {540 if got := FieldString(tc.in); got != tc.want {541 t.Fatalf("FieldString(%v) = %q, want %q", tc.in, got, tc.want)542 }543 }544}545546func TestRound7(t *testing.T) {547 if got := Round7(-121.308105091063071234); got != -121.3081051 {548 t.Fatalf("Round7 = %v", got)549 }550}551552// The curated table itself must be internally sound: slugs unique and553// well-formed, point datasets fully spelled, zone functions only on ground.554// Every city walks with the national enrichment appended, so a private555// city registered from cities_local.go is held to the same rules,556// collisions with the national slugs included.557func TestCuratedTableIsSound(t *testing.T) {558 for slug, city := range Cities {559 if slug != city.Slug {560 t.Fatalf("city %q keyed as %q", city.Slug, slug)561 }562 if city.MaxZoom < 1 || city.BBox[0] >= city.BBox[2] || city.BBox[1] >= city.BBox[3] {563 t.Fatalf("city %q declares no ground or no pyramid", slug)564 }565 seen := map[string]bool{}566 for _, dataset := range city.AllDatasets() {567 if dataset.Slug == "" || seen[dataset.Slug] {568 t.Fatalf("dataset slug %q is empty or doubled", dataset.Slug)569 }570 seen[dataset.Slug] = true571 if (dataset.ItemID == "") == (dataset.Server == "") || dataset.Title == "" {572 t.Fatalf("dataset %q wants exactly one identity, hub or national", dataset.Slug)573 }574 if dataset.Server != "" && len(dataset.Keep) == 0 {575 t.Fatalf("dataset %q pages a national layer without a row identity", dataset.Slug)576 }577 isPoint := dataset.Geometry == "point"578 if isPoint != (dataset.Group != "") {579 t.Fatalf("dataset %q: points and only points make pins", dataset.Slug)580 }581 if isPoint && (dataset.TitleOf == nil || dataset.Describe == nil) {582 t.Fatalf("dataset %q makes pins without spelling them", dataset.Slug)583 }584 if isPoint && dataset.ZoneOf != nil {585 t.Fatalf("dataset %q zones points", dataset.Slug)586 }587 if dataset.Geometry == "line" && dataset.ZoneOf != nil && dataset.StrokeWidth <= 0 {588 t.Fatalf("dataset %q zones lines without a ribbon", dataset.Slug)589 }590 // A label policy, when curated, must be a word the registry591 // admits, because it rides the declaration verbatim.592 if dataset.Label != "" {593 if err := semconv.Validate(semconv.EntityCollection,594 map[string]string{semconv.KeyLabelPolicy: dataset.Label}); err != nil {595 t.Fatalf("dataset %q label: %v", dataset.Slug, err)596 }597 }598 }599 }600}