The pipeline has always projected onto a plane, and for a planet that was a quiet lie the pin cards had to correct in prose. Now the map itself declares what its raster pictures: a sphere, flattened by the equirectangular projection whose window and ground ride as attributes. The mapping is the whole story -- any reader runs a pin's packed position backward through it and stands on the planet -- and a test holds it to the same transform the translator projects features through, so the declaration and the projection can never drift apart. Each pin also carries the coordinates its source published, verbatim, as location attributes beside its card text: provenance in structure rather than trapped in prose. Every map that says nothing remains a plane, which every map was until the planets arrived, and the viewer draws spheres on its plane exactly as before -- the declared fallback -- until a renderer arrives that can do them justice.
- commit
- 5eb88b474ae0a43d33a521533160131e5f09cb84
- parent
- a61fc23562ec4edb82abb51082772a805a4210d5
- author
- Dami <dami.etoile@gmail.com>
| M | internal/ | 28 | +++++++++++++++++++ |
| M | internal/ | 44 | ++++++++++++++++++++++++++++++ |
| M | tools/ | 3 | +- |
@@ -191,6 +191,19 @@ func Translate(doc []byte) ([]byte, error) {
191191 InitialLatitude: mgdoc.SyntheticLatitude(mgdoc.WorldSize / 4),192192 InitialLongitude: mgdoc.SyntheticLongitude(mgdoc.WorldSize / 2),193193 Config: mgdoc.Config{TileSets: sets},194+ // The map says what it pictures: a sphere, flattened by the equi-195+ // rectangular projection into the top half of the world square. The196+ // mapping is the whole story -- any reader can run a pin's packed197+ // position backward through it and stand on the planet -- and it is198+ // exactly the transform worldPixel projects features through, so the199+ // two can never disagree without a test noticing.200+ Attrs: map[string]string{201+ semconv.KeyGeometrySurface: semconv.SurfaceSphere,202+ semconv.KeyGeometryProjection: semconv.ProjectionEquirect,203+ semconv.KeyGeometryEquirectPx: equirectPx,204+ semconv.KeyGeometryEquirectDeg: equirectDeg,205+ semconv.KeyGeometryBody: capture.Body,206+ },194207 Game: mgdoc.Game{195208 ID: gameID,196209 // A planet is a "game" to the pipeline, which is the point being@@ -244,6 +257,14 @@ func extension(given string) string {
244257 return "jpg"245258 }246259 260+// The declared flattening, spelled once for the map attributes and for261+// worldPixel below: the mosaic fills the top half of the world square and262+// pictures the whole ground, -180..180 west to east, 90..-90 top to bottom.263+const (264+ equirectPx = "0,0,8192,4096"265+ equirectDeg = "-180,90,180,-90"266+)267+247268 // worldPixel lands a feature's planetary coordinates on the picture. The248269 // mosaic spans longitude -180..180 west to east and latitude 90..-90 top to249270 // bottom; the Gazetteer speaks east-positive 0..360, so longitudes wrap into@@ -350,6 +371,13 @@ func buildGroups(capture *Capture, ids *mgdoc.IDSpace, scope string) ([]mgdoc.Gr
350371 Description: describe(feature),351372 Latitude: mgdoc.SyntheticLatitude(y),352373 Longitude: mgdoc.SyntheticLongitude(x),374+ // The coordinates as the Gazetteer published them, verbatim:375+ // provenance beside the synthetic position, so no reader ever376+ // has to parse a pin's card to learn where it truly is.377+ Attrs: map[string]string{378+ semconv.KeyGeoLat: strconv.FormatFloat(feature.Latitude, 'f', -1, 64),379+ semconv.KeyGeoLon: strconv.FormatFloat(feature.Longitude, 'f', -1, 64),380+ },353381 })354382 }355383 categories = append(categories, category)@@ -10,6 +10,7 @@ import (
1010 1111 "github.com/FelineStateMachine/atlas/internal/icons"1212 "github.com/FelineStateMachine/atlas/internal/mgdoc"13+ "github.com/FelineStateMachine/atlas/internal/semconv"1314 )1415 1516 // fixture is a small capture exercising everything Translate decides: two@@ -273,6 +274,49 @@ func TestTranslateRejectsBadCaptures(t *testing.T) {
273274 }274275 }275276 277+// TestDeclaredMappingInverts is the honesty check the geometry conventions278+// stand on: a pin's packed synthetic position, run through the viewer's279+// projection and backward through the mapping the map itself declares, must280+// recover the coordinates the Gazetteer published -- for every pin, without281+// the pin saying anything.282+func TestDeclaredMappingInverts(t *testing.T) {283+ m := translate(t, fixture())284+ mapping, err := semconv.ParseEquirect(285+ m.Attrs[semconv.KeyGeometryEquirectPx],286+ m.Attrs[semconv.KeyGeometryEquirectDeg])287+ if err != nil {288+ t.Fatal(err)289+ }290+ if m.Attrs[semconv.KeyGeometrySurface] != semconv.SurfaceSphere ||291+ m.Attrs[semconv.KeyGeometryBody] != "mars" {292+ t.Fatalf("geometry attrs: %v", m.Attrs)293+ }294+ published := map[string][2]float64{}295+ for _, feature := range fixture().Features {296+ published[feature.Name] = [2]float64{feature.Latitude, feature.Longitude}297+ }298+ for _, group := range m.Groups {299+ for _, category := range group.Categories {300+ for _, location := range category.Locations {301+ x := mgdoc.ProjectX(location.Longitude, mgdoc.SourceZoom, 0)302+ y := mgdoc.ProjectY(location.Latitude, mgdoc.SourceZoom, 0)303+ lat, lon := mapping.Invert(x, y)304+ if lon < 0 {305+ lon += 360306+ }307+ want := published[location.Title]308+ if math.Abs(lat-want[0]) > 1e-9 || math.Abs(lon-want[1]) > 1e-9 {309+ t.Errorf("%s recovered at %v,%v, published %v", location.Title, lat, lon, want)310+ }311+ // And the pin's own account agrees with what was published.312+ if location.Attrs[semconv.KeyGeoLat] == "" || location.Attrs[semconv.KeyGeoLon] == "" {313+ t.Errorf("%s carries no true coordinates", location.Title)314+ }315+ }316+ }317+ }318+}319+276320 // TestStandardIconsAreVendored holds the curation table to the library: a277321 // promise the translator writes into a capture's translation must be one the278322 // resolution pass can keep.@@ -25,7 +25,8 @@ import (
2525 // 3 origin provenance on every map; overlap merges across map slugs2626 // 4 semantic conventions: attributes ride every payload2727 // 5 standard icons resolved for categories that declare one28-const policyRevision = 528+// 6 geometry declared: spheres say so, pins carry true coordinates29+const policyRevision = 62930 3031 // writeBundles packs each game into its own .atlas file, named by game,3132 // capture day, and stamp. The directory is a registry, not a mirror: a new