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 / doc / synthetic.go
synthetic.go 3.4K
1package doc23import "math"45// Synthetic coordinates: how a source that publishes a picture rather than a6// planet says where something is.7//8// A document's positions are latitude and longitude in the volume's own9// projection (see the package comment). For a real planet those are the real10// thing. For a picture -- a game's sheet, a rendered city, a flattened mosaic --11// the ground is an image, and what a source knows about a feature is which pixel12// of that image it sits on. These functions turn a pixel into the position that13// projects back onto exactly that pixel, by inverting the same spherical14// Mercator every reader uses.15//16// The window is the corpus's own: a 32-tile square, 8192 pixels across, whose17// pyramid comes to rest in a single tile. It is not the format's -- format v318// carries whatever window a volume declares -- and it is not any one source's19// either, which is why it lives here rather than in a source directory. A source20// publishing a picture measures in these pixels; nothing downstream has to know21// that the degrees were once pixels, because the projection is the same one it22// would have applied anyway.23//24// Two sources describing the same ground this way land on the same numbers,25// which is what lets a merge compare them. A source whose ground is a planet26// ignores all of this and publishes what it knows.27const (28 // SyntheticZoom is the tile zoom whose pixels are the world's units: the29 // square is 32 tiles across, and 2^5 is 32.30 SyntheticZoom = 531 // SyntheticTileSize is the edge of one tile, in pixels.32 SyntheticTileSize = 25633 // SyntheticWorldSize is the edge of the world square, in pixels.34 SyntheticWorldSize = SyntheticTileSize << SyntheticZoom35)3637// SyntheticPosition is where a pixel of a source's own picture stands, measured38// from the top left of the world square.39func SyntheticPosition(x, y float64) Position {40 return Position{Lat: SyntheticLat(y), Lng: SyntheticLng(x)}41}4243// SyntheticLng inverts the reader's projection across the square.44func SyntheticLng(x float64) float64 {45 worldTiles := math.Pow(2, SyntheticZoom)46 return ((x/SyntheticTileSize)/worldTiles)*360 - 18047}4849// SyntheticLat inverts the reader's projection down the square.50func SyntheticLat(y float64) float64 {51 worldTiles := math.Pow(2, SyntheticZoom)52 yTile := y / SyntheticTileSize53 return math.Atan(math.Sinh(math.Pi*(1-2*yTile/worldTiles))) * 180 / math.Pi54}5556// Title spells a slug out for a person, which is all the naming some sources57// give a thing: "night-city" becomes "Night City".58func Title(slug string) string {59 out := []rune(slug)60 capitalize := true61 for index, r := range out {62 switch {63 case r == '-':64 out[index] = ' '65 capitalize = true66 case capitalize && r >= 'a' && r <= 'z':67 out[index] = r - ('a' - 'A')68 capitalize = false69 default:70 capitalize = false71 }72 }73 return string(out)74}7576// Slugify spells a title the way an artwork key or a world slug is spelled:77// lower case, everything else run together as a single hyphen.78func Slugify(value string) string {79 out := make([]rune, 0, len(value))80 for _, r := range value {81 switch {82 case r >= 'a' && r <= 'z', r >= '0' && r <= '9':83 out = append(out, r)84 case r >= 'A' && r <= 'Z':85 out = append(out, r+'a'-'A')86 default:87 if len(out) > 0 && out[len(out)-1] != '-' {88 out = append(out, '-')89 }90 }91 }92 for len(out) > 0 && out[len(out)-1] == '-' {93 out = out[:len(out)-1]94 }95 return string(out)96}