// intersectsLatEdge reports if the edge AB intersects the given edge of constant // latitude. Requires the points to have unit length. func intersectsLatEdge(a, b Point, lat s1.Angle, lng s1.Interval) bool { // Unfortunately, lines of constant latitude are curves on // the sphere. They can intersect a straight edge in 0, 1, or 2 points. // First, compute the normal to the plane AB that points vaguely north. z := a.PointCross(b) if z.Z < 0 { z = Point{z.Mul(-1)} } // Extend this to an orthonormal frame (x,y,z) where x is the direction // where the great circle through AB achieves its maximium latitude. y := z.PointCross(PointFromCoords(0, 0, 1)) x := y.Cross(z.Vector) // Compute the angle "theta" from the x-axis (in the x-y plane defined // above) where the great circle intersects the given line of latitude. sinLat := math.Sin(float64(lat)) if math.Abs(sinLat) >= x.Z { // The great circle does not reach the given latitude. return false } cosTheta := sinLat / x.Z sinTheta := math.Sqrt(1 - cosTheta*cosTheta) theta := math.Atan2(sinTheta, cosTheta) // The candidate intersection points are located +/- theta in the x-y // plane. For an intersection to be valid, we need to check that the // intersection point is contained in the interior of the edge AB and // also that it is contained within the given longitude interval "lng". // Compute the range of theta values spanned by the edge AB. abTheta := s1.IntervalFromEndpoints( math.Atan2(a.Dot(y.Vector), a.Dot(x)), math.Atan2(b.Dot(y.Vector), b.Dot(x))) if abTheta.Contains(theta) { // Check if the intersection point is also in the given lng interval. isect := x.Mul(cosTheta).Add(y.Mul(sinTheta)) if lng.Contains(math.Atan2(isect.Y, isect.X)) { return true } } if abTheta.Contains(-theta) { // Check if the other intersection point is also in the given lng interval. isect := x.Mul(cosTheta).Sub(y.Mul(sinTheta)) if lng.Contains(math.Atan2(isect.Y, isect.X)) { return true } } return false }
// testClipToPaddedFace performs a comprehensive set of tests across all faces and // with random padding for the given points. // // We do this by defining an (x,y) coordinate system for the plane containing AB, // and converting points along the great circle AB to angles in the range // [-Pi, Pi]. We then accumulate the angle intervals spanned by each // clipped edge; the union over all 6 faces should approximately equal the // interval covered by the original edge. func testClipToPaddedFace(t *testing.T, a, b Point) { a = Point{a.Normalize()} b = Point{b.Normalize()} if a.Vector == b.Mul(-1) { return } norm := Point{a.PointCross(b).Normalize()} aTan := Point{norm.Cross(a.Vector)} padding := 0.0 if !oneIn(10) { padding = 1e-10 * math.Pow(1e-5, randomFloat64()) } xAxis := a yAxis := aTan // Given the points A and B, we expect all angles generated from the clipping // to fall within this range. expectedAngles := s1.Interval{0, float64(a.Angle(b.Vector))} if expectedAngles.IsInverted() { expectedAngles = s1.Interval{expectedAngles.Hi, expectedAngles.Lo} } maxAngles := expectedAngles.Expanded(faceClipErrorRadians) var actualAngles s1.Interval for face := 0; face < 6; face++ { aUV, bUV, intersects := ClipToPaddedFace(a, b, face, padding) if !intersects { continue } aClip := Point{faceUVToXYZ(face, aUV.X, aUV.Y).Normalize()} bClip := Point{faceUVToXYZ(face, bUV.X, bUV.Y).Normalize()} desc := fmt.Sprintf("on face %d, a=%v, b=%v, aClip=%v, bClip=%v,", face, a, b, aClip, bClip) if got := math.Abs(aClip.Dot(norm.Vector)); got > faceClipErrorRadians { t.Errorf("%s abs(%v.Dot(%v)) = %v, want <= %v", desc, aClip, norm, got, faceClipErrorRadians) } if got := math.Abs(bClip.Dot(norm.Vector)); got > faceClipErrorRadians { t.Errorf("%s abs(%v.Dot(%v)) = %v, want <= %v", desc, bClip, norm, got, faceClipErrorRadians) } if float64(aClip.Angle(a.Vector)) > faceClipErrorRadians { if got := math.Max(math.Abs(aUV.X), math.Abs(aUV.Y)); !float64Eq(got, 1+padding) { t.Errorf("%s the largest component of %v = %v, want %v", desc, aUV, got, 1+padding) } } if float64(bClip.Angle(b.Vector)) > faceClipErrorRadians { if got := math.Max(math.Abs(bUV.X), math.Abs(bUV.Y)); !float64Eq(got, 1+padding) { t.Errorf("%s the largest component of %v = %v, want %v", desc, bUV, got, 1+padding) } } aAngle := math.Atan2(aClip.Dot(yAxis.Vector), aClip.Dot(xAxis.Vector)) bAngle := math.Atan2(bClip.Dot(yAxis.Vector), bClip.Dot(xAxis.Vector)) // Rounding errors may cause bAngle to be slightly less than aAngle. // We handle this by constructing the interval with FromPointPair, // which is okay since the interval length is much less than math.Pi. faceAngles := s1.IntervalFromEndpoints(aAngle, bAngle) if faceAngles.IsInverted() { faceAngles = s1.Interval{faceAngles.Hi, faceAngles.Lo} } if !maxAngles.ContainsInterval(faceAngles) { t.Errorf("%s %v.ContainsInterval(%v) = false, but should have contained this interval", desc, maxAngles, faceAngles) } actualAngles = actualAngles.Union(faceAngles) } if !actualAngles.Expanded(faceClipErrorRadians).ContainsInterval(expectedAngles) { t.Errorf("the union of all angle segments should be larger than the expected angle") } }