clone

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

feat/atlas-next-horizon
atlas / internal / generate / compose / conventions.go
conventions.go 3.8K
1package compose23import (4 "fmt"5 "hash/fnv"6 "maps"78 "github.com/FelineStateMachine/atlas/format/semconv"9 "github.com/FelineStateMachine/atlas/internal/generate/curation"10 "github.com/FelineStateMachine/atlas/internal/generate/doc"11)1213// speakConventions makes every world answer in the shared vocabulary, and then14// holds the whole volume to the registry.15//16// Some of the vocabulary is a source's to speak -- how a collection draws, what17// its true coordinates are -- and arrives already spoken in the document. The18// rest is composition's, because it is composition that knows it: a collection's19// kind is mirrored so a reader may trust the attribute or the field beside it,20// the artwork's form is declared once it has actually been resolved, and the21// rim a world's markers wear comes from curation. Nothing here overwrites what a22// source said.23//24// Validation is producer-strict and it is the gate: an unregistered atlas key,25// a value outside its vocabulary, or a key on the wrong entity fails the build26// here, one step before a bundle, rather than riding into somebody's library.27func speakConventions(tables curation.Tables, volume string, worlds []composedWorld) error {28 for worldIndex := range worlds {29 world := &worlds[worldIndex]30 if world.IconOutset != "" {31 world.Attrs = withAttr(world.Attrs, semconv.KeyIconOutset, world.IconOutset)32 }33 if err := semconv.Validate(semconv.EntityWorld, world.Attrs); err != nil {34 return fmt.Errorf("world %s: %w", world.Slug, err)35 }36 for index := range world.Collections {37 collection := &world.Collections[index]38 if _, declared := collection.Attrs[semconv.KeyGeometryKind]; !declared {39 collection.Attrs = withAttr(collection.Attrs, semconv.KeyGeometryKind, collection.Kind)40 }41 if collection.Kind == doc.KindPoint {42 // Where two sources are known to spell one concept two ways,43 // the shared name rides the collection itself, so a later merge44 // reads identity off the payload rather than a table of its own.45 if shared := tables.CollectionEquivalent(volume, collection.Icon); shared != "" {46 if _, declared := collection.Attrs[semconv.KeyCollectionKey]; !declared {47 collection.Attrs = withAttr(collection.Attrs, semconv.KeyCollectionKey, shared)48 }49 }50 if collection.IconAsset != "" {51 kind := semconv.IconKindGlyph52 if collection.IconPicture {53 kind = semconv.IconKindPicture54 }55 collection.Attrs = withAttr(collection.Attrs, semconv.KeyIconKind, kind)56 }57 }58 if err := semconv.Validate(semconv.EntityCollection, collection.Attrs); err != nil {59 return fmt.Errorf("world %s collection %q: %w", world.Slug, collection.Title, err)60 }61 for _, feature := range collection.Features {62 if err := semconv.Validate(semconv.EntityFeature, feature.Attrs); err != nil {63 return fmt.Errorf("world %s feature %q: %w", world.Slug, feature.Title, err)64 }65 }66 }67 }68 return nil69}7071// withAttr sets one attribute on a copy of the map, never on the map itself:72// a split sheet's pieces share their source's attributes by reference, and73// speaking for one piece must not put words in another's mouth.74func withAttr(attrs map[string]string, key, value string) map[string]string {75 out := make(map[string]string, len(attrs)+1)76 maps.Copy(out, attrs)77 out[key] = value78 return out79}8081// claimID derives a collection's number from the world it sits in and the key82// it declared. See numberCollections for why, and for the collision rule.83func claimID(world int64, key string, used map[int64]string) (int64, error) {84 hash := fnv.New32a()85 fmt.Fprintf(hash, "%d:collection:%s", world, key)86 id := int64(hash.Sum32() & 0x7fffffff)87 if id == 0 {88 id = 189 }90 if holder, taken := used[id]; taken {91 return 0, fmt.Errorf("collection %q collides with %q on id %d", key, holder, id)92 }93 used[id] = key94 return id, nil95}