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 / format / semconv / behavior.go
behavior.go 4.4K
1package semconv23import (4 "fmt"5 "strconv"6 "strings"7)89// RenderAs answers how a point collection draws, from its attributes first10// and a legacy display-type field when no attribute speaks. This is the one11// rule readers used to hold as a private string compare, spelled once for12// every consumer: text is text, and everything else is a pin.13//14// The legacy parameter survives only at ingestion, where a producer speaks15// this key for captures that predate the conventions; no payload carries a16// display type.17func RenderAs(attrs map[string]string, legacyDisplayType string) string {18 if value, declared := attrs[KeyRenderAs]; declared {19 return value20 }21 if legacyDisplayType == RenderAsText {22 return RenderAsText23 }24 return RenderAsPin25}2627// LabelPolicy answers whether a collection's features wear their names on the28// map, in the producer's word alone: a reader's own override is the29// application's to lay over the top of this.30//31// The ladder, widest step first. A declared [KeyLabelPolicy] is the answer32// whatever the kind. Absent one, an area speaks -- [LabelAlways] -- because33// every map drew its region names before the key existed; a path waits; and a34// point collection speaks exactly when its curation drew it as text, since35// floating names are labels a producer pinned on rather than a different kind36// of thing to draw.37func LabelPolicy(kind string, attrs map[string]string) string {38 if value, declared := attrs[KeyLabelPolicy]; declared {39 return value40 }41 switch kind {42 case GeometryArea:43 return LabelAlways44 case GeometryPath:45 return LabelQuiet46 default:47 if RenderAs(attrs, "") == RenderAsText {48 return LabelAlways49 }50 return LabelQuiet51 }52}5354// Equirect is a declared flattening of a sphere: the raster window it fills55// in world pixels, and the ground that window pictures in degrees. West may56// exceed east numerically -- a world centered on the antimeridian -- and the57// span still reads west to east.58type Equirect struct {59 X, Y, W, H float6460 West, North, East, South float6461}6263// ParseEquirect reads the two mapping attributes into one transform, refusing64// windows with no area and grounds with no extent.65func ParseEquirect(px, deg string) (Equirect, error) {66 pxParts, err := floats(px, 4)67 if err != nil {68 return Equirect{}, fmt.Errorf("equirect px: %w", err)69 }70 degParts, err := floats(deg, 4)71 if err != nil {72 return Equirect{}, fmt.Errorf("equirect deg: %w", err)73 }74 e := Equirect{75 X: pxParts[0], Y: pxParts[1], W: pxParts[2], H: pxParts[3],76 West: degParts[0], North: degParts[1], East: degParts[2], South: degParts[3],77 }78 if e.W <= 0 || e.H <= 0 {79 return Equirect{}, fmt.Errorf("equirect window %q has no area", px)80 }81 if e.North == e.South || e.West == e.East {82 return Equirect{}, fmt.Errorf("equirect ground %q has no extent", deg)83 }84 return e, nil85}8687// EquirectOf reads a world's declared mapping out of its attributes. The88// false result means the world declares no equirectangular flattening, which89// for a plane is the ordinary case.90func EquirectOf(attrs map[string]string) (Equirect, bool, error) {91 px, hasPx := attrs[KeyGeometryEquirectPx]92 deg, hasDeg := attrs[KeyGeometryEquirectDeg]93 if !hasPx && !hasDeg {94 return Equirect{}, false, nil95 }96 mapping, err := ParseEquirect(px, deg)97 if err != nil {98 return Equirect{}, true, err99 }100 return mapping, true, nil101}102103// Apply lands true coordinates on the raster: degrees in, world pixels out.104func (e Equirect) Apply(lat, lon float64) (x, y float64) {105 x = e.X + (lon-e.West)/(e.East-e.West)*e.W106 y = e.Y + (e.North-lat)/(e.North-e.South)*e.H107 return x, y108}109110// Invert recovers true coordinates from the raster: world pixels in, degrees111// out. Every packed position runs back through this to stand on the sphere,112// so the mapping declared on the world is the whole story and no feature113// needs to carry its own.114func (e Equirect) Invert(x, y float64) (lat, lon float64) {115 lon = e.West + (x-e.X)/e.W*(e.East-e.West)116 lat = e.North - (y-e.Y)/e.H*(e.North-e.South)117 return lat, lon118}119120func floats(value string, count int) ([]float64, error) {121 parts := strings.Split(value, ",")122 if len(parts) != count {123 return nil, fmt.Errorf("%q wants %d numbers", value, count)124 }125 out := make([]float64, count)126 for at, part := range parts {127 parsed, err := strconv.ParseFloat(strings.TrimSpace(part), 64)128 if err != nil {129 return nil, fmt.Errorf("%q is not a number", part)130 }131 out[at] = parsed132 }133 return out, nil134}