1package main23import (4 "strconv"56 "github.com/FelineStateMachine/atlas/internal/bundle"7)89// The catalog was once one file holding every location of every game, fetched10// whole at startup and held for the session. Adding a game made the wait11// longer for anyone who never opened it. A bundle instead carries one payload12// per map, so opening Atlas costs the same however many games are installed.13//14// Each map splits three ways by when its parts are needed:15//16// - maps/<slug>.json its layers and collections, read when the map opens17// - maps/<slug>.bin its point features, packed, read when the map opens18// - maps/<slug>.text descriptions and cross-references, read when a feature is opened19//20// Descriptions were half the catalog by weight and are read one feature at a21// time, so they are the part most worth not loading. Point features are22// overwhelmingly numbers, and numbers written as text cost several times what23// they measure; shape features are mostly geometry either way, so they ride24// the JSON inline under the collection that declares them.2526// worldDetail is everything needed to draw a map except its point features,27// which travel packed alongside. Collections is the map's whole structure in28// one flat array, order significant: the packed payload's owner column29// indexes it.30type worldDetail struct {31 // Grid travels with the layers it describes: a map cut from a window of its32 // own is the only one that carries it, and it is needed exactly when the33 // map is opened.34 Grid *worldGrid `json:"grid,omitempty"`35 Lenses []lens `json:"lenses"`36 Collections []wireCollection `json:"collections"`37 // Attrs is the map speaking the shared conventions -- its geometry, its38 // marker outset -- for any reader that knows the vocabulary.39 Attrs map[string]string `json:"attrs,omitempty"`40 // Merged is the provenance of any other sources folded into this map.41 // The viewer ignores it; it exists so the merged payload carries its own42 // account.43 Merged []mergedSource `json:"merged,omitempty"`44}4546// wireCollection is one collection as the payload spells it. A point47// collection carries no inline features -- its members ride the packed48// payload, owned by this collection's index -- where a path or area49// collection inlines its features whole.50type wireCollection struct {51 ID int64 `json:"id"`52 Title string `json:"title"`53 Group string `json:"group,omitempty"`54 Kind string `json:"kind"`55 Icon string `json:"icon,omitempty"`56 IconAsset string `json:"iconAsset,omitempty"`57 IconPicture bool `json:"iconPicture,omitempty"`58 Color string `json:"color,omitempty"`59 IconColor string `json:"iconColor,omitempty"`60 Visible bool `json:"visible"`61 Attrs map[string]string `json:"attrs,omitempty"`62 Features []wireFeature `json:"features,omitempty"`63}6465// wireFeature is one shape feature as the payload inlines it. Its prose66// defers into the text payload behind the hasText marker, exactly the way a67// point feature's does; its attributes stay inline, because a card needs them68// the moment ground is asked about.69type wireFeature struct {70 ID int64 `json:"id"`71 Title string `json:"title"`72 Subtitle string `json:"subtitle,omitempty"`73 HasText bool `json:"hasText,omitempty"`74 Parent *int64 `json:"parent,omitempty"`75 Center *coordinate `json:"center,omitempty"`76 Shard int64 `json:"shard,omitempty"`77 Geometry []geometry `json:"geometry"`78 Attrs map[string]string `json:"attrs,omitempty"`79}8081// locationText holds what only a selected feature needs.82type locationText struct {83 Description string `json:"d,omitempty"`84 Links []catalogLink `json:"l,omitempty"`85 // Attrs carries a point feature's own conventions -- its true planetary86 // coordinates, when a source published them -- read when its card opens,87 // like everything else here.88 Attrs map[string]string `json:"a,omitempty"`89}9091// buildPayload splits one map three ways: its layers and collections as a92// detail structure, its point features packed, and its descriptions keyed by93// feature -- every kind defers its prose the same way. Feature identifiers94// share one claimed space, so one map never keys two things alike.95//96// Point collections are written first and shape collections after, so a97// packed location's owner is simply the ordinal of its collection among the98// points -- and the wire order is the legend order: categories as the source99// grouped them, then the curated shape collections, then the implicit one.100func buildPayload(m catalogWorld) (worldDetail, []byte, map[string]locationText) {101 detail := worldDetail{Grid: m.Grid, Lenses: m.Lenses, Attrs: m.Attrs, Merged: m.Merged}102 var locations []bundle.Location103 text := make(map[string]locationText)104 for _, collection := range m.Collections {105 if collection.Kind != kindPoint {106 continue107 }108 owner := uint16(len(detail.Collections))109 detail.Collections = append(detail.Collections, listedCollection(collection))110 for _, pin := range collection.Features {111 var member int64112 if pin.Member != nil {113 member = *pin.Member114 }115 locations = append(locations, bundle.Location{116 ID: pin.ID,117 Title: pin.Title,118 Lat: pin.Lat,119 Lng: pin.Lng,120 Member: member,121 Shard: pin.Shard,122 Owner: owner,123 })124 if pin.Description != "" || len(pin.Links) > 0 || len(pin.Attrs) > 0 {125 text[strconv.FormatInt(pin.ID, 10)] = locationText{126 Description: pin.Description,127 Links: pin.Links,128 Attrs: pin.Attrs,129 }130 }131 }132 }133 for _, collection := range m.Collections {134 if collection.Kind == kindPoint {135 continue136 }137 listed := listedCollection(collection)138 for _, shape := range collection.Features {139 feature := wireFeature{140 ID: shape.ID,141 Title: shape.Title,142 Subtitle: shape.Subtitle,143 Parent: shape.Parent,144 Center: shape.Center,145 Shard: shape.Shard,146 Geometry: shape.Geometry,147 Attrs: shape.Attrs,148 }149 if shape.Description != "" {150 text[strconv.FormatInt(shape.ID, 10)] = locationText{Description: shape.Description}151 feature.HasText = true152 }153 listed.Features = append(listed.Features, feature)154 }155 detail.Collections = append(detail.Collections, listed)156 }157 return detail, bundle.PackLocations(locations), text158}159160// listedCollection is the collection as the wire lists it, features left for161// the caller to fill in or leave packed.162func listedCollection(collection worldCollection) wireCollection {163 return wireCollection{164 ID: collection.ID,165 Title: collection.Title,166 Group: collection.Group,167 Kind: collection.Kind,168 Icon: collection.Icon,169 IconAsset: collection.IconAsset,170 IconPicture: collection.IconPicture,171 Color: collection.Color,172 IconColor: collection.IconColor,173 Visible: collection.Visible,174 Attrs: collection.Attrs,175 }176}