1package cells23import (4 "math"56 "github.com/FelineStateMachine/atlas/format/semconv"7)89// The Web-Mercator flattening, and the real-mode geohash it anchors.10//11// A plane world that declares an earth Mercator window and names its body12// (bend-or) is a picture OF somewhere, and its geohash addresses are the real13// ones: classic WGS84 geohash — longitude first, five bits to a character,14// the same alphabet — run in degrees and put back on the picture through the15// declared flattening. This file is the server's half of that arithmetic,16// mirroring analysis/semconv/geometry.ts (the mapping) and the real mode of17// analysis/cellsystems/geohash.ts (the walk), because the server computes18// held-cell membership, the footer counts and the navigator's field length19// without asking the seam.20//21// The window reader is deliberately NOT [MappingOf]: that one is S2's, gated22// on `atlas.geometry.projection = equirect` exactly as the registry requires23// of a sphere. A plane's Mercator window carries no projection key at all —24// the registry's projection enum names only equirect, so there is nothing a25// plane could declare — and the `atlas.geometry.mercator.*` keys themselves26// say what the numbers are. A world that declares a DIFFERENT projection27// outright is declaring these numbers are something else, and is refused.2829// Mercator is a declared Web-Mercator flattening: x linear in longitude,30// y linear in the projected latitude asinh(tan φ), which is what a31// real-world tile window actually is (spec/registry.yaml, mercator.px).32type Mercator struct {33 X, Y, W, H float6434 West, North, East, South float6435 top, bottom float6436}3738// mercatorProjection is the projection value the seam's reader accepts for39// this window (analysis/semconv/geometry.ts). It is not in the registry's40// enum — no validated bundle can declare it — and it is spelled here only so41// a world that DOES declare a projection is read the same way on both lanes.42const mercatorProjection = "mercator"4344// MercatorOf reads a world's declared Mercator window out of its attributes,45// or refuses. It mirrors `mercatorMapping` in analysis/semconv/geometry.ts,46// relaxation and all: the pair is read when the projection key says47// `mercator` or says nothing, and refused under any other declaration, a48// missing half, a window with no area or a ground with no extent.49func MercatorOf(attrs map[string]string) (Mercator, bool) {50 if declared, has := attrs[semconv.KeyGeometryProjection]; has && declared != mercatorProjection {51 return Mercator{}, false52 }53 px, hasPx := attrs[semconv.KeyGeometryMercatorPx]54 deg, hasDeg := attrs[semconv.KeyGeometryMercatorDeg]55 if !hasPx || !hasDeg {56 return Mercator{}, false57 }58 // The same shape checks the seam's `declaredWindow` makes: inverting a59 // degenerate window would divide by zero, and silence beats a NaN.60 window, err := semconv.ParseEquirect(px, deg)61 if err != nil {62 return Mercator{}, false63 }64 m := Mercator{65 X: window.X, Y: window.Y, W: window.W, H: window.H,66 West: window.West, North: window.North, East: window.East, South: window.South,67 }68 m.top, m.bottom = mercatorY(m.North), mercatorY(m.South)69 return m, true70}7172// mercatorY is the projected latitude: asinh(tan φ), the quantity y is73// linear in.74func mercatorY(lat float64) float64 {75 return math.Asinh(math.Tan(lat * math.Pi / 180))76}7778// LatLng puts a point back on the earth: world pixels in, true degrees out.79// The x, y are the application's own — y **negative-down** — and the sign is80// flipped here, on the one line where the two conventions meet, exactly as81// [Mapping.LatLng] flips it for the equirect window.82func (m Mercator) LatLng(x, y float64) (lat, lng float64) {83 down := -y84 lng = m.West + (x-m.X)/m.W*(m.East-m.West)85 lat = math.Atan(math.Sinh(m.top-(down-m.Y)/m.H*(m.top-m.bottom))) * 180 / math.Pi86 return lat, lng87}8889// World lands true coordinates on the picture: degrees in, world pixels out,90// y negative-down — the reverse of [Mercator.LatLng] and the same one flip.91func (m Mercator) World(lat, lng float64) (x, y float64) {92 x = m.X + (lng-m.West)/(m.East-m.West)*m.W93 down := m.Y + (m.top-mercatorY(lat))/(m.top-m.bottom)*m.H94 return x, -down95}9697// EarthMercator answers the real-mode condition whole, and the mapping with98// it: the world is a PLANE (absent means plane — a sphere's geohash is the99// synthetic one over its picture), it declares an invertible Mercator100// window, and it names the body pictured as earth. Mars is a sphere with an101// equirect window and no earth body, and fails every clause.102func EarthMercator(attrs map[string]string) (Mercator, bool) {103 surface := attrs[semconv.KeyGeometrySurface]104 if surface != "" && surface != semconv.SurfacePlane {105 return Mercator{}, false106 }107 if attrs[semconv.KeyGeometryBody] != "earth" {108 return Mercator{}, false109 }110 return MercatorOf(attrs)111}112113// GeohashBaseDepth is where the anchored telescope's layers start: the114// smallest depth ≥ 1 at which a cell's longitude span fits the window's OR115// its latitude span does, capped at 9. Cell spans halve from 360°/180° per116// the 5-bit alternation — `d` characters split longitude ceil(5d/2) times117// and latitude floor(5d/2) — so every span is a power-of-two division and118// exact. For bend-or's window (0.32909° × 0.23650°) this is 4: the depth-4119// latitude span (0.17578125°) fits where the longitude span (0.3515625°)120// just misses.121func GeohashBaseDepth(m Mercator) int {122 lngSpan := m.East - m.West123 latSpan := m.North - m.South124 for depth := 1; depth < 9; depth++ {125 bits := 5 * depth126 lng := 360 / math.Pow(2, math.Ceil(float64(bits)/2))127 lat := 180 / math.Pow(2, math.Floor(float64(bits)/2))128 if lng <= lngSpan || lat <= latSpan {129 return depth130 }131 }132 return 9133}134135// GeohashDepthOf is the ground-aware hash-length floor: the base depth plus136// two — three layers of precision, wherever the layers start — which is 6137// for bend-or and [GeohashMaxDepth] everywhere the world is not anchored.138// It is the length the navigator's field accepts, so `maxlength` on the139// input and the cut in the record agree with the seam's `geohashDepth`.140func GeohashDepthOf(attrs map[string]string) int {141 if m, anchored := EarthMercator(attrs); anchored {142 return GeohashBaseDepth(m) + 2143 }144 return GeohashMaxDepth145}146147// geohashDegrees is the classic mask walk, forward: a hash to its own ground148// in true degrees. The same skip rule as [GeohashExtent]: a character149// outside the alphabet does not consume a halving.150func geohashDegrees(hash string) (west, south, east, north float64) {151 west, east = -180, 180152 south, north = -90, 90153 splitLng := true154 for _, character := range hash {155 value := indexOfLower(character)156 if value < 0 {157 continue158 }159 for _, mask := range Masks {160 if splitLng {161 middle := (west + east) / 2162 if value&mask != 0 {163 west = middle164 } else {165 east = middle166 }167 } else {168 middle := (south + north) / 2169 if value&mask != 0 {170 south = middle171 } else {172 north = middle173 }174 }175 splitLng = !splitLng176 }177 }178 return west, south, east, north179}180181// indexOfLower is the alphabet ordinal of a character, lowercased, or -1.182func indexOfLower(character rune) int {183 if character >= 'A' && character <= 'Z' {184 character += 'a' - 'A'185 }186 for at, own := range GeohashAlphabet {187 if own == character {188 return at189 }190 }191 return -1192}193194// GeohashRealCellAt is the real address under a point, at the depth asked:195// world pixels back to true degrees through the flattening, then the196// textbook walk. A point outside the declared window still has a real197// geohash — the ground is real earth — so nothing here refuses, which is198// the one place real mode and [GeohashCellAt] part company.199func GeohashRealCellAt(m Mercator, x, y float64, depth int) string {200 lat, lng := m.LatLng(x, y)201 west, east := -180.0, 180.0202 south, north := -90.0, 90.0203 splitLng := true204 hash := make([]byte, 0, depth)205 for range depth {206 value := 0207 for _, mask := range Masks {208 if splitLng {209 middle := (west + east) / 2210 if lng >= middle {211 value |= mask212 west = middle213 } else {214 east = middle215 }216 } else {217 middle := (south + north) / 2218 if lat >= middle {219 value |= mask220 south = middle221 } else {222 north = middle223 }224 }225 splitLng = !splitLng226 }227 hash = append(hash, GeohashAlphabet[value])228 }229 return string(hash)230}231232// GeohashRealHeld is the held-cell question for a real address: the point233// goes back to true degrees and the cell's own degree rectangle is asked,234// boundaries inclusive on every edge exactly as the synthetic rectangle is.235// The root is the whole earth and holds every point of the picture's plane.236func GeohashRealHeld(m Mercator, hash string) Held {237 if hash == "" {238 return func(float64, float64) bool { return true }239 }240 west, south, east, north := geohashDegrees(hash)241 return func(x, y float64) bool {242 lat, lng := m.LatLng(x, y)243 return lng >= west && lng <= east && lat >= south && lat <= north244 }245}246247// GeohashRealExtent is a real cell's rectangle in world pixels. Mercator248// keeps latitude lines horizontal, so the cell stays a rectangle and two249// corners are enough; the extents may overhang the declared window, which250// is honest — the cell is the planet's even where the picture stops.251func GeohashRealExtent(m Mercator, hash string) Extent {252 west, south, east, north := geohashDegrees(hash)253 minX, minY := m.World(south, west)254 maxX, maxY := m.World(north, east)255 return Extent{MinX: minX, MinY: minY, MaxX: maxX, MaxY: maxY}256}