Пример #1
0
    public void Test_S2LatLngRect_GetDirectedHausdorffDistanceRandomPairs()
    {
        // Test random pairs.
        int kIters = 1000;

        for (int i = 0; i < kIters; ++i)
        {
            S2LatLngRect a =
                S2LatLngRect.FromPointPair(new S2LatLng(S2Testing.RandomPoint()),
                                           new S2LatLng(S2Testing.RandomPoint()));
            S2LatLngRect b =
                S2LatLngRect.FromPointPair(new S2LatLng(S2Testing.RandomPoint()),
                                           new S2LatLng(S2Testing.RandomPoint()));
            // a and b are *minimum* bounding rectangles of two random points, in
            // particular, their Voronoi diagrams are always of the same topology. We
            // take the "complements" of a and b for more thorough testing.
            S2LatLngRect a2 = new(a.Lat, a.Lng.Complement());
            S2LatLngRect b2 = new(b.Lat, b.Lng.Complement());

            // Note that "a" and "b" come from the same distribution, so there is no
            // need to test pairs such as (b, a), (b, a2), etc.
            VerifyGetDirectedHausdorffDistance(a, b);
            VerifyGetDirectedHausdorffDistance(a, b2);
            VerifyGetDirectedHausdorffDistance(a2, b);
            VerifyGetDirectedHausdorffDistance(a2, b2);
        }
    }
        public void testGetDistanceRandomPairs()
        {
            // Test random pairs.
            for (var i = 0; i < 10000; ++i)
            {
                var a =
                    S2LatLngRect.FromPointPair(new S2LatLng(randomPoint()), new S2LatLng(randomPoint()));
                var b =
                    S2LatLngRect.FromPointPair(new S2LatLng(randomPoint()), new S2LatLng(randomPoint()));
                verifyGetDistance(a, b);


                var c = new S2LatLng(randomPoint());
                verifyGetRectPointDistance(a, c);
                verifyGetRectPointDistance(b, c);
            }
        }
Пример #3
0
    public void Test_S2LatLngRect_GetDistanceRandomPairs()
    {
        // Test random pairs.
        for (int i = 0; i < 10000; ++i)
        {
            S2LatLngRect a =
                S2LatLngRect.FromPointPair(new S2LatLng(S2Testing.RandomPoint()),
                                           new S2LatLng(S2Testing.RandomPoint()));
            S2LatLngRect b =
                S2LatLngRect.FromPointPair(new S2LatLng(S2Testing.RandomPoint()),
                                           new S2LatLng(S2Testing.RandomPoint()));
            VerifyGetDistance(a, b);


            S2LatLng c = new(S2Testing.RandomPoint());
            VerifyGetRectPointDistance(a, c);
            VerifyGetRectPointDistance(b, c);
        }
    }
