Skip to content

Port S2Earth #151

Open
Open
Task
@flwyd

Description

@flwyd

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 an earth 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 the s2 package. Units could be in s2 or a unit 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.

Metadata

Metadata

Assignees

Type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions