gtsocial-umbx

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | README | LICENSE

edge_tessellator.go (14380B)


      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 }