forked from gsn/predictor
113 lines
2.7 KiB
Go
113 lines
2.7 KiB
Go
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
|
|
}
|