Skip to content

Commit 926b01c

Browse files
rsneddsymonds
authored andcommitted
s2: Add Rect.Centroid.
Signed-off-by: David Symonds <dsymonds@golang.org>
1 parent a3f1189 commit 926b01c

File tree

2 files changed

+147
-1
lines changed

2 files changed

+147
-1
lines changed

s2/rect.go

Lines changed: 70 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -637,5 +637,74 @@ func bisectorIntersection(lat r1.Interval, lng s1.Angle) Point {
637637
return orthoLng.PointCross(PointFromLatLng(orthoBisector))
638638
}
639639

640+
// Centroid returns the true centroid of the given Rect multiplied by its
641+
// surface area. The result is not unit length, so you may want to normalize it.
642+
// Note that in general the centroid is *not* at the center of the rectangle, and
643+
// in fact it may not even be contained by the rectangle. (It is the "center of
644+
// mass" of the rectangle viewed as subset of the unit sphere, i.e. it is the
645+
// point in space about which this curved shape would rotate.)
646+
//
647+
// The reason for multiplying the result by the rectangle area is to make it
648+
// easier to compute the centroid of more complicated shapes. The centroid
649+
// of a union of disjoint regions can be computed simply by adding their
650+
// Centroid results.
651+
func (r Rect) Centroid() Point {
652+
// When a sphere is divided into slices of constant thickness by a set
653+
// of parallel planes, all slices have the same surface area. This
654+
// implies that the z-component of the centroid is simply the midpoint
655+
// of the z-interval spanned by the Rect.
656+
//
657+
// Similarly, it is easy to see that the (x,y) of the centroid lies in
658+
// the plane through the midpoint of the rectangle's longitude interval.
659+
// We only need to determine the distance "d" of this point from the
660+
// z-axis.
661+
//
662+
// Let's restrict our attention to a particular z-value. In this
663+
// z-plane, the Rect is a circular arc. The centroid of this arc
664+
// lies on a radial line through the midpoint of the arc, and at a
665+
// distance from the z-axis of
666+
//
667+
// r * (sin(alpha) / alpha)
668+
//
669+
// where r = sqrt(1-z^2) is the radius of the arc, and "alpha" is half
670+
// of the arc length (i.e., the arc covers longitudes [-alpha, alpha]).
671+
//
672+
// To find the centroid distance from the z-axis for the entire
673+
// rectangle, we just need to integrate over the z-interval. This gives
674+
//
675+
// d = Integrate[sqrt(1-z^2)*sin(alpha)/alpha, z1..z2] / (z2 - z1)
676+
//
677+
// where [z1, z2] is the range of z-values covered by the rectangle.
678+
// This simplifies to
679+
//
680+
// d = sin(alpha)/(2*alpha*(z2-z1))*(z2*r2 - z1*r1 + theta2 - theta1)
681+
//
682+
// where [theta1, theta2] is the latitude interval, z1=sin(theta1),
683+
// z2=sin(theta2), r1=cos(theta1), and r2=cos(theta2).
684+
//
685+
// Finally, we want to return not the centroid itself, but the centroid
686+
// scaled by the area of the rectangle. The area of the rectangle is
687+
//
688+
// A = 2 * alpha * (z2 - z1)
689+
//
690+
// which fortunately appears in the denominator of "d".
691+
692+
if r.IsEmpty() {
693+
return Point{}
694+
}
695+
696+
z1 := math.Sin(r.Lat.Lo)
697+
z2 := math.Sin(r.Lat.Hi)
698+
r1 := math.Cos(r.Lat.Lo)
699+
r2 := math.Cos(r.Lat.Hi)
700+
701+
alpha := 0.5 * r.Lng.Length()
702+
r0 := math.Sin(alpha) * (r2*z2 - r1*z1 + r.Lat.Length())
703+
lng := r.Lng.Center()
704+
z := alpha * (z2 + z1) * (z2 - z1) // scaled by the area
705+
706+
return Point{r3.Vector{r0 * math.Cos(lng), r0 * math.Sin(lng), z}}
707+
}
708+
640709
// BUG: The major differences from the C++ version are:
641-
// - GetCentroid, Get*Distance, Vertex, InteriorContains(LatLng|Rect|Point)
710+
// - Get*Distance, Vertex, InteriorContains(LatLng|Rect|Point)

s2/rect_test.go

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import (
1919
"testing"
2020

2121
"github.com/golang/geo/r1"
22+
"github.com/golang/geo/r2"
2223
"github.com/golang/geo/r3"
2324
"github.com/golang/geo/s1"
2425
)
@@ -1129,3 +1130,79 @@ func TestRectApproxEqual(t *testing.T) {
11291130
}
11301131
}
11311132
}
1133+
1134+
func TestRectCentroidEmptyFull(t *testing.T) {
1135+
// Empty and full rectangles.
1136+
if got, want := EmptyRect().Centroid(), (Point{}); !got.ApproxEqual(want) {
1137+
t.Errorf("%v.Centroid() = %v, want %v", EmptyRect(), got, want)
1138+
}
1139+
if got, want := FullRect().Centroid().Norm(), epsilon; !float64Eq(got, want) {
1140+
t.Errorf("%v.Centroid().Norm() = %v, want %v", FullRect(), got, want)
1141+
}
1142+
}
1143+
1144+
func testRectCentroidSplitting(t *testing.T, r Rect, leftSplits int) {
1145+
// Recursively verify that when a rectangle is split into two pieces, the
1146+
// centroids of the children sum to give the centroid of the parent.
1147+
var child0, child1 Rect
1148+
if oneIn(2) {
1149+
lat := randomUniformFloat64(r.Lat.Lo, r.Lat.Hi)
1150+
child0 = Rect{r1.Interval{r.Lat.Lo, lat}, r.Lng}
1151+
child1 = Rect{r1.Interval{lat, r.Lat.Hi}, r.Lng}
1152+
} else {
1153+
lng := randomUniformFloat64(r.Lng.Lo, r.Lng.Hi)
1154+
child0 = Rect{r.Lat, s1.Interval{r.Lng.Lo, lng}}
1155+
child1 = Rect{r.Lat, s1.Interval{lng, r.Lng.Hi}}
1156+
}
1157+
1158+
if got, want := r.Centroid().Sub(child0.Centroid().Vector).Sub(child1.Centroid().Vector).Norm(), 1e-15; got > want {
1159+
t.Errorf("%v.Centroid() - %v.Centroid() - %v.Centroid = %v, want ~0", r, child0, child1, got)
1160+
}
1161+
if leftSplits > 0 {
1162+
testRectCentroidSplitting(t, child0, leftSplits-1)
1163+
testRectCentroidSplitting(t, child1, leftSplits-1)
1164+
}
1165+
}
1166+
1167+
func TestRectCentroidFullRange(t *testing.T) {
1168+
// Rectangles that cover the full longitude range.
1169+
for i := 0; i < 100; i++ {
1170+
lat1 := randomUniformFloat64(-math.Pi/2, math.Pi/2)
1171+
lat2 := randomUniformFloat64(-math.Pi/2, math.Pi/2)
1172+
r := Rect{r1.Interval{lat1, lat2}, s1.FullInterval()}
1173+
centroid := r.Centroid()
1174+
if want := 0.5 * (math.Sin(lat1) + math.Sin(lat2)) * r.Area(); !float64Near(want, centroid.Z, epsilon) {
1175+
t.Errorf("%v.Centroid().Z was %v, want %v", r, centroid.Z, want)
1176+
}
1177+
if got := (r2.Point{centroid.X, centroid.Y}.Norm()); got > epsilon {
1178+
t.Errorf("%v.Centroid().Norm() was %v, want > %v ", r, got, epsilon)
1179+
}
1180+
}
1181+
1182+
// Rectangles that cover the full latitude range.
1183+
for i := 0; i < 100; i++ {
1184+
lat1 := randomUniformFloat64(-math.Pi, math.Pi)
1185+
lat2 := randomUniformFloat64(-math.Pi, math.Pi)
1186+
r := Rect{r1.Interval{-math.Pi / 2, math.Pi / 2}, s1.Interval{lat1, lat2}}
1187+
centroid := r.Centroid()
1188+
1189+
if got, want := math.Abs(centroid.Z), epsilon; got > want {
1190+
t.Errorf("math.Abs(%v.Centroid().Z) = %v, want <= %v", r, got, want)
1191+
}
1192+
1193+
if got, want := LatLngFromPoint(centroid).Lng.Radians(), r.Lng.Center(); !float64Near(got, want, epsilon) {
1194+
t.Errorf("%v.Lng.Radians() = %v, want %v", centroid, got, want)
1195+
}
1196+
1197+
alpha := 0.5 * r.Lng.Length()
1198+
if got, want := (r2.Point{centroid.X, centroid.Y}.Norm()), (0.25 * math.Pi * math.Sin(alpha) / alpha * r.Area()); !float64Near(got, want, epsilon) {
1199+
t.Errorf("%v.Centroid().Norm() = %v, want ~%v", got, want, epsilon)
1200+
}
1201+
}
1202+
1203+
// Finally, verify that when a rectangle is recursively split into pieces,
1204+
// the centroids of the pieces add to give the centroid of their parent.
1205+
// To make the code simpler we avoid rectangles that cross the 180 degree
1206+
// line of longitude.
1207+
testRectCentroidSplitting(t, Rect{r1.Interval{-math.Pi / 2, math.Pi / 2}, s1.Interval{-math.Pi, math.Pi}}, 10)
1208+
}

0 commit comments

Comments
 (0)