...

Source file src/github.com/golang/geo/s2/edge_tessellator.go

Documentation: github.com/golang/geo/s2

     1  // Copyright 2018 Google Inc. All rights reserved.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package s2
    16  
    17  import (
    18  	"github.com/golang/geo/r2"
    19  	"github.com/golang/geo/s1"
    20  )
    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  
   162  const (
   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
   170  	// corresponds to a distance less than 1 micrometer on the Earth's
   171  	// surface, but is still much larger than the expected projection and
   172  	// interpolation errors).
   173  	minTessellationTolerance s1.Angle = 1e-13
   174  )
   175  
   176  // EdgeTessellator converts an edge in a given projection (e.g., Mercator) into
   177  // a chain of spherical geodesic edges such that the maximum distance between
   178  // the original edge and the geodesic edge chain is at most the requested
   179  // tolerance. Similarly, it can convert a spherical geodesic edge into a chain
   180  // of edges in a given 2D projection such that the maximum distance between the
   181  // geodesic edge and the chain of projected edges is at most the requested tolerance.
   182  //
   183  //	Method      | Input                  | Output
   184  //	------------|------------------------|-----------------------
   185  //	Projected   | S2 geodesics           | Planar projected edges
   186  //	Unprojected | Planar projected edges | S2 geodesics
   187  type EdgeTessellator struct {
   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
   193  }
   194  
   195  // NewEdgeTessellator creates a new edge tessellator for the given projection and tolerance.
   196  func NewEdgeTessellator(p Projection, tolerance s1.Angle) *EdgeTessellator {
   197  	return &EdgeTessellator{
   198  		projection:      p,
   199  		scaledTolerance: s1.ChordAngleFromAngle(maxAngle(tolerance, minTessellationTolerance)),
   200  	}
   201  }
   202  
   203  // AppendProjected converts the spherical geodesic edge AB to a chain of planar edges
   204  // in the given projection and returns the corresponding vertices.
   205  //
   206  // If the given projection has one or more coordinate axes that wrap, then
   207  // every vertex's coordinates will be as close as possible to the previous
   208  // vertex's coordinates. Note that this may yield vertices whose
   209  // coordinates are outside the usual range. For example, tessellating the
   210  // edge (0:170, 0:-170) (in lat:lng notation) yields (0:170, 0:190).
   211  func (e *EdgeTessellator) AppendProjected(a, b Point, vertices []r2.Point) []r2.Point {
   212  	pa := e.projection.Project(a)
   213  	if len(vertices) == 0 {
   214  		vertices = []r2.Point{pa}
   215  	} else {
   216  		pa = e.projection.WrapDestination(vertices[len(vertices)-1], pa)
   217  	}
   218  
   219  	pb := e.projection.Project(b)
   220  	return e.appendProjected(pa, a, pb, b, vertices)
   221  }
   222  
   223  // appendProjected splits a geodesic edge AB as necessary and returns the
   224  // projected vertices appended to the given vertices.
   225  //
   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 {
   230  		return append(vertices, pb)
   231  	}
   232  
   233  	mid := Point{a.Add(b.Vector).Normalize()}
   234  	pmid := e.projection.WrapDestination(pa, e.projection.Project(mid))
   235  	vertices = e.appendProjected(pa, a, pmid, mid, vertices)
   236  	return e.appendProjected(pmid, mid, pb, b, vertices)
   237  }
   238  
   239  // AppendUnprojected converts the planar edge AB in the given projection to a chain of
   240  // spherical geodesic edges and returns the vertices.
   241  //
   242  // Note that to construct a Loop, you must eliminate the duplicate first and last
   243  // vertex. Note also that if the given projection involves coordinate wrapping
   244  // (e.g. across the 180 degree meridian) then the first and last vertices may not
   245  // be exactly the same.
   246  func (e *EdgeTessellator) AppendUnprojected(pa, pb r2.Point, vertices []Point) []Point {
   247  	a := e.projection.Unproject(pa)
   248  	b := e.projection.Unproject(pb)
   249  
   250  	if len(vertices) == 0 {
   251  		vertices = []Point{a}
   252  	}
   253  
   254  	// Note that coordinate wrapping can create a small amount of error. For
   255  	// example in the edge chain "0:-175, 0:179, 0:-177", the first edge is
   256  	// transformed into "0:-175, 0:-181" while the second is transformed into
   257  	// "0:179, 0:183". The two coordinate pairs for the middle vertex
   258  	// ("0:-181" and "0:179") may not yield exactly the same S2Point.
   259  	return e.appendUnprojected(pa, a, pb, b, vertices)
   260  }
   261  
   262  // appendUnprojected interpolates a projected edge and appends the corresponding
   263  // points on the sphere.
   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 {
   267  		return append(vertices, b)
   268  	}
   269  
   270  	pmid := e.projection.Interpolate(0.5, pa, pb)
   271  	mid := e.projection.Unproject(pmid)
   272  
   273  	vertices = e.appendUnprojected(pa, a, pmid, mid, vertices)
   274  	return e.appendUnprojected(pmid, mid, pb, b, vertices)
   275  }
   276  
   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()
   283  	}
   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))
   291  }
   292  

View as plain text