package elevation import ( "encoding/binary" "fmt" "math" "os" mmap "github.com/edsrzf/mmap-go" ) // Dataset provides global elevation lookup, compatible with ruaumoko. // Binary format: int16 little-endian elevation values in metres, row-major (lat, lon). // Latitude axis: -90 to +90 (south to north), Longitude axis: 0 to 360 (wraps). // Resolution: 120 cells per degree (30 arc-seconds). const ( CellsPerDegree = 120 NumLats = 180*CellsPerDegree + 1 // 21601 NumLons = 360 * CellsPerDegree // 43200 DataSize = NumLats * NumLons * 2 // 1,866,326,400 bytes (~1.74 GiB) ) // Dataset is a memory-mapped global elevation grid. type Dataset struct { mm mmap.MMap file *os.File } // Open opens an existing elevation dataset file. func Open(path string) (*Dataset, error) { f, err := os.Open(path) if err != nil { return nil, fmt.Errorf("open elevation: %w", err) } info, err := f.Stat() if err != nil { f.Close() return nil, fmt.Errorf("stat elevation: %w", err) } if info.Size() != DataSize { f.Close() return nil, fmt.Errorf("elevation dataset should be %d bytes (was %d)", DataSize, info.Size()) } mm, err := mmap.Map(f, mmap.RDONLY, 0) if err != nil { f.Close() return nil, fmt.Errorf("mmap elevation: %w", err) } return &Dataset{mm: mm, file: f}, nil } // getCell reads the int16 elevation at grid indices (latIdx, lngIdx). func (d *Dataset) getCell(latIdx, lngIdx int) int16 { // Clamp latitude if latIdx < 0 { latIdx = 0 } if latIdx >= NumLats { latIdx = NumLats - 1 } // Wrap longitude lngIdx = lngIdx % NumLons if lngIdx < 0 { lngIdx += NumLons } off := (latIdx*NumLons + lngIdx) * 2 return int16(binary.LittleEndian.Uint16(d.mm[off : off+2])) } // Get returns the interpolated elevation in metres at the given coordinates. // lat: -90 to +90, lng: 0 to 360 (or -180 to 180, will be normalised). func (d *Dataset) Get(lat, lng float64) float64 { // Normalise longitude to [0, 360) if lng < 0 { lng += 360 } // Convert to cell coordinates latCell := (lat + 90.0) * CellsPerDegree lngCell := lng * CellsPerDegree lat0 := int(math.Floor(latCell)) lng0 := int(math.Floor(lngCell)) latFrac := latCell - float64(lat0) lngFrac := lngCell - float64(lng0) // Bilinear interpolation v00 := float64(d.getCell(lat0, lng0)) v10 := float64(d.getCell(lat0+1, lng0)) v01 := float64(d.getCell(lat0, lng0+1)) v11 := float64(d.getCell(lat0+1, lng0+1)) return (1-latFrac)*((1-lngFrac)*v00+lngFrac*v01) + latFrac*((1-lngFrac)*v10+lngFrac*v11) } // Close unmaps and closes the dataset. func (d *Dataset) Close() error { if d.mm != nil { d.mm.Unmap() d.mm = nil } if d.file != nil { err := d.file.Close() d.file = nil return err } return nil }