Пример #4
0
 public void Test_S2LatLngRect_FromPointPair()
 {
     Assert.Equal(S2LatLngRect.FromPointPair(S2LatLng.FromDegrees(-35, -140), S2LatLng.FromDegrees(15, 155)), RectFromDegrees(-35, 155, 15, -140));
     Assert.Equal(S2LatLngRect.FromPointPair(S2LatLng.FromDegrees(25, -70), S2LatLng.FromDegrees(-90, 80)), RectFromDegrees(-90, -70, 25, 80));
 }
        public void testBasic()
        {
            // Most of the S2LatLngRect methods have trivial implementations that
            // use the R1Interval and S1Interval classes, so most of the testing
            // is done in those unit tests.

            // Test basic properties of empty and full caps.
            var empty = S2LatLngRect.Empty;
            var full  = S2LatLngRect.Full;

            assertTrue(empty.IsValid);
            assertTrue(empty.IsEmpty);
            assertTrue(full.IsValid);
            assertTrue(full.IsFull);

            // assertTrue various constructors and accessor methods.
            var d1 = rectFromDegrees(-90, 0, -45, 180);

            assertDoubleNear(d1.LatLo.Degrees, -90);
            assertDoubleNear(d1.LatHi.Degrees, -45);
            assertDoubleNear(d1.LngLo.Degrees, 0);
            assertDoubleNear(d1.LngHi.Degrees, 180);
            assertTrue(d1.Lat.Equals(new R1Interval(-S2.PiOver2, -S2.PiOver4)));
            assertTrue(d1.Lng.Equals(new S1Interval(0, S2.Pi)));

            // FromCenterSize()
            assertTrue(
                S2LatLngRect.FromCenterSize(S2LatLng.FromDegrees(80, 170), S2LatLng.FromDegrees(40, 60))
                .ApproxEquals(rectFromDegrees(60, 140, 90, -160)));
            assertTrue(S2LatLngRect
                       .FromCenterSize(S2LatLng.FromDegrees(10, 40), S2LatLng.FromDegrees(210, 400)).IsFull);
            assertTrue(
                S2LatLngRect.FromCenterSize(S2LatLng.FromDegrees(-90, 180), S2LatLng.FromDegrees(20, 50))
                .ApproxEquals(rectFromDegrees(-90, 155, -80, -155)));

            // FromPoint(), FromPointPair()
            assertEquals(S2LatLngRect.FromPoint(d1.Lo), new S2LatLngRect(d1.Lo, d1.Lo));
            assertEquals(
                S2LatLngRect.FromPointPair(S2LatLng.FromDegrees(-35, -140), S2LatLng.FromDegrees(15, 155)),
                rectFromDegrees(-35, 155, 15, -140));
            assertEquals(
                S2LatLngRect.FromPointPair(S2LatLng.FromDegrees(25, -70), S2LatLng.FromDegrees(-90, 80)),
                rectFromDegrees(-90, -70, 25, 80));

            // GetCenter(), GetVertex(), Contains(S2LatLng), InteriorContains(S2LatLng).
            var eqM180    = S2LatLng.FromRadians(0, -S2.Pi);
            var northPole = S2LatLng.FromRadians(S2.PiOver2, 0);
            var r1        = new S2LatLngRect(eqM180, northPole);

            assertEquals(r1.Center, S2LatLng.FromRadians(S2.PiOver4, -S2.PiOver2));
            assertEquals(r1.GetVertex(0), S2LatLng.FromRadians(0, S2.Pi));
            assertEquals(r1.GetVertex(1), S2LatLng.FromRadians(0, 0));
            assertEquals(r1.GetVertex(2), S2LatLng.FromRadians(S2.PiOver2, 0));
            assertEquals(r1.GetVertex(3), S2LatLng.FromRadians(S2.PiOver2, S2.Pi));
            assertTrue(r1.Contains(S2LatLng.FromDegrees(30, -45)));
            assertTrue(!r1.Contains(S2LatLng.FromDegrees(30, 45)));
            assertTrue(!r1.InteriorContains(eqM180) && !r1.InteriorContains(northPole));
            assertTrue(r1.Contains(new S2Point(0.5, -0.3, 0.1)));
            assertTrue(!r1.Contains(new S2Point(0.5, 0.2, 0.1)));

            // Make sure that GetVertex() returns vertices in CCW order.
            for (var i = 0; i < 4; ++i)
            {
                var lat = S2.PiOver4 * (i - 2);
                var lng = S2.PiOver2 * (i - 2) + 0.2;
                var r   = new S2LatLngRect(new R1Interval(lat, lat + S2.PiOver4), new S1Interval(
                                               Math.IEEERemainder(lng, 2 * S2.Pi), Math.IEEERemainder(lng + S2.PiOver2, 2 * S2.Pi)));
                for (var k = 0; k < 4; ++k)
                {
                    assertTrue(
                        S2.SimpleCcw(r.GetVertex((k - 1) & 3).ToPoint(), r.GetVertex(k).ToPoint(),
                                     r.GetVertex((k + 1) & 3).ToPoint()));
                }
            }

            // Contains(S2LatLngRect), InteriorContains(S2LatLngRect),
            // Intersects(), InteriorIntersects(), Union(), Intersection().
            //
            // Much more testing of these methods is done in s1interval_unittest
            // and r1interval_unittest.

            var r1Mid      = rectFromDegrees(45, -90, 45, -90);
            var reqM180    = new S2LatLngRect(eqM180, eqM180);
            var rNorthPole = new S2LatLngRect(northPole, northPole);

            testIntervalOps(r1, r1Mid, "TTTT", r1, r1Mid);
            testIntervalOps(r1, reqM180, "TFTF", r1, reqM180);
            testIntervalOps(r1, rNorthPole, "TFTF", r1, rNorthPole);

            assertTrue(r1.Equals(rectFromDegrees(0, -180, 90, 0)));
            testIntervalOps(r1, rectFromDegrees(-10, -1, 1, 20), "FFTT", rectFromDegrees(-10, -180, 90, 20),
                            rectFromDegrees(0, -1, 1, 0));
            testIntervalOps(r1, rectFromDegrees(-10, -1, 0, 20), "FFTF", rectFromDegrees(-10, -180, 90, 20),
                            rectFromDegrees(0, -1, 0, 0));
            testIntervalOps(r1, rectFromDegrees(-10, 0, 1, 20), "FFTF", rectFromDegrees(-10, -180, 90, 20),
                            rectFromDegrees(0, 0, 1, 0));

            testIntervalOps(rectFromDegrees(-15, -160, -15, -150), rectFromDegrees(20, 145, 25, 155),
                            "FFFF", rectFromDegrees(-15, 145, 25, -150), empty);
            testIntervalOps(rectFromDegrees(70, -10, 90, -140), rectFromDegrees(60, 175, 80, 5), "FFTT",
                            rectFromDegrees(60, -180, 90, 180), rectFromDegrees(70, 175, 80, 5));

            // assertTrue that the intersection of two rectangles that overlap in
            // latitude
            // but not longitude is valid, and vice versa.
            testIntervalOps(rectFromDegrees(12, 30, 60, 60), rectFromDegrees(0, 0, 30, 18), "FFFF",
                            rectFromDegrees(0, 0, 60, 60), empty);
            testIntervalOps(rectFromDegrees(0, 0, 18, 42), rectFromDegrees(30, 12, 42, 60), "FFFF",
                            rectFromDegrees(0, 0, 42, 60), empty);

            // AddPoint()
            var p = S2LatLngRect.Empty;

            p = p.AddPoint(S2LatLng.FromDegrees(0, 0));
            p = p.AddPoint(S2LatLng.FromRadians(0, -S2.PiOver2));
            p = p.AddPoint(S2LatLng.FromRadians(S2.PiOver4, -S2.Pi));
            p = p.AddPoint(new S2Point(0, 0, 1));
            assertTrue(p.Equals(r1));

            // Expanded()
            assertTrue(
                rectFromDegrees(70, 150, 80, 170).Expanded(S2LatLng.FromDegrees(20, 30)).ApproxEquals(
                    rectFromDegrees(50, 120, 90, -160)));
            assertTrue(S2LatLngRect.Empty.Expanded(S2LatLng.FromDegrees(20, 30)).IsEmpty);
            assertTrue(S2LatLngRect.Full.Expanded(S2LatLng.FromDegrees(20, 30)).IsFull);
            assertTrue(
                rectFromDegrees(-90, 170, 10, 20).Expanded(S2LatLng.FromDegrees(30, 80)).ApproxEquals(
                    rectFromDegrees(-90, -180, 40, 180)));

            // ConvolveWithCap()
            var llr1 =
                new S2LatLngRect(S2LatLng.FromDegrees(0, 170), S2LatLng.FromDegrees(0, -170))
                .ConvolveWithCap(S1Angle.FromDegrees(15));
            var llr2 =
                new S2LatLngRect(S2LatLng.FromDegrees(-15, 155), S2LatLng.FromDegrees(15, -155));

            assertTrue(llr1.ApproxEquals(llr2));

            llr1 = new S2LatLngRect(S2LatLng.FromDegrees(60, 150), S2LatLng.FromDegrees(80, 10))
                   .ConvolveWithCap(S1Angle.FromDegrees(15));
            llr2 = new S2LatLngRect(S2LatLng.FromDegrees(45, -180), S2LatLng.FromDegrees(90, 180));
            assertTrue(llr1.ApproxEquals(llr2));

            // GetCapBound(), bounding cap at center is smaller:
            assertTrue(new S2LatLngRect(S2LatLng.FromDegrees(-45, -45), S2LatLng.FromDegrees(45, 45)).CapBound.ApproxEquals(S2Cap.FromAxisHeight(new S2Point(1, 0, 0), 0.5)));
            // GetCapBound(), bounding cap at north pole is smaller:
            assertTrue(new S2LatLngRect(S2LatLng.FromDegrees(88, -80), S2LatLng.FromDegrees(89, 80)).CapBound.ApproxEquals(S2Cap.FromAxisAngle(new S2Point(0, 0, 1), S1Angle.FromDegrees(2))));
            // GetCapBound(), longitude span > 180 degrees:
            assertTrue(
                new S2LatLngRect(S2LatLng.FromDegrees(-30, -150), S2LatLng.FromDegrees(-10, 50)).CapBound
                .ApproxEquals(S2Cap.FromAxisAngle(new S2Point(0, 0, -1), S1Angle.FromDegrees(80))));

            // Contains(S2Cell), MayIntersect(S2Cell), Intersects(S2Cell)

            // Special cases.
            testCellOps(empty, S2Cell.FromFacePosLevel(3, (byte)0, 0), 0);
            testCellOps(full, S2Cell.FromFacePosLevel(2, (byte)0, 0), 4);
            testCellOps(full, S2Cell.FromFacePosLevel(5, (byte)0, 25), 4);

            // This rectangle includes the first quadrant of face 0. It's expanded
            // slightly because cell bounding rectangles are slightly conservative.
            var r4 = rectFromDegrees(-45.1, -45.1, 0.1, 0.1);

            testCellOps(r4, S2Cell.FromFacePosLevel(0, (byte)0, 0), 3);
            testCellOps(r4, S2Cell.FromFacePosLevel(0, (byte)0, 1), 4);
            testCellOps(r4, S2Cell.FromFacePosLevel(1, (byte)0, 1), 0);

            // This rectangle intersects the first quadrant of face 0.
            var r5 = rectFromDegrees(-10, -45, 10, 0);

            testCellOps(r5, S2Cell.FromFacePosLevel(0, (byte)0, 0), 3);
            testCellOps(r5, S2Cell.FromFacePosLevel(0, (byte)0, 1), 3);
            testCellOps(r5, S2Cell.FromFacePosLevel(1, (byte)0, 1), 0);

            // Rectangle consisting of a single point.
            testCellOps(rectFromDegrees(4, 4, 4, 4), S2Cell.FromFacePosLevel(0, (byte)0, 0), 3);

            // Rectangles that intersect the bounding rectangle of a face
            // but not the face itself.
            testCellOps(rectFromDegrees(41, -87, 42, -79), S2Cell.FromFacePosLevel(2, (byte)0, 0), 1);
            testCellOps(rectFromDegrees(-41, 160, -40, -160), S2Cell.FromFacePosLevel(5, (byte)0, 0), 1);
            {
                // This is the leaf cell at the top right hand corner of face 0.
                // It has two angles of 60 degrees and two of 120 degrees.
                var cell0tr  = new S2Cell(new S2Point(1 + 1e-12, 1, 1));
                var bound0tr = cell0tr.RectBound;
                var v0       = new S2LatLng(cell0tr.GetVertexRaw(0));
                testCellOps(
                    rectFromDegrees(v0.Lat.Degrees - 1e-8, v0.Lng.Degrees - 1e-8,
                                    v0.Lat.Degrees - 2e-10, v0.Lng.Degrees + 1e-10), cell0tr, 1);
            }

            // Rectangles that intersect a face but where no vertex of one region
            // is contained by the other region. The first one passes through
            // a corner of one of the face cells.
            testCellOps(rectFromDegrees(-37, -70, -36, -20), S2Cell.FromFacePosLevel(5, (byte)0, 0), 2);
            {
                // These two intersect like a diamond and a square.
                var cell202  = S2Cell.FromFacePosLevel(2, (byte)0, 2);
                var bound202 = cell202.RectBound;
                testCellOps(
                    rectFromDegrees(bound202.Lo.Lat.Degrees + 3, bound202.Lo.Lng.Degrees + 3,
                                    bound202.Hi.Lat.Degrees - 3, bound202.Hi.Lng.Degrees - 3), cell202, 2);
            }
        }
Пример #6
0
        async void Load_Click(object sender, RoutedEventArgs e)
        {
            var picker = new Windows.Storage.Pickers.FileOpenPicker();

            picker.ViewMode = Windows.Storage.Pickers.PickerViewMode.Thumbnail;
            picker.SuggestedStartLocation = Windows.Storage.Pickers.PickerLocationId.PicturesLibrary;
            picker.FileTypeFilter.Add(".geojson");
            picker.FileTypeFilter.Add(".geoJSON");
            picker.FileTypeFilter.Add(".csv");

            Windows.Storage.StorageFile file = await picker.PickSingleFileAsync();

            if (file != null)
            {
                try
                {
                    using (StreamReader sr = new StreamReader(file.Path))
                    {
                        string content = sr.ReadToEnd();
                        if (file.FileType == ".csv")
                        {
                            string[] lines = content.Split('\n');
                            for (int i = 0; i < lines.Length; i++)
                            {
                                var item = lines[i];

                                if (String.IsNullOrWhiteSpace(item))
                                {
                                    continue;
                                }

                                string[] info = item.Split(',');
                                double   lat  = 0;
                                double   lon  = 0;

                                if (!Double.TryParse(info[info.Length - 2], out lon) || !Double.TryParse(info[info.Length - 3], out lat))
                                {
                                    if (i == 0)
                                    {
                                        continue;
                                    }
                                    var messageDialog = new MessageDialog("Zeile " + i + " (" + item + ") hat nicht das richtige Format!");
                                    messageDialog.Commands.Add(new UICommand("OK", new UICommandInvokedHandler(this.CommandInvokedHandler)));
                                    messageDialog.DefaultCommandIndex = 0;
                                    messageDialog.Title = "Fehlerhafte Datei";
                                    await messageDialog.ShowAsync();

                                    return;
                                }

                                StringBuilder sb = new StringBuilder("");

                                for (int j = 0; j < info.Length - 3; j++)
                                {
                                    sb.Append(info[j]);
                                }

                                PointOfInterest poi = new PointOfInterest()
                                {
                                    Location = new Geopoint(new BasicGeoposition()
                                    {
                                        Latitude = lat, Longitude = lon
                                    }),
                                    DisplayName           = sb.ToString(),
                                    NormalizedAnchorPoint = AnchorPoints[2],
                                    Flag = PointOfInterest.PORTAL,
                                    Leaf = FindExactCell(new BasicGeoposition()
                                    {
                                        Latitude = lat, Longitude = lon
                                    }, 30).Id.Id
                                };

                                portals.Add(poi);
                                //DataAccess.AddData(poi);
                            }
                        }
                        else
                        {
                            var features = JsonConvert.DeserializeObject <FeatureCollection>(content).Features;

                            foreach (var p in features)
                            {
                                double lon  = ((Point)p.Geometry).Coordinates.Longitude;
                                double lat  = ((Point)p.Geometry).Coordinates.Latitude;
                                int    flag = PointOfInterest.PORTAL;
                                if (p.Properties.ContainsKey("Flag"))
                                {
                                    flag = (int)p.Properties["Flag"];
                                }

                                PointOfInterest poi = new PointOfInterest()
                                {
                                    Location = new Geopoint(new BasicGeoposition()
                                    {
                                        Latitude = lat, Longitude = lon
                                    }),
                                    DisplayName           = (string)p.Properties["name"],
                                    NormalizedAnchorPoint = AnchorPoints[flag],
                                    Flag = flag,
                                    Leaf = FindExactCell(new BasicGeoposition()
                                    {
                                        Latitude = lat, Longitude = lon
                                    }, 30).Id.Id
                                };

                                portals.Add(poi);
                                //DataAccess.AddData(poi);
                            }
                        }
                        AddLayers();
                    }
                }
                catch (UnauthorizedAccessException)
                {
                    Windows.Storage.StorageFolder installedLocation = Windows.ApplicationModel.Package.Current.InstalledLocation;
                    var messageDialog = new MessageDialog("Zugriff auf den Pfad " + file.Path +
                                                          " wurde verweigert.\nBitte gewähren Sie Zugriff unter \n\nEinstellungen -> Privatsphäre -> Dateisystem\n\noder bewegen Sie die Datei an diesen Ort:\n"
                                                          + installedLocation.Path);
                    messageDialog.Commands.Add(new UICommand("OK", new UICommandInvokedHandler(this.CommandInvokedHandler)));
                    messageDialog.DefaultCommandIndex = 0;
                    messageDialog.Title = "Keine Zugriffsrechte";
                    await messageDialog.ShowAsync();

                    return;
                }

                // outermost bounds
                double maxLat = -360;
                double maxLon = -360;
                double minLat = 360;
                double minLon = 360;

                // Find out all Lvl17 cells
                foreach (var p in portals)
                {
                    double lat = p.Location.Position.Latitude;
                    double lon = p.Location.Position.Longitude;

                    if (lon < minLon)
                    {
                        minLon = lon;
                    }
                    if (lon > maxLon)
                    {
                        maxLon = lon;
                    }
                    if (lat < minLat)
                    {
                        minLat = lat;
                    }
                    if (lat > maxLat)
                    {
                        maxLat = lat;
                    }

                    var cell = FindExactCell(p.Location.Position, 17);

                    MapPolygon polygon = new MapPolygon
                    {
                        Path = new Geopath(new List <BasicGeoposition>()
                        {
                            FromS2Point(cell.GetVertex(0)),
                            FromS2Point(cell.GetVertex(1)),
                            FromS2Point(cell.GetVertex(2)),
                            FromS2Point(cell.GetVertex(3))
                        }),

                        ZIndex          = 1,
                        FillColor       = Color.FromArgb(50, 50, 50, 50),
                        StrokeColor     = Colors.Black,
                        StrokeThickness = 2,
                        StrokeDashed    = false
                    };
                    lvl17Cells.Add(polygon);
                    AddCell(polygon);
                }

                // Find out Lvl14 cells
                var area = S2LatLngRect.FromPointPair(
                    S2LatLng.FromDegrees(minLat, minLon),
                    S2LatLng.FromDegrees(maxLat, maxLon)
                    );

                var cells14 = new List <S2CellId>();
                lvl14Coverer.GetCovering(area, cells14);

                for (int i = 0; i < cells14.Count; i++)
                {
                    S2Cell cell = new S2Cell(cells14[i]);

                    MapPolygon polygon = new MapPolygon
                    {
                        Path = new Geopath(new List <BasicGeoposition>()
                        {
                            FromS2Point(cell.GetVertex(0)),
                            FromS2Point(cell.GetVertex(1)),
                            FromS2Point(cell.GetVertex(2)),
                            FromS2Point(cell.GetVertex(3))
                        }),

                        ZIndex          = 1,
                        FillColor       = Color.FromArgb(50, 0, 0, 50),
                        StrokeColor     = Colors.Blue,
                        StrokeThickness = 2,
                        StrokeDashed    = false
                    };
                    lvl14Cells.Add(polygon);
                    AddCell(polygon);
                }

                BasicGeoposition newCenter = new BasicGeoposition();
                newCenter.Latitude  = (minLat + maxLat) / 2;
                newCenter.Longitude = (minLon + maxLon) / 2;
                PokeMap.Center      = new Geopoint(newCenter);
            }
        }
Пример #7
0
    // Common back end for AddPoint() and AddLatLng().  b and b_latlng
    // must refer to the same vertex.
    private void AddInternal(S2Point b, S2LatLng b_latlng)
    {
        // Simple consistency check to verify that b and b_latlng are alternate
        // representations of the same vertex.
        System.Diagnostics.Debug.Assert(S2.ApproxEquals(b, b_latlng.ToPoint()));

        if (bound_.IsEmpty())
        {
            bound_ = bound_.AddPoint(b_latlng);
        }
        else
        {
            // First compute the cross product N = A x B robustly.  This is the normal
            // to the great circle through A and B.  We don't use S2.RobustCrossProd()
            // since that method returns an arbitrary vector orthogonal to A if the two
            // vectors are proportional, and we want the zero vector in that case.
            var n = (a_ - b).CrossProd(a_ + b);  // N = 2 * (A x B)

            // The relative error in N gets large as its norm gets very small (i.e.,
            // when the two points are nearly identical or antipodal).  We handle this
            // by choosing a maximum allowable error, and if the error is greater than
            // this we fall back to a different technique.  Since it turns out that
            // the other sources of error in converting the normal to a maximum
            // latitude add up to at most 1.16 * S2Constants.DoubleEpsilon (see below), and it is
            // desirable to have the total error be a multiple of S2Constants.DoubleEpsilon, we have
            // chosen to limit the maximum error in the normal to 3.84 * S2Constants.DoubleEpsilon.
            // It is possible to show that the error is less than this when
            //
            //   n.Norm >= 8 * Math.Sqrt(3) / (3.84 - 0.5 - Math.Sqrt(3)) * S2Constants.DoubleEpsilon
            //            = 1.91346e-15 (about 8.618 * S2Constants.DoubleEpsilon)
            var n_norm = n.Norm();
            if (n_norm < 1.91346e-15)
            {
                // A and B are either nearly identical or nearly antipodal (to within
                // 4.309 * S2Constants.DoubleEpsilon, or about 6 nanometers on the earth's surface).
                if (a_.DotProd(b) < 0)
                {
                    // The two points are nearly antipodal.  The easiest solution is to
                    // assume that the edge between A and B could go in any direction
                    // around the sphere.
                    bound_ = S2LatLngRect.Full;
                }
                else
                {
                    // The two points are nearly identical (to within 4.309 * S2Constants.DoubleEpsilon).
                    // In this case we can just use the bounding rectangle of the points,
                    // since after the expansion done by GetBound() this rectangle is
                    // guaranteed to include the (lat,lng) values of all points along AB.
                    bound_ = bound_.Union(S2LatLngRect.FromPointPair(a_latlng_, b_latlng));
                }
            }
            else
            {
                // Compute the longitude range spanned by AB.
                var lng_ab = S1Interval.FromPointPair(a_latlng_.LngRadians, b_latlng.LngRadians);
                if (lng_ab.GetLength() >= Math.PI - 2 * S2.DoubleEpsilon)
                {
                    // The points lie on nearly opposite lines of longitude to within the
                    // maximum error of the calculation.  (Note that this test relies on
                    // the fact that Math.PI is slightly less than the true value of Pi, and
                    // that representable values near Math.PI are 2 * S2Constants.DoubleEpsilon apart.)
                    // The easiest solution is to assume that AB could go on either side
                    // of the pole.
                    lng_ab = S1Interval.Full;
                }

                // Next we compute the latitude range spanned by the edge AB.  We start
                // with the range spanning the two endpoints of the edge:
                var lat_ab = R1Interval.FromPointPair(a_latlng_.LatRadians, b_latlng.LatRadians);

                // This is the desired range unless the edge AB crosses the plane
                // through N and the Z-axis (which is where the great circle through A
                // and B attains its minimum and maximum latitudes).  To test whether AB
                // crosses this plane, we compute a vector M perpendicular to this
                // plane and then project A and B onto it.
                var m   = n.CrossProd(new S2Point(0, 0, 1));
                var m_a = m.DotProd(a_);
                var m_b = m.DotProd(b);

                // We want to test the signs of "m_a" and "m_b", so we need to bound
                // the error in these calculations.  It is possible to show that the
                // total error is bounded by
                //
                //  (1 + Math.Sqrt(3)) * S2Constants.DoubleEpsilon * n_norm + 8 * Math.Sqrt(3) * (S2Constants.DoubleEpsilon**2)
                //    = 6.06638e-16 * n_norm + 6.83174e-31

                double m_error = 6.06638e-16 * n_norm + 6.83174e-31;
                if (m_a * m_b < 0 || Math.Abs(m_a) <= m_error || Math.Abs(m_b) <= m_error)
                {
                    // Minimum/maximum latitude *may* occur in the edge interior.
                    //
                    // The maximum latitude is 90 degrees minus the latitude of N.  We
                    // compute this directly using atan2 in order to get maximum accuracy
                    // near the poles.
                    //
                    // Our goal is compute a bound that contains the computed latitudes of
                    // all S2Points P that pass the point-in-polygon containment test.
                    // There are three sources of error we need to consider:
                    //  - the directional error in N (at most 3.84 * S2Constants.DoubleEpsilon)
                    //  - converting N to a maximum latitude
                    //  - computing the latitude of the test point P
                    // The latter two sources of error are at most 0.955 * S2Constants.DoubleEpsilon
                    // individually, but it is possible to show by a more complex analysis
                    // that together they can add up to at most 1.16 * S2Constants.DoubleEpsilon, for a
                    // total error of 5 * S2Constants.DoubleEpsilon.
                    //
                    // We add 3 * S2Constants.DoubleEpsilon to the bound here, and GetBound() will pad
                    // the bound by another 2 * S2Constants.DoubleEpsilon.
                    var max_lat = Math.Min(
                        Math.Atan2(Math.Sqrt(n[0] * n[0] + n[1] * n[1]), Math.Abs(n[2])) + 3 * S2.DoubleEpsilon,
                        S2.M_PI_2);

                    // In order to get tight bounds when the two points are close together,
                    // we also bound the min/max latitude relative to the latitudes of the
                    // endpoints A and B.  First we compute the distance between A and B,
                    // and then we compute the maximum change in latitude between any two
                    // points along the great circle that are separated by this distance.
                    // This gives us a latitude change "budget".  Some of this budget must
                    // be spent getting from A to B; the remainder bounds the round-trip
                    // distance (in latitude) from A or B to the min or max latitude
                    // attained along the edge AB.
                    //
                    // There is a maximum relative error of 4.5 * DBL_EPSILON in computing
                    // the squared distance (a_ - b), which means a maximum error of (4.5
                    // / 2 + 0.5) == 2.75 * DBL_EPSILON in computing Norm().  The sin()
                    // and multiply each have a relative error of 0.5 * DBL_EPSILON which
                    // we round up to a total of 4 * DBL_EPSILON.
                    var          lat_budget_z = 0.5 * (a_ - b).Norm() * Math.Sin(max_lat);
                    const double folded       = (1 + 4 * S2.DoubleEpsilon);
                    var          lat_budget   = 2 * Math.Asin(Math.Min(folded * lat_budget_z, 1.0));
                    var          max_delta    = 0.5 * (lat_budget - lat_ab.GetLength()) + S2.DoubleEpsilon;

                    // Test whether AB passes through the point of maximum latitude or
                    // minimum latitude.  If the dot product(s) are small enough then the
                    // result may be ambiguous.
                    if (m_a <= m_error && m_b >= -m_error)
                    {
                        lat_ab = new R1Interval(lat_ab.Lo, Math.Min(max_lat, lat_ab.Hi + max_delta));
                    }
                    if (m_b <= m_error && m_a >= -m_error)
                    {
                        lat_ab = new R1Interval(Math.Max(-max_lat, lat_ab.Lo - max_delta), lat_ab.Lo);
                    }
                }
                bound_ = bound_.Union(new S2LatLngRect(lat_ab, lng_ab));
            }
        }
        a_        = b;
        a_latlng_ = b_latlng;
    }