1package doc23import (4 "math"5 "strconv"6)78// The whole-sphere equirectangular window: how a planet's global mosaic sits in9// the world square.10//11// A global equirectangular image is twice as wide as it is tall, so it fills12// the top half of the square: x runs the full width from longitude -180 to 180,13// y runs the top half from latitude 90 down to -90. Like the synthetic window14// above, this is the corpus's own convention rather than any one source's --15// NASA Trek's mosaics and the Blue Marble base map both flatten this way, and16// two sources describing one planet this way land on the same pixels, which is17// what lets their readings meet. The world declares the mapping through the18// registered conventions (atlas.geometry.equirect.px/.deg), spelled from these19// same constants, so the raster and the declaration cannot disagree.20const (21 // EquirectPx is the raster window the projection fills, as22 // atlas.geometry.equirect.px spells it: x, y, w, h in world pixels.23 EquirectPx = "0,0,8192,4096"24 // EquirectDeg is the ground that window pictures, as25 // atlas.geometry.equirect.deg spells it: west, north, east, south.26 EquirectDeg = "-180,90,180,-90"27)2829// EquirectCenter is where a reader opens on a whole-sphere world: the centre of30// the planet's picture, halfway across the world square and a quarter down it,31// which is where a 2:1 image's middle sits in a square.32func EquirectCenter() Position {33 return SyntheticPosition(SyntheticWorldSize/2, SyntheticWorldSize/4)34}3536// EquirectLevelExtent reports the last tile column and row of one square level37// of a whole-sphere pyramid: the full width of the world square, and the top38// half of its height. A crawler captures exactly these tiles and the deriver39// expects exactly these bounds, because both ask here.40func EquirectLevelExtent(zoom int) (maxX, maxY int) {41 if zoom == 0 {42 return 0, 043 }44 return 1<<zoom - 1, 1<<(zoom-1) - 145}4647// EquirectFrame declares a whole-sphere pyramid for the deriver. Every level48// down to zero is declared: a level left unsaid would be measured against the49// corpus's shared square, which this world does not sit in, and the half-height50// windows are what tell frame discovery where the planet actually is. Every51// mosaic of a body shares that window whatever its depth, which is what lets52// siblings ride one world as its lenses.53func EquirectFrame(maxZoom int, format string) *Frame {54 if format == "" {55 format = "jpg"56 }57 frame := &Frame{58 MaxZoom: maxZoom,59 Format: format,60 Windows: make(map[string]TileWindow, maxZoom+1),61 }62 for zoom := 0; zoom <= maxZoom; zoom++ {63 maxX, maxY := EquirectLevelExtent(zoom)64 frame.Windows[strconv.Itoa(zoom)] = TileWindow{MaxX: maxX, MaxY: maxY}65 }66 return frame67}6869// EquirectWorldPixel lands planetary coordinates on the picture. The window70// spans longitude -180..180 west to east and latitude 90..-90 top to bottom; a71// longitude in an east-positive 0..360 spelling wraps into that half-open72// window first. X crosses the full world square, y only its top half, because73// that is where a 2:1 image sits in a square.74func EquirectWorldPixel(longitude, latitude float64) (x, y float64) {75 wrapped := math.Mod(longitude+180, 360) - 18076 x = (wrapped + 180) / 360 * SyntheticWorldSize77 y = (90 - latitude) / 180 * (SyntheticWorldSize / 2)78 return x, y79}