Description
I'm interested in adding Earth distance and area measurements to Go. There are some API considerations I'd like input on.
Units
C++ S2Earth has three sets of methods, taking or returning meters (double), kilometers (double), and util::units::Meters
. The util class uses 32-bit floats, I think because it's part of a general-purpose physical units library. I see that S2Earth uses a radius of 6,371,010 meters, which is about 3 meters larger than the R2 value of 6,371,007.1809 meters reported by Wikipedia. We should match the C++ value for numerical stability between languages.
In Go we could create something like type Distance float64
and let that type do conversion between meters, kilometers, feet, miles, and any other useful measure, without the awkward precision loss from the C++ unit class. I think the following code will inline effectively, but haven't verified this hypothesis:
// Copyright 2025 Google LLC. All rights reserved.
const (
Meter Distance = 1e0
Kilometer = 1e3 * Meter
Centimeter = 1e-2 * Meter
Millimeter = 1e-3 * Meter
Foot = 0.3048 * Meter
Mile = 1609.344 * Meter
Inch = 0.0254 * Meter
)
func Meters(m float64) Distance { return Distance(m) * Meter }
func Kilometers(km float64) Distance { return Distance(km) * Kilometer }
func Centimeters(cm float64) Distance { return Distance(cm) * Centimeter }
func Millimeters(mm float64) Distance { return Distance(mm) * Millimeter }
func Feet(ft float64) Distance { return Distance(ft) * Foot }
func Miles(mi float64) Distance { return Distance(mi) * Mile }
func Inches(in float64) Distance { return Distance(in) * Inch }
func (m Distance) Millimeters() float64 { return float64(m / Millimeter) }
func (m Distance) Centimeters() float64 { return float64(m / Centimeter) }
func (m Distance) Meters() float64 { return float64(m) }
func (m Distance) Kilometers() float64 { return float64(m / Kilometer) }
func (m Distance) Inches() float64 { return float64(m / Inch) }
func (m Distance) Feet() float64 { return float64(m / Foot) }
func (m Distance) Miles() float64 { return float64(m / Mile) }
func (m Distance) String() string { return fmt.Sprintf("%0.3f meters", m) }
The constant values allow other code to declare constants by multiplication, e.g. if dist < 5 * unit.Mile
or radius = 6371010 * unit.Meter
or const Furlong = unit.Mile / 8
. One can also do if dist.Miles() < 5
. The constructors are useful when working with a float64 variable, instead of saying if dist < unit.Distance(minDistMiles) * unit.Mile
one can say if dist < unit.Miles(minDistMiles)
.
Using a Distance type that compiles to a float64 would let us have a single set of Earth functions, e.g. AngleToDistance(s1.Angle) Distance
and DistanceToAngle(Distance) s1.Angle
.
Area could work similarly, with square meters as the base value. This approach enables easy conversions like acres or hectares without cluttering the Earth measurement functions with every possible unit.
Packages
I can think of a couple approaches to package structure for this.
- Option 1: units and conversion in
earth
package.
var a, b s2.Point = …
dist := earth.AngleToDistance(a.Distance(b))
if dist < 5 * earth.Kilometers { … }
- Option 2: units in a
unit
package, conversion in anearth
package. This makes units look a little less weird if you're using S2 to model a sphere that's not Earth, e.g. Mars or a basketball.
var a, b s2.Point = …
dist := earth.AngleToDistance(a.Distance(b))
if dist < 5 * unit.Kilometers { … }
- Option 3: an
Earth
value in thes2
package. Units could be ins2
or aunit
package. (I think a separate package would be clearer, since distances are measures of s1 values and one could also create measurement conversions for r2.)
var a, b s2.Point = …
dist := s2.Earth.AngleToDistance(a.Distance(b))
if dist < 5 * unit.Kilometers { … }
I think this would entail declaring a type (maybe type Radius unit.Distance
) and methods on that type, with const Earth Radius = 6371010
This would allow users to do measurements on other spherical bodies, e.g. const Mars s2.Radius = 3389500
or const Basketball s2.Radius = 29.5 * unit.Inch
. I don't know if this receiver setup would have implications for inlining.