clone

an offline map explorer where each world, from a game map to a city's open data, ships as one .atlas file

main
atlas / internal / enrich / volume.go
volume.go 14.2K
1package enrich23import (4 "encoding/json"5 "math"67 "github.com/FelineStateMachine/atlas/format/semconv"8)910// Volume is one reading of one volume, as the enrich lane sees it: the subject,11// the grounds within it, and what stands on each.12//13// It is the lane's own model, not the generate lane's document and not a14// bundle's payload, because the enrich lane may import neither the pipeline half15// that writes documents nor be imported by it. What travels between them is16// data: a caller that holds both -- the one binary's enrich subcommand -- adapts17// a document into this and this back into a document. See docs/enrich.md, "the18// seam".19type Volume struct {20 Slug string21 Title string22 // Source is where this reading came from. It is the only place a source is23 // named, and it survives into a bundle only through a ledger.24 Source Source25 Worlds []World26 // Icons is the artwork the volume's collections name, by key.27 Icons []Icon28}2930// Source is one capture source, in the two spellings a ledger and a card need.31type Source struct {32 Name string33 Label string34}3536// World is one ground within a volume.37type World struct {38 Slug string39 Title string40 // ID is the world's numeric identity where its source had one, zero41 // otherwise. Nothing here derives from it; it is carried so a round trip42 // through this model gives back the document it came from.43 ID int6444 Center Position45 // Capture and CapturedAt are the archived bytes this ground was read from.46 // They travel because a world contributed whole by one reading has to be47 // able to say where it came from when the volume is composed, exactly as a48 // world that was there all along does.49 Capture Capture50 CapturedAt string51 // Grid is the tile window this world's coordinates are projected against.52 // An enricher that measures distance needs it; one that only reads53 // attributes does not, and a volume assembled for such an enricher may54 // leave it zero.55 Grid Grid56 Lenses []Lens57 Collections []Collection58 Attrs map[string]string59 // Ledger is the world's provenance: the origin account it was composed60 // with, and one account per contribution folded in since.61 Ledger []Account62}6364// Capture names the archived bytes a world was read from: provenance, and a65// determinism receipt.66type Capture struct {67 Kind string68 ID int6469 Locator string70 ContentHash string71}7273// Grid is the tile window a world is cut from, and the world square it lands74// in. It is what turns the volume's own degrees into the world pixels every75// radius in this lane is measured in.76type Grid struct {77 SourceZoom int78 FirstTile int79 TileSize int80 Size int81}8283// Ready reports whether a grid can project. A zero grid cannot, and an enricher84// that needs one says so rather than measuring against nothing.85func (g Grid) Ready() bool { return g.TileSize > 0 && g.Size > 0 }8687// ProjectX and ProjectY mirror the reader's own projection: the volume's88// coordinates are spherical Mercator against its tile window, whether that89// window pictures a real planet or a game's artwork.90func (g Grid) ProjectX(lng float64) float64 {91 worldTiles := math.Pow(2, float64(g.SourceZoom))92 xTile := ((lng + 180) / 360) * worldTiles93 return (xTile - float64(g.FirstTile)) * float64(g.TileSize)94}9596func (g Grid) ProjectY(lat float64) float64 {97 worldTiles := math.Pow(2, float64(g.SourceZoom))98 yTile := (1 - math.Asinh(math.Tan(lat*math.Pi/180))/math.Pi) / 2 * worldTiles99 return (yTile - float64(g.FirstTile)) * float64(g.TileSize)100}101102// UnprojectLng and UnprojectLat send world pixels back to the volume's own103// degrees, which is how a donor feature lands on the serving ground.104func (g Grid) UnprojectLng(x float64) float64 {105 worldTiles := math.Pow(2, float64(g.SourceZoom))106 xTile := x/float64(g.TileSize) + float64(g.FirstTile)107 return (xTile/worldTiles)*360 - 180108}109110func (g Grid) UnprojectLat(y float64) float64 {111 worldTiles := math.Pow(2, float64(g.SourceZoom))112 yTile := y/float64(g.TileSize) + float64(g.FirstTile)113 return math.Atan(math.Sinh(math.Pi*(1-2*yTile/worldTiles))) * 180 / math.Pi114}115116// Position is a point in the volume's own projection.117type Position struct {118 Lat float64 `json:"lat"`119 Lng float64 `json:"lng"`120}121122// Lens is one picture of a world.123type Lens struct {124 Name string `json:"name"`125 // TileSet is the key the picture's pyramid is found under.126 TileSet string `json:"tileSet"`127 // Stamp is the pyramid's derivation stamp, where whatever derived it said128 // so. It is carried through a contribution so a lens attached by the enrich129 // lane says what it was made from, exactly as one composed from a source's130 // own tile set does.131 Stamp string `json:"stamp,omitempty"`132 // AlignedWith names the tile set this picture was resampled into, where the133 // picture is somebody else's raster warped onto this ground.134 AlignedWith string `json:"alignedWith,omitempty"`135}136137// Collection is an ordered group of features of one kind.138type Collection struct {139 ID int64 `json:"id"`140 Key string `json:"key,omitempty"`141 Title string `json:"title"`142 Group string `json:"group,omitempty"`143 Kind string `json:"kind"`144 Icon string `json:"icon,omitempty"`145 IconAsset string `json:"iconAsset,omitempty"`146 IconPicture bool `json:"iconPicture,omitempty"`147 Color string `json:"color,omitempty"`148 IconColor string `json:"iconColor,omitempty"`149 Visible bool `json:"visible"`150 Attrs map[string]string `json:"attrs,omitempty"`151 Features []Feature `json:"features,omitempty"`152}153154// Feature is one thing on a world, whatever its dimensionality.155type Feature struct {156 ID int64 `json:"id"`157 Title string `json:"title"`158 Subtitle string `json:"subtitle,omitempty"`159 Description string `json:"description,omitempty"`160 At *Position `json:"at,omitempty"`161 Center *Position `json:"center,omitempty"`162 Geometry []Geometry `json:"geometry,omitempty"`163 Member int64 `json:"member,omitempty"`164 Parent int64 `json:"parent,omitempty"`165 Links []Link `json:"links,omitempty"`166 Attrs map[string]string `json:"attrs,omitempty"`167}168169// Geometry is one GeoJSON part of a shape feature. Coordinates are carried170// opaquely: nothing in this lane rewrites them, so a source's numbers reach the171// payload unrounded.172type Geometry struct {173 Type string `json:"type"`174 Coordinates json.RawMessage `json:"coordinates"`175}176177// Link is a cross-reference to another feature of the same world.178type Link struct {179 Title string `json:"title"`180 Feature int64 `json:"feature"`181}182183// Icon is one piece of collection artwork, carried whole.184type Icon struct {185 Key string `json:"key"`186 File string `json:"file"`187 Data []byte `json:"data"`188}189190// The geometry kinds a collection may declare, spelled here so this lane does191// not reach into another's constants for them.192const (193 KindPoint = semconv.GeometryPoint194 KindPath = semconv.GeometryPath195 KindArea = semconv.GeometryArea196)197198// Clone is a deep copy. An enricher that wants to try something folds it into199// a clone and hands back operations, so the volume a caller passed in is never200// half-changed by an enricher that then failed.201func (v *Volume) Clone() *Volume {202 out := &Volume{Slug: v.Slug, Title: v.Title, Source: v.Source}203 out.Worlds = make([]World, 0, len(v.Worlds))204 for index := range v.Worlds {205 out.Worlds = append(out.Worlds, v.Worlds[index].Clone())206 }207 out.Icons = make([]Icon, 0, len(v.Icons))208 for _, icon := range v.Icons {209 out.Icons = append(out.Icons, Icon{Key: icon.Key, File: icon.File, Data: cloneBytes(icon.Data)})210 }211 return out212}213214// Clone is a deep copy of one world.215func (w *World) Clone() World {216 out := *w217 out.Lenses = append([]Lens(nil), w.Lenses...)218 out.Attrs = cloneAttrs(w.Attrs)219 out.Ledger = append([]Account(nil), w.Ledger...)220 out.Collections = make([]Collection, 0, len(w.Collections))221 for index := range w.Collections {222 out.Collections = append(out.Collections, w.Collections[index].Clone())223 }224 return out225}226227// Clone is a deep copy of one collection.228func (c *Collection) Clone() Collection {229 out := *c230 out.Attrs = cloneAttrs(c.Attrs)231 out.Features = make([]Feature, 0, len(c.Features))232 for index := range c.Features {233 out.Features = append(out.Features, c.Features[index].Clone())234 }235 return out236}237238// Clone is a deep copy of one feature.239func (f *Feature) Clone() Feature {240 out := *f241 out.Attrs = cloneAttrs(f.Attrs)242 if f.At != nil {243 at := *f.At244 out.At = &at245 }246 if f.Center != nil {247 center := *f.Center248 out.Center = &center249 }250 out.Links = append([]Link(nil), f.Links...)251 out.Geometry = make([]Geometry, 0, len(f.Geometry))252 for _, geometry := range f.Geometry {253 out.Geometry = append(out.Geometry, Geometry{254 Type: geometry.Type,255 Coordinates: append(json.RawMessage(nil), geometry.Coordinates...),256 })257 }258 if len(out.Geometry) == 0 {259 out.Geometry = nil260 }261 return out262}263264func cloneAttrs(attrs map[string]string) map[string]string {265 if attrs == nil {266 return nil267 }268 out := make(map[string]string, len(attrs))269 for key, value := range attrs {270 out[key] = value271 }272 return out273}274275func cloneBytes(data []byte) []byte {276 if data == nil {277 return nil278 }279 return append([]byte(nil), data...)280}281282// NewestCapture is the newest capture time across a volume's worlds: what a283// build of it is versioned by, and what decides which of two readings serves.284func (v *Volume) NewestCapture() string {285 newest := ""286 for _, world := range v.Worlds {287 if world.CapturedAt > newest {288 newest = world.CapturedAt289 }290 }291 return newest292}293294// World finds a world by slug.295func (v *Volume) World(slug string) *World {296 for index := range v.Worlds {297 if v.Worlds[index].Slug == slug {298 return &v.Worlds[index]299 }300 }301 return nil302}303304// Collection finds a collection by its numeric identity.305func (w *World) Collection(id int64) *Collection {306 for index := range w.Collections {307 if w.Collections[index].ID == id {308 return &w.Collections[index]309 }310 }311 return nil312}313314// Feature finds a feature by id, anywhere in the world. Identifiers share one315// space per world, so this is unambiguous.316func (w *World) Feature(id int64) (*Collection, *Feature) {317 for index := range w.Collections {318 collection := &w.Collections[index]319 for at := range collection.Features {320 if collection.Features[at].ID == id {321 return collection, &collection.Features[at]322 }323 }324 }325 return nil, nil326}327328// MergeIdentity is the name a collection meets other sources under: the329// declared merge identity its payload carries, then the key its source gave it,330// then its artwork key. A collection with none of the three meets nothing,331// which is the honest answer rather than a guessed pairing.332func MergeIdentity(c Collection) string {333 if key := c.Attrs[semconv.KeyCollectionKey]; key != "" {334 return key335 }336 if c.Key != "" {337 return c.Key338 }339 return c.Icon340}341342// Serving picks the reading a registry would serve: the newest capture wins,343// and among equal captures the earlier reading in the given order stands. It is344// pure and total, so two runs over the same readings always agree on which one345// the others are folded into.346func Serving(volumes []*Volume) int {347 winner := -1348 for index, volume := range volumes {349 if winner < 0 || volume.NewestCapture() > volumes[winner].NewestCapture() {350 winner = index351 }352 }353 return winner354}355356// Positions walks a geometry's coordinates and yields every position in it, as357// longitude and latitude. It is the one place a geometry's shape is looked358// into, and it looks only for pairs of numbers, so a MultiPolygon and a359// LineString are read by the same code.360func (g Geometry) Positions() [][2]float64 {361 var value any362 if err := json.Unmarshal(g.Coordinates, &value); err != nil {363 return nil364 }365 var out [][2]float64366 var walk func(node any)367 walk = func(node any) {368 list, ok := node.([]any)369 if !ok {370 return371 }372 if len(list) >= 2 {373 lng, lngOK := list[0].(float64)374 lat, latOK := list[1].(float64)375 if lngOK && latOK {376 out = append(out, [2]float64{lng, lat})377 return378 }379 }380 for _, child := range list {381 walk(child)382 }383 }384 walk(value)385 return out386}387388// Rings reads a geometry as polygon rings: a list of polygons, each a list of389// rings, each a list of positions. A geometry that is not made of rings reads390// as none, which is what a caller asking about ground wants to hear.391func (g Geometry) Rings() [][][][2]float64 {392 if g.Type != "Polygon" && g.Type != "MultiPolygon" {393 return nil394 }395 var value any396 if err := json.Unmarshal(g.Coordinates, &value); err != nil {397 return nil398 }399 if g.Type == "Polygon" {400 polygon := readRings(value)401 if polygon == nil {402 return nil403 }404 return [][][][2]float64{polygon}405 }406 list, ok := value.([]any)407 if !ok {408 return nil409 }410 var out [][][][2]float64411 for _, polygon := range list {412 if rings := readRings(polygon); rings != nil {413 out = append(out, rings)414 }415 }416 return out417}418419// Lines reads a geometry as lines. A geometry that is not made of lines reads420// as none.421func (g Geometry) Lines() [][][2]float64 {422 if g.Type != "LineString" && g.Type != "MultiLineString" {423 return nil424 }425 var value any426 if err := json.Unmarshal(g.Coordinates, &value); err != nil {427 return nil428 }429 if g.Type == "LineString" {430 line := readPositions(value)431 if line == nil {432 return nil433 }434 return [][][2]float64{line}435 }436 list, ok := value.([]any)437 if !ok {438 return nil439 }440 var out [][][2]float64441 for _, line := range list {442 if positions := readPositions(line); positions != nil {443 out = append(out, positions)444 }445 }446 return out447}448449func readRings(node any) [][][2]float64 {450 list, ok := node.([]any)451 if !ok {452 return nil453 }454 var out [][][2]float64455 for _, ring := range list {456 if positions := readPositions(ring); positions != nil {457 out = append(out, positions)458 }459 }460 return out461}462463func readPositions(node any) [][2]float64 {464 list, ok := node.([]any)465 if !ok {466 return nil467 }468 out := make([][2]float64, 0, len(list))469 for _, position := range list {470 pair, ok := position.([]any)471 if !ok || len(pair) < 2 {472 continue473 }474 lng, lngOK := pair[0].(float64)475 lat, latOK := pair[1].(float64)476 if lngOK && latOK {477 out = append(out, [2]float64{lng, lat})478 }479 }480 return out481}