15
15
package s2
16
16
17
17
import (
18
- "math"
19
-
20
18
"github.com/golang/geo/r2"
21
19
"github.com/golang/geo/s1"
22
20
)
23
21
22
+ // Tessellation is implemented by subdividing the edge until the estimated
23
+ // maximum error is below the given tolerance. Estimating error is a hard
24
+ // problem, especially when the only methods available are point evaluation of
25
+ // the projection and its inverse. (These are the only methods that
26
+ // Projection provides, which makes it easier and less error-prone to
27
+ // implement new projections.)
28
+ //
29
+ // One technique that significantly increases robustness is to treat the
30
+ // geodesic and projected edges as parametric curves rather than geometric ones.
31
+ // Given a spherical edge AB and a projection p:S2->R2, let f(t) be the
32
+ // normalized arc length parametrization of AB and let g(t) be the normalized
33
+ // arc length parameterization of the projected edge p(A)p(B). (In other words,
34
+ // f(0)=A, f(1)=B, g(0)=p(A), g(1)=p(B).) We now define the geometric error as
35
+ // the maximum distance from the point p^-1(g(t)) to the geodesic edge AB for
36
+ // any t in [0,1], where p^-1 denotes the inverse projection. In other words,
37
+ // the geometric error is the maximum distance from any point on the projected
38
+ // edge (mapped back onto the sphere) to the geodesic edge AB. On the other
39
+ // hand we define the parametric error as the maximum distance between the
40
+ // points f(t) and p^-1(g(t)) for any t in [0,1], i.e. the maximum distance
41
+ // (measured on the sphere) between the geodesic and projected points at the
42
+ // same interpolation fraction t.
43
+ //
44
+ // The easiest way to estimate the parametric error is to simply evaluate both
45
+ // edges at their midpoints and measure the distance between them (the "midpoint
46
+ // method"). This is very fast and works quite well for most edges, however it
47
+ // has one major drawback: it doesn't handle points of inflection (i.e., points
48
+ // where the curvature changes sign). For example, edges in the Mercator and
49
+ // Plate Carree projections always curve towards the equator relative to the
50
+ // corresponding geodesic edge, so in these projections there is a point of
51
+ // inflection whenever the projected edge crosses the equator. The worst case
52
+ // occurs when the edge endpoints have different longitudes but the same
53
+ // absolute latitude, since in that case the error is non-zero but the edges
54
+ // have exactly the same midpoint (on the equator).
55
+ //
56
+ // One solution to this problem is to split the input edges at all inflection
57
+ // points (i.e., along the equator in the case of the Mercator and Plate Carree
58
+ // projections). However for general projections these inflection points can
59
+ // occur anywhere on the sphere (e.g., consider the Transverse Mercator
60
+ // projection). This could be addressed by adding methods to the S2Projection
61
+ // interface to split edges at inflection points but this would make it harder
62
+ // and more error-prone to implement new projections.
63
+ //
64
+ // Another problem with this approach is that the midpoint method sometimes
65
+ // underestimates the true error even when edges do not cross the equator.
66
+ // For the Plate Carree and Mercator projections, the midpoint method can
67
+ // underestimate the error by up to 3%.
68
+ //
69
+ // Both of these problems can be solved as follows. We assume that the error
70
+ // can be modeled as a convex combination of two worst-case functions, one
71
+ // where the error is maximized at the edge midpoint and another where the
72
+ // error is *minimized* (i.e., zero) at the edge midpoint. For example, we
73
+ // could choose these functions as:
74
+ //
75
+ // E1(x) = 1 - x^2
76
+ // E2(x) = x * (1 - x^2)
77
+ //
78
+ // where for convenience we use an interpolation parameter "x" in the range
79
+ // [-1, 1] rather than the original "t" in the range [0, 1]. Note that both
80
+ // error functions must have roots at x = {-1, 1} since the error must be zero
81
+ // at the edge endpoints. E1 is simply a parabola whose maximum value is 1
82
+ // attained at x = 0, while E2 is a cubic with an additional root at x = 0,
83
+ // and whose maximum value is 2 * sqrt(3) / 9 attained at x = 1 / sqrt(3).
84
+ //
85
+ // Next, it is convenient to scale these functions so that the both have a
86
+ // maximum value of 1. E1 already satisfies this requirement, and we simply
87
+ // redefine E2 as
88
+ //
89
+ // E2(x) = x * (1 - x^2) / (2 * sqrt(3) / 9)
90
+ //
91
+ // Now define x0 to be the point where these two functions intersect, i.e. the
92
+ // point in the range (-1, 1) where E1(x0) = E2(x0). This value has the very
93
+ // convenient property that if we evaluate the actual error E(x0), then the
94
+ // maximum error on the entire interval [-1, 1] is bounded by
95
+ //
96
+ // E(x) <= E(x0) / E1(x0)
97
+ //
98
+ // since whether the error is modeled using E1 or E2, the resulting function
99
+ // has the same maximum value (namely E(x0) / E1(x0)). If it is modeled as
100
+ // some other convex combination of E1 and E2, the maximum value can only
101
+ // decrease.
102
+ //
103
+ // Finally, since E2 is not symmetric about the y-axis, we must also allow for
104
+ // the possibility that the error is a convex combination of E1 and -E2. This
105
+ // can be handled by evaluating the error at E(-x0) as well, and then
106
+ // computing the final error bound as
107
+ //
108
+ // E(x) <= max(E(x0), E(-x0)) / E1(x0) .
109
+ //
110
+ // Effectively, this method is simply evaluating the error at two points about
111
+ // 1/3 and 2/3 of the way along the edges, and then scaling the maximum of
112
+ // these two errors by a constant factor. Intuitively, the reason this works
113
+ // is that if the two edges cross somewhere in the interior, then at least one
114
+ // of these points will be far from the crossing.
115
+ //
116
+ // The actual algorithm implemented below has some additional refinements.
117
+ // First, edges longer than 90 degrees are always subdivided; this avoids
118
+ // various unusual situations that can happen with very long edges, and there
119
+ // is really no reason to avoid adding vertices to edges that are so long.
120
+ //
121
+ // Second, the error function E1 above needs to be modified to take into
122
+ // account spherical distortions. (It turns out that spherical distortions are
123
+ // beneficial in the case of E2, i.e. they only make its error estimates
124
+ // slightly more conservative.) To do this, we model E1 as the maximum error
125
+ // in a Plate Carree edge of length 90 degrees or less. This turns out to be
126
+ // an edge from 45:-90 to 45:90 (in lat:lng format). The corresponding error
127
+ // as a function of "x" in the range [-1, 1] can be computed as the distance
128
+ // between the Plate Caree edge point (45, 90 * x) and the geodesic
129
+ // edge point (90 - 45 * abs(x), 90 * sgn(x)). Using the Haversine formula,
130
+ // the corresponding function E1 (normalized to have a maximum value of 1) is:
131
+ //
132
+ // E1(x) =
133
+ // asin(sqrt(sin(Pi / 8 * (1 - x)) ^ 2 +
134
+ // sin(Pi / 4 * (1 - x)) ^ 2 * cos(Pi / 4) * sin(Pi / 4 * x))) /
135
+ // asin(sqrt((1 - 1 / sqrt(2)) / 2))
136
+ //
137
+ // Note that this function does not need to be evaluated at runtime, it
138
+ // simply affects the calculation of the value x0 where E1(x0) = E2(x0)
139
+ // and the corresponding scaling factor C = 1 / E1(x0).
140
+ //
141
+ // ------------------------------------------------------------------
142
+ //
143
+ // In the case of the Mercator and Plate Carree projections this strategy
144
+ // produces a conservative upper bound (verified using 10 million random
145
+ // edges). Furthermore the bound is nearly tight; the scaling constant is
146
+ // C = 1.19289, whereas the maximum observed value was 1.19254.
147
+ //
148
+ // Compared to the simpler midpoint evaluation method, this strategy requires
149
+ // more function evaluations (currently twice as many, but with a smarter
150
+ // tessellation algorithm it will only be 50% more). It also results in a
151
+ // small amount of additional tessellation (about 1.5%) compared to the
152
+ // midpoint method, but this is due almost entirely to the fact that the
153
+ // midpoint method does not yield conservative error estimates.
154
+ //
155
+ // For random edges with a tolerance of 1 meter, the expected amount of
156
+ // overtessellation is as follows:
157
+ //
158
+ // Midpoint Method Cubic Method
159
+ // Plate Carree 1.8% 3.0%
160
+ // Mercator 15.8% 17.4%
161
+
24
162
const (
25
- // MinTessellationTolerance is the minimum supported tolerance (which
163
+ // tessellationInterpolationFraction is the fraction at which the two edges
164
+ // are evaluated in order to measure the error between them. (Edges are
165
+ // evaluated at two points measured this fraction from either end.)
166
+ tessellationInterpolationFraction = 0.31215691082248312
167
+ tessellationScaleFactor = 0.83829992569888509
168
+
169
+ // minTessellationTolerance is the minimum supported tolerance (which
26
170
// corresponds to a distance less than 1 micrometer on the Earth's
27
171
// surface, but is still much larger than the expected projection and
28
172
// interpolation errors).
29
- MinTessellationTolerance s1.Angle = 1e-13
173
+ minTessellationTolerance s1.Angle = 1e-13
30
174
)
31
175
32
176
// EdgeTessellator converts an edge in a given projection (e.g., Mercator) into
@@ -41,17 +185,18 @@ const (
41
185
// Projected | S2 geodesics | Planar projected edges
42
186
// Unprojected | Planar projected edges | S2 geodesics
43
187
type EdgeTessellator struct {
44
- projection Projection
45
- tolerance s1.ChordAngle
46
- wrapDistance r2.Point
188
+ projection Projection
189
+
190
+ // The given tolerance scaled by a constant fraction so that it can be
191
+ // compared against the result returned by estimateMaxError.
192
+ scaledTolerance s1.ChordAngle
47
193
}
48
194
49
195
// NewEdgeTessellator creates a new edge tessellator for the given projection and tolerance.
50
196
func NewEdgeTessellator (p Projection , tolerance s1.Angle ) * EdgeTessellator {
51
197
return & EdgeTessellator {
52
- projection : p ,
53
- tolerance : s1 .ChordAngleFromAngle (maxAngle (tolerance , MinTessellationTolerance )),
54
- wrapDistance : p .WrapDistance (),
198
+ projection : p ,
199
+ scaledTolerance : s1 .ChordAngleFromAngle (maxAngle (tolerance , minTessellationTolerance )),
55
200
}
56
201
}
57
202
@@ -68,46 +213,25 @@ func (e *EdgeTessellator) AppendProjected(a, b Point, vertices []r2.Point) []r2.
68
213
if len (vertices ) == 0 {
69
214
vertices = []r2.Point {pa }
70
215
} else {
71
- pa = e .wrapDestination (vertices [len (vertices )- 1 ], pa )
216
+ pa = e .projection . WrapDestination (vertices [len (vertices )- 1 ], pa )
72
217
}
73
218
74
- pb := e .wrapDestination ( pa , e . projection .Project (b ) )
219
+ pb := e .projection .Project (b )
75
220
return e .appendProjected (pa , a , pb , b , vertices )
76
221
}
77
222
78
223
// appendProjected splits a geodesic edge AB as necessary and returns the
79
224
// projected vertices appended to the given vertices.
80
225
//
81
- // The maximum recursion depth is (math.Pi / MinTessellationTolerance) < 45
82
- func (e * EdgeTessellator ) appendProjected (pa r2.Point , a Point , pb r2.Point , b Point , vertices []r2.Point ) []r2.Point {
83
- // It's impossible to robustly test whether a projected edge is close enough
84
- // to a geodesic edge without knowing the details of the projection
85
- // function, but the following heuristic works well for a wide range of map
86
- // projections. The idea is simply to test whether the midpoint of the
87
- // projected edge is close enough to the midpoint of the geodesic edge.
88
- //
89
- // This measures the distance between the two edges by treating them as
90
- // parametric curves rather than geometric ones. The problem with
91
- // measuring, say, the minimum distance from the projected midpoint to the
92
- // geodesic edge is that this is a lower bound on the value we want, because
93
- // the maximum separation between the two curves is generally not attained
94
- // at the midpoint of the projected edge. The distance between the curve
95
- // midpoints is at least an upper bound on the distance from either midpoint
96
- // to opposite curve. It's not necessarily an upper bound on the maximum
97
- // distance between the two curves, but it is a powerful requirement because
98
- // it demands that the two curves stay parametrically close together. This
99
- // turns out to be much more robust with respect for projections with
100
- // singularities (e.g., the North and South poles in the rectangular and
101
- // Mercator projections) because the curve parameterization speed changes
102
- // rapidly near such singularities.
103
- mid := Point {a .Add (b .Vector ).Normalize ()}
104
- testMid := e .projection .Unproject (e .projection .Interpolate (0.5 , pa , pb ))
105
-
106
- if ChordAngleBetweenPoints (mid , testMid ) < e .tolerance {
226
+ // The maximum recursion depth is (math.Pi / minTessellationTolerance) < 45
227
+ func (e * EdgeTessellator ) appendProjected (pa r2.Point , a Point , pbIn r2.Point , b Point , vertices []r2.Point ) []r2.Point {
228
+ pb := e .projection .WrapDestination (pa , pbIn )
229
+ if e .estimateMaxError (pa , a , pb , b ) <= e .scaledTolerance {
107
230
return append (vertices , pb )
108
231
}
109
232
110
- pmid := e .wrapDestination (pa , e .projection .Project (mid ))
233
+ mid := Point {a .Add (b .Vector ).Normalize ()}
234
+ pmid := e .projection .WrapDestination (pa , e .projection .Project (mid ))
111
235
vertices = e .appendProjected (pa , a , pmid , mid , vertices )
112
236
return e .appendProjected (pmid , mid , pb , b , vertices )
113
237
}
@@ -120,7 +244,6 @@ func (e *EdgeTessellator) appendProjected(pa r2.Point, a Point, pb r2.Point, b P
120
244
// (e.g. across the 180 degree meridian) then the first and last vertices may not
121
245
// be exactly the same.
122
246
func (e * EdgeTessellator ) AppendUnprojected (pa , pb r2.Point , vertices []Point ) []Point {
123
- pb2 := e .wrapDestination (pa , pb )
124
247
a := e .projection .Unproject (pa )
125
248
b := e .projection .Unproject (pb )
126
249
@@ -133,35 +256,36 @@ func (e *EdgeTessellator) AppendUnprojected(pa, pb r2.Point, vertices []Point) [
133
256
// transformed into "0:-175, 0:-181" while the second is transformed into
134
257
// "0:179, 0:183". The two coordinate pairs for the middle vertex
135
258
// ("0:-181" and "0:179") may not yield exactly the same S2Point.
136
- return e .appendUnprojected (pa , a , pb2 , b , vertices )
259
+ return e .appendUnprojected (pa , a , pb , b , vertices )
137
260
}
138
261
139
262
// appendUnprojected interpolates a projected edge and appends the corresponding
140
263
// points on the sphere.
141
- func (e * EdgeTessellator ) appendUnprojected (pa r2.Point , a Point , pb r2.Point , b Point , vertices []Point ) []Point {
142
- pmid := e .projection .Interpolate (0.5 , pa , pb )
143
- mid := e .projection .Unproject (pmid )
144
- testMid := Point {a .Add (b .Vector ).Normalize ()}
145
-
146
- if ChordAngleBetweenPoints (mid , testMid ) < e .tolerance {
264
+ func (e * EdgeTessellator ) appendUnprojected (pa r2.Point , a Point , pbIn r2.Point , b Point , vertices []Point ) []Point {
265
+ pb := e .projection .WrapDestination (pa , pbIn )
266
+ if e .estimateMaxError (pa , a , pb , b ) <= e .scaledTolerance {
147
267
return append (vertices , b )
148
268
}
149
269
270
+ pmid := e .projection .Interpolate (0.5 , pa , pb )
271
+ mid := e .projection .Unproject (pmid )
272
+
150
273
vertices = e .appendUnprojected (pa , a , pmid , mid , vertices )
151
274
return e .appendUnprojected (pmid , mid , pb , b , vertices )
152
275
}
153
276
154
- // wrapDestination returns the coordinates of the edge destination wrapped if
155
- // necessary to obtain the shortest edge.
156
- func (e * EdgeTessellator ) wrapDestination (pa , pb r2.Point ) r2.Point {
157
- x := pb .X
158
- y := pb .Y
159
- // The code below ensures that pb is unmodified unless wrapping is required.
160
- if e .wrapDistance .X > 0 && math .Abs (x - pa .X ) > 0.5 * e .wrapDistance .X {
161
- x = pa .X + math .Remainder (x - pa .X , e .wrapDistance .X )
162
- }
163
- if e .wrapDistance .Y > 0 && math .Abs (y - pa .Y ) > 0.5 * e .wrapDistance .Y {
164
- y = pa .Y + math .Remainder (y - pa .Y , e .wrapDistance .Y )
277
+ func (e * EdgeTessellator ) estimateMaxError (pa r2.Point , a Point , pb r2.Point , b Point ) s1.ChordAngle {
278
+ // See the algorithm description at the top of this file.
279
+ // We always tessellate edges longer than 90 degrees on the sphere, since the
280
+ // approximation below is not robust enough to handle such edges.
281
+ if a .Dot (b .Vector ) < - 1e-14 {
282
+ return s1 .InfChordAngle ()
165
283
}
166
- return r2.Point {x , y }
284
+ t1 := tessellationInterpolationFraction
285
+ t2 := 1 - tessellationInterpolationFraction
286
+ mid1 := Interpolate (t1 , a , b )
287
+ mid2 := Interpolate (t2 , a , b )
288
+ pmid1 := e .projection .Unproject (e .projection .Interpolate (t1 , pa , pb ))
289
+ pmid2 := e .projection .Unproject (e .projection .Interpolate (t2 , pa , pb ))
290
+ return maxChordAngle (ChordAngleBetweenPoints (mid1 , pmid1 ), ChordAngleBetweenPoints (mid2 , pmid2 ))
167
291
}
0 commit comments