diff --git a/README.md b/README.md new file mode 100644 index 0000000..3736c52 --- /dev/null +++ b/README.md @@ -0,0 +1,4 @@ +# S2Swift + +Implementation of Google's S2 geometry library in Swift + diff --git a/Sphere2Go.xcodeproj/project.pbxproj b/Sphere2Go.xcodeproj/project.pbxproj new file mode 100644 index 0000000..10e9c9f --- /dev/null +++ b/Sphere2Go.xcodeproj/project.pbxproj @@ -0,0 +1,663 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXBuildFile section */ + 7700AE781CCFDC7C00606F25 /* R1Interval.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7770ED2C1CCAB37C00C543EC /* R1Interval.swift */; }; + 7700AE7B1CD013DB00606F25 /* R2Rect.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7770ED2E1CCAB38900C543EC /* R2Rect.swift */; }; + 7700AE7E1CD0257000606F25 /* R3Vector.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7770ED301CCAB38F00C543EC /* R3Vector.swift */; }; + 7700AE961CD04FCF00606F25 /* S1Angle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7770ED421CCAC90600C543EC /* S1Angle.swift */; }; + 7700AE981CD04FCF00606F25 /* S1Interval.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7770ED471CCAC92C00C543EC /* S1Interval.swift */; }; + 7700AE991CD04FCF00606F25 /* S2LatLng.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7770ED4A1CCAD86200C543EC /* S2LatLng.swift */; }; + 7700AE9A1CD04FCF00606F25 /* S2Point.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7770ED4D1CCADC0000C543EC /* S2Point.swift */; }; + 7700AE9B1CD04FCF00606F25 /* S2Region.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7770ED541CCB0B3D00C543EC /* S2Region.swift */; }; + 7700AE9C1CD04FCF00606F25 /* S2Shape.swift in Sources */ = {isa = PBXBuildFile; fileRef = 771EE8FE1CCBF83F00BE111F /* S2Shape.swift */; }; + 7700AE9D1CD04FCF00606F25 /* S2Rect.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7770ED521CCB0B0700C543EC /* S2Rect.swift */; }; + 7700AE9E1CD04FCF00606F25 /* S2Loop.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7770ED561CCB0B8900C543EC /* S2Loop.swift */; }; + 7700AE9F1CD04FCF00606F25 /* S2Polygon.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7770ED501CCB0AE400C543EC /* S2Polygon.swift */; }; + 7700AEA01CD04FCF00606F25 /* S2Cap.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7770ED681CCB0C0000C543EC /* S2Cap.swift */; }; + 7700AEA11CD04FCF00606F25 /* S2Metric.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7770ED661CCB0BE700C543EC /* S2Metric.swift */; }; + 7700AEA31CD04FCF00606F25 /* S2CellId.swift in Sources */ = {isa = PBXBuildFile; fileRef = 771EE8FC1CCBDDDC00BE111F /* S2CellId.swift */; }; + 7700AEA41CD04FCF00606F25 /* S2Cell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 771EE8FA1CCBD7FF00BE111F /* S2Cell.swift */; }; + 7700AEA51CD04FCF00606F25 /* S2CellUnion.swift in Sources */ = {isa = PBXBuildFile; fileRef = 771EE9001CCC000300BE111F /* S2CellUnion.swift */; }; + 7700AEA61CD04FCF00606F25 /* S2EdgeUtility.swift in Sources */ = {isa = PBXBuildFile; fileRef = 771EE9021CCC03FD00BE111F /* S2EdgeUtility.swift */; }; + 7700AEA71CD04FCF00606F25 /* S2Matrix.swift in Sources */ = {isa = PBXBuildFile; fileRef = 772EBA371CCC4FAC005D39AC /* S2Matrix.swift */; }; + 7700AEAA1CD04FD400606F25 /* PriorityQueue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7701F8221CCD5E2C0013898B /* PriorityQueue.swift */; }; + 7701F8231CCD5E2C0013898B /* PriorityQueue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7701F8221CCD5E2C0013898B /* PriorityQueue.swift */; }; + 7705EBB61E2EC8E800F1151A /* R1IntervalTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7705EBB21E2EC8E800F1151A /* R1IntervalTests.swift */; }; + 7705EBB71E2EC8E800F1151A /* R2RectTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7705EBB31E2EC8E800F1151A /* R2RectTests.swift */; }; + 7705EBB81E2EC8E800F1151A /* R3VectorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7705EBB41E2EC8E800F1151A /* R3VectorTests.swift */; }; + 7705EBC01E2EC8FA00F1151A /* S1IntervalTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7705EBB91E2EC8FA00F1151A /* S1IntervalTests.swift */; }; + 7705EBC11E2EC8FA00F1151A /* S2CapTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7705EBBA1E2EC8FA00F1151A /* S2CapTests.swift */; }; + 7705EBC21E2EC8FA00F1151A /* S2CellIdTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7705EBBB1E2EC8FA00F1151A /* S2CellIdTests.swift */; }; + 7705EBC31E2EC8FA00F1151A /* S2CellTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7705EBBC1E2EC8FA00F1151A /* S2CellTests.swift */; }; + 7705EBC41E2EC8FA00F1151A /* S2CellUnionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7705EBBD1E2EC8FA00F1151A /* S2CellUnionTests.swift */; }; + 7705EBC51E2EC8FA00F1151A /* S2LatLngTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7705EBBE1E2EC8FA00F1151A /* S2LatLngTests.swift */; }; + 771221061E7B2866003B62C8 /* S2PolygonTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 771221051E7B2866003B62C8 /* S2PolygonTests.swift */; }; + 771221081E7B2898003B62C8 /* S2PolylineTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 771221071E7B2898003B62C8 /* S2PolylineTests.swift */; }; + 771DD34F1E2EC9D800F5DEDE /* S2MetricTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 771DD34D1E2EC9D800F5DEDE /* S2MetricTests.swift */; }; + 771EE8FB1CCBD7FF00BE111F /* S2Cell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 771EE8FA1CCBD7FF00BE111F /* S2Cell.swift */; }; + 771EE8FD1CCBDDDC00BE111F /* S2CellId.swift in Sources */ = {isa = PBXBuildFile; fileRef = 771EE8FC1CCBDDDC00BE111F /* S2CellId.swift */; }; + 771EE8FF1CCBF83F00BE111F /* S2Shape.swift in Sources */ = {isa = PBXBuildFile; fileRef = 771EE8FE1CCBF83F00BE111F /* S2Shape.swift */; }; + 771EE9011CCC000300BE111F /* S2CellUnion.swift in Sources */ = {isa = PBXBuildFile; fileRef = 771EE9001CCC000300BE111F /* S2CellUnion.swift */; }; + 771EE9031CCC03FD00BE111F /* S2EdgeUtility.swift in Sources */ = {isa = PBXBuildFile; fileRef = 771EE9021CCC03FD00BE111F /* S2EdgeUtility.swift */; }; + 772EBA381CCC4FAC005D39AC /* S2Matrix.swift in Sources */ = {isa = PBXBuildFile; fileRef = 772EBA371CCC4FAC005D39AC /* S2Matrix.swift */; }; + 77633A161E2ECA2900497830 /* S2RegionCovererTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77633A131E2ECA2900497830 /* S2RegionCovererTests.swift */; }; + 77633A171E2ECA2900497830 /* S2ShapeIndexTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77633A141E2ECA2900497830 /* S2ShapeIndexTests.swift */; }; + 7770ED3F1CCAC21C00C543EC /* R1Interval.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7770ED2C1CCAB37C00C543EC /* R1Interval.swift */; }; + 7770ED401CCAC21C00C543EC /* R2Rect.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7770ED2E1CCAB38900C543EC /* R2Rect.swift */; }; + 7770ED411CCAC21C00C543EC /* R3Vector.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7770ED301CCAB38F00C543EC /* R3Vector.swift */; }; + 7770ED5D1CCB0BCF00C543EC /* S1Angle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7770ED421CCAC90600C543EC /* S1Angle.swift */; }; + 7770ED5F1CCB0BCF00C543EC /* S1Interval.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7770ED471CCAC92C00C543EC /* S1Interval.swift */; }; + 7770ED601CCB0BCF00C543EC /* S2LatLng.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7770ED4A1CCAD86200C543EC /* S2LatLng.swift */; }; + 7770ED611CCB0BCF00C543EC /* S2Point.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7770ED4D1CCADC0000C543EC /* S2Point.swift */; }; + 7770ED621CCB0BCF00C543EC /* S2Polygon.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7770ED501CCB0AE400C543EC /* S2Polygon.swift */; }; + 7770ED631CCB0BCF00C543EC /* S2Loop.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7770ED561CCB0B8900C543EC /* S2Loop.swift */; }; + 7770ED641CCB0BCF00C543EC /* S2Rect.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7770ED521CCB0B0700C543EC /* S2Rect.swift */; }; + 7770ED651CCB0BCF00C543EC /* S2Region.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7770ED541CCB0B3D00C543EC /* S2Region.swift */; }; + 7770ED671CCB0BE700C543EC /* S2Metric.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7770ED661CCB0BE700C543EC /* S2Metric.swift */; }; + 7770ED691CCB0C0000C543EC /* S2Cap.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7770ED681CCB0C0000C543EC /* S2Cap.swift */; }; + 777BBA9E1E30253600514AD7 /* S2Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 777BBA9D1E30253600514AD7 /* S2Tests.swift */; }; + 779A2CEB1D0C8D8B0067EB78 /* S2Cube.swift in Sources */ = {isa = PBXBuildFile; fileRef = 779A2CEA1D0C8D8B0067EB78 /* S2Cube.swift */; }; + 779A2CEC1D0C8DA40067EB78 /* S2Cube.swift in Sources */ = {isa = PBXBuildFile; fileRef = 779A2CEA1D0C8D8B0067EB78 /* S2Cube.swift */; }; + 77A3E5911E347823004DB619 /* S2Polyline.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77A3E5901E347823004DB619 /* S2Polyline.swift */; }; + 77B111041E2ECA5C0032B28C /* S2PointTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77B111031E2ECA5C0032B28C /* S2PointTests.swift */; }; + 77D260F41E2EC9AE0020C806 /* S2LoopTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77D260F31E2EC9AE0020C806 /* S2LoopTests.swift */; }; + 77E163EA1CF3B78400D51F5B /* S2RegionCoverer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7701F8201CCD57070013898B /* S2RegionCoverer.swift */; }; + 77E163EC1CF3B78400D51F5B /* S2RegionCoverer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7701F8201CCD57070013898B /* S2RegionCoverer.swift */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + 7700AE6B1CCFD15200606F25 /* Sphere2GoLibTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = Sphere2GoLibTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 7701F8201CCD57070013898B /* S2RegionCoverer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = S2RegionCoverer.swift; sourceTree = ""; }; + 7701F8221CCD5E2C0013898B /* PriorityQueue.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PriorityQueue.swift; sourceTree = ""; }; + 7705EBB11E2EC8E800F1151A /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 7705EBB21E2EC8E800F1151A /* R1IntervalTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = R1IntervalTests.swift; sourceTree = ""; }; + 7705EBB31E2EC8E800F1151A /* R2RectTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = R2RectTests.swift; sourceTree = ""; }; + 7705EBB41E2EC8E800F1151A /* R3VectorTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = R3VectorTests.swift; sourceTree = ""; }; + 7705EBB91E2EC8FA00F1151A /* S1IntervalTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = S1IntervalTests.swift; sourceTree = ""; }; + 7705EBBA1E2EC8FA00F1151A /* S2CapTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = S2CapTests.swift; sourceTree = ""; }; + 7705EBBB1E2EC8FA00F1151A /* S2CellIdTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = S2CellIdTests.swift; sourceTree = ""; }; + 7705EBBC1E2EC8FA00F1151A /* S2CellTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = S2CellTests.swift; sourceTree = ""; }; + 7705EBBD1E2EC8FA00F1151A /* S2CellUnionTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = S2CellUnionTests.swift; sourceTree = ""; }; + 7705EBBE1E2EC8FA00F1151A /* S2LatLngTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = S2LatLngTests.swift; sourceTree = ""; }; + 771221051E7B2866003B62C8 /* S2PolygonTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = S2PolygonTests.swift; sourceTree = ""; }; + 771221071E7B2898003B62C8 /* S2PolylineTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = S2PolylineTests.swift; sourceTree = ""; }; + 771DD34D1E2EC9D800F5DEDE /* S2MetricTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = S2MetricTests.swift; sourceTree = ""; }; + 771EE8FA1CCBD7FF00BE111F /* S2Cell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = S2Cell.swift; sourceTree = ""; }; + 771EE8FC1CCBDDDC00BE111F /* S2CellId.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = S2CellId.swift; sourceTree = ""; }; + 771EE8FE1CCBF83F00BE111F /* S2Shape.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = S2Shape.swift; sourceTree = ""; }; + 771EE9001CCC000300BE111F /* S2CellUnion.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = S2CellUnion.swift; sourceTree = ""; }; + 771EE9021CCC03FD00BE111F /* S2EdgeUtility.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = S2EdgeUtility.swift; sourceTree = ""; }; + 772EBA371CCC4FAC005D39AC /* S2Matrix.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = S2Matrix.swift; sourceTree = ""; }; + 77633A131E2ECA2900497830 /* S2RegionCovererTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = S2RegionCovererTests.swift; sourceTree = ""; }; + 77633A141E2ECA2900497830 /* S2ShapeIndexTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = S2ShapeIndexTests.swift; sourceTree = ""; }; + 7770ED2C1CCAB37C00C543EC /* R1Interval.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = R1Interval.swift; sourceTree = ""; }; + 7770ED2E1CCAB38900C543EC /* R2Rect.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = R2Rect.swift; sourceTree = ""; }; + 7770ED301CCAB38F00C543EC /* R3Vector.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = R3Vector.swift; sourceTree = ""; }; + 7770ED371CCAC21100C543EC /* Sphere2GoLib.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Sphere2GoLib.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 7770ED391CCAC21100C543EC /* Sphere2Lib.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = Sphere2Lib.h; sourceTree = ""; }; + 7770ED3B1CCAC21100C543EC /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 7770ED421CCAC90600C543EC /* S1Angle.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = S1Angle.swift; sourceTree = ""; }; + 7770ED471CCAC92C00C543EC /* S1Interval.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = S1Interval.swift; sourceTree = ""; }; + 7770ED4A1CCAD86200C543EC /* S2LatLng.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = S2LatLng.swift; sourceTree = ""; }; + 7770ED4D1CCADC0000C543EC /* S2Point.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = S2Point.swift; sourceTree = ""; }; + 7770ED501CCB0AE400C543EC /* S2Polygon.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = S2Polygon.swift; sourceTree = ""; }; + 7770ED521CCB0B0700C543EC /* S2Rect.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = S2Rect.swift; sourceTree = ""; }; + 7770ED541CCB0B3D00C543EC /* S2Region.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = S2Region.swift; sourceTree = ""; }; + 7770ED561CCB0B8900C543EC /* S2Loop.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = S2Loop.swift; sourceTree = ""; }; + 7770ED661CCB0BE700C543EC /* S2Metric.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = S2Metric.swift; sourceTree = ""; }; + 7770ED681CCB0C0000C543EC /* S2Cap.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = S2Cap.swift; sourceTree = ""; }; + 777BBA9D1E30253600514AD7 /* S2Tests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = S2Tests.swift; sourceTree = ""; }; + 779A2CEA1D0C8D8B0067EB78 /* S2Cube.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = S2Cube.swift; sourceTree = ""; }; + 77A3E5901E347823004DB619 /* S2Polyline.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = S2Polyline.swift; sourceTree = ""; }; + 77B111031E2ECA5C0032B28C /* S2PointTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = S2PointTests.swift; sourceTree = ""; }; + 77D260F31E2EC9AE0020C806 /* S2LoopTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = S2LoopTests.swift; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 7700AE681CCFD15200606F25 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 7770ED331CCAC21100C543EC /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 7700AE6C1CCFD15200606F25 /* Sphere2LibTests */ = { + isa = PBXGroup; + children = ( + 7705EBB11E2EC8E800F1151A /* Info.plist */, + 777BBA9D1E30253600514AD7 /* S2Tests.swift */, + 7784CE761E7CB13A007D91D7 /* r */, + 7784CE771E7CB143007D91D7 /* s */, + ); + name = Sphere2LibTests; + path = Sphere2GoLibTests; + sourceTree = ""; + }; + 7770ED151CCAB33000C543EC = { + isa = PBXGroup; + children = ( + 7770ED201CCAB33000C543EC /* Sphere2 */, + 7770ED381CCAC21100C543EC /* Sphere2Lib */, + 7700AE6C1CCFD15200606F25 /* Sphere2LibTests */, + 7770ED1F1CCAB33000C543EC /* Products */, + ); + sourceTree = ""; + }; + 7770ED1F1CCAB33000C543EC /* Products */ = { + isa = PBXGroup; + children = ( + 7770ED371CCAC21100C543EC /* Sphere2GoLib.framework */, + 7700AE6B1CCFD15200606F25 /* Sphere2GoLibTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 7770ED201CCAB33000C543EC /* Sphere2 */ = { + isa = PBXGroup; + children = ( + 7701F8221CCD5E2C0013898B /* PriorityQueue.swift */, + 7770ED2A1CCAB34500C543EC /* r */, + 7770ED2B1CCAB35800C543EC /* s */, + ); + name = Sphere2; + path = Sphere2Go; + sourceTree = ""; + }; + 7770ED2A1CCAB34500C543EC /* r */ = { + isa = PBXGroup; + children = ( + 7770ED2C1CCAB37C00C543EC /* R1Interval.swift */, + 7770ED2E1CCAB38900C543EC /* R2Rect.swift */, + 7770ED301CCAB38F00C543EC /* R3Vector.swift */, + ); + name = r; + sourceTree = ""; + }; + 7770ED2B1CCAB35800C543EC /* s */ = { + isa = PBXGroup; + children = ( + 7770ED421CCAC90600C543EC /* S1Angle.swift */, + 7770ED471CCAC92C00C543EC /* S1Interval.swift */, + 7770ED4A1CCAD86200C543EC /* S2LatLng.swift */, + 7770ED4D1CCADC0000C543EC /* S2Point.swift */, + 779A2CEA1D0C8D8B0067EB78 /* S2Cube.swift */, + 7770ED541CCB0B3D00C543EC /* S2Region.swift */, + 771EE8FE1CCBF83F00BE111F /* S2Shape.swift */, + 7770ED521CCB0B0700C543EC /* S2Rect.swift */, + 7770ED561CCB0B8900C543EC /* S2Loop.swift */, + 77A3E5901E347823004DB619 /* S2Polyline.swift */, + 7770ED501CCB0AE400C543EC /* S2Polygon.swift */, + 7770ED681CCB0C0000C543EC /* S2Cap.swift */, + 7770ED661CCB0BE700C543EC /* S2Metric.swift */, + 771EE8FC1CCBDDDC00BE111F /* S2CellId.swift */, + 771EE8FA1CCBD7FF00BE111F /* S2Cell.swift */, + 771EE9001CCC000300BE111F /* S2CellUnion.swift */, + 771EE9021CCC03FD00BE111F /* S2EdgeUtility.swift */, + 772EBA371CCC4FAC005D39AC /* S2Matrix.swift */, + 7701F8201CCD57070013898B /* S2RegionCoverer.swift */, + ); + name = s; + sourceTree = ""; + }; + 7770ED381CCAC21100C543EC /* Sphere2Lib */ = { + isa = PBXGroup; + children = ( + 7770ED3B1CCAC21100C543EC /* Info.plist */, + 7770ED391CCAC21100C543EC /* Sphere2Lib.h */, + ); + name = Sphere2Lib; + path = Sphere2GoLib; + sourceTree = ""; + }; + 7784CE761E7CB13A007D91D7 /* r */ = { + isa = PBXGroup; + children = ( + 7705EBB21E2EC8E800F1151A /* R1IntervalTests.swift */, + 7705EBB31E2EC8E800F1151A /* R2RectTests.swift */, + 7705EBB41E2EC8E800F1151A /* R3VectorTests.swift */, + ); + name = r; + sourceTree = ""; + }; + 7784CE771E7CB143007D91D7 /* s */ = { + isa = PBXGroup; + children = ( + 7705EBB91E2EC8FA00F1151A /* S1IntervalTests.swift */, + 7705EBBA1E2EC8FA00F1151A /* S2CapTests.swift */, + 7705EBBB1E2EC8FA00F1151A /* S2CellIdTests.swift */, + 7705EBBC1E2EC8FA00F1151A /* S2CellTests.swift */, + 7705EBBD1E2EC8FA00F1151A /* S2CellUnionTests.swift */, + 7705EBBE1E2EC8FA00F1151A /* S2LatLngTests.swift */, + 77D260F31E2EC9AE0020C806 /* S2LoopTests.swift */, + 77B111031E2ECA5C0032B28C /* S2PointTests.swift */, + 771DD34D1E2EC9D800F5DEDE /* S2MetricTests.swift */, + 77633A131E2ECA2900497830 /* S2RegionCovererTests.swift */, + 77633A141E2ECA2900497830 /* S2ShapeIndexTests.swift */, + 771221051E7B2866003B62C8 /* S2PolygonTests.swift */, + 771221071E7B2898003B62C8 /* S2PolylineTests.swift */, + ); + name = s; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXHeadersBuildPhase section */ + 7770ED341CCAC21100C543EC /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXHeadersBuildPhase section */ + +/* Begin PBXNativeTarget section */ + 7700AE6A1CCFD15200606F25 /* Sphere2GoLibTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 7700AE751CCFD15200606F25 /* Build configuration list for PBXNativeTarget "Sphere2GoLibTests" */; + buildPhases = ( + 7700AE671CCFD15200606F25 /* Sources */, + 7700AE681CCFD15200606F25 /* Frameworks */, + 7700AE691CCFD15200606F25 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Sphere2GoLibTests; + productName = Sphere2GoLibTests; + productReference = 7700AE6B1CCFD15200606F25 /* Sphere2GoLibTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + 7770ED361CCAC21100C543EC /* Sphere2GoLib */ = { + isa = PBXNativeTarget; + buildConfigurationList = 7770ED3C1CCAC21100C543EC /* Build configuration list for PBXNativeTarget "Sphere2GoLib" */; + buildPhases = ( + 7770ED321CCAC21100C543EC /* Sources */, + 7770ED331CCAC21100C543EC /* Frameworks */, + 7770ED341CCAC21100C543EC /* Headers */, + 7770ED351CCAC21100C543EC /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Sphere2GoLib; + productName = Sphere2GoLib; + productReference = 7770ED371CCAC21100C543EC /* Sphere2GoLib.framework */; + productType = "com.apple.product-type.framework"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 7770ED161CCAB33000C543EC /* Project object */ = { + isa = PBXProject; + attributes = { + LastSwiftUpdateCheck = 0730; + LastUpgradeCheck = 1210; + ORGANIZATIONNAME = "Axel Huesemann"; + TargetAttributes = { + 7700AE6A1CCFD15200606F25 = { + CreatedOnToolsVersion = 7.3; + DevelopmentTeam = V89SMR8J9T; + LastSwiftMigration = 1210; + }; + 7770ED361CCAC21100C543EC = { + CreatedOnToolsVersion = 7.3; + DevelopmentTeam = V89SMR8J9T; + LastSwiftMigration = 1210; + }; + }; + }; + buildConfigurationList = 7770ED191CCAB33000C543EC /* Build configuration list for PBXProject "Sphere2Go" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 7770ED151CCAB33000C543EC; + productRefGroup = 7770ED1F1CCAB33000C543EC /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 7770ED361CCAC21100C543EC /* Sphere2GoLib */, + 7700AE6A1CCFD15200606F25 /* Sphere2GoLibTests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 7700AE691CCFD15200606F25 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 7770ED351CCAC21100C543EC /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 7700AE671CCFD15200606F25 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 7700AEA61CD04FCF00606F25 /* S2EdgeUtility.swift in Sources */, + 7700AEA71CD04FCF00606F25 /* S2Matrix.swift in Sources */, + 7700AEA31CD04FCF00606F25 /* S2CellId.swift in Sources */, + 77B111041E2ECA5C0032B28C /* S2PointTests.swift in Sources */, + 777BBA9E1E30253600514AD7 /* S2Tests.swift in Sources */, + 77E163EC1CF3B78400D51F5B /* S2RegionCoverer.swift in Sources */, + 7705EBC01E2EC8FA00F1151A /* S1IntervalTests.swift in Sources */, + 7700AE9A1CD04FCF00606F25 /* S2Point.swift in Sources */, + 7700AE9B1CD04FCF00606F25 /* S2Region.swift in Sources */, + 7700AE9D1CD04FCF00606F25 /* S2Rect.swift in Sources */, + 7700AE981CD04FCF00606F25 /* S1Interval.swift in Sources */, + 7700AE7B1CD013DB00606F25 /* R2Rect.swift in Sources */, + 7705EBC51E2EC8FA00F1151A /* S2LatLngTests.swift in Sources */, + 7700AEA41CD04FCF00606F25 /* S2Cell.swift in Sources */, + 7705EBC21E2EC8FA00F1151A /* S2CellIdTests.swift in Sources */, + 7700AEAA1CD04FD400606F25 /* PriorityQueue.swift in Sources */, + 7705EBC11E2EC8FA00F1151A /* S2CapTests.swift in Sources */, + 771221061E7B2866003B62C8 /* S2PolygonTests.swift in Sources */, + 7700AEA01CD04FCF00606F25 /* S2Cap.swift in Sources */, + 7705EBB81E2EC8E800F1151A /* R3VectorTests.swift in Sources */, + 7705EBC41E2EC8FA00F1151A /* S2CellUnionTests.swift in Sources */, + 7700AE961CD04FCF00606F25 /* S1Angle.swift in Sources */, + 7700AE9E1CD04FCF00606F25 /* S2Loop.swift in Sources */, + 7705EBB61E2EC8E800F1151A /* R1IntervalTests.swift in Sources */, + 7705EBC31E2EC8FA00F1151A /* S2CellTests.swift in Sources */, + 77633A171E2ECA2900497830 /* S2ShapeIndexTests.swift in Sources */, + 77633A161E2ECA2900497830 /* S2RegionCovererTests.swift in Sources */, + 7700AE9C1CD04FCF00606F25 /* S2Shape.swift in Sources */, + 7700AE991CD04FCF00606F25 /* S2LatLng.swift in Sources */, + 7705EBB71E2EC8E800F1151A /* R2RectTests.swift in Sources */, + 771221081E7B2898003B62C8 /* S2PolylineTests.swift in Sources */, + 771DD34F1E2EC9D800F5DEDE /* S2MetricTests.swift in Sources */, + 7700AE7E1CD0257000606F25 /* R3Vector.swift in Sources */, + 7700AEA51CD04FCF00606F25 /* S2CellUnion.swift in Sources */, + 7700AE9F1CD04FCF00606F25 /* S2Polygon.swift in Sources */, + 7700AEA11CD04FCF00606F25 /* S2Metric.swift in Sources */, + 7700AE781CCFDC7C00606F25 /* R1Interval.swift in Sources */, + 77D260F41E2EC9AE0020C806 /* S2LoopTests.swift in Sources */, + 779A2CEC1D0C8DA40067EB78 /* S2Cube.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 7770ED321CCAC21100C543EC /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 7770ED3F1CCAC21C00C543EC /* R1Interval.swift in Sources */, + 77E163EA1CF3B78400D51F5B /* S2RegionCoverer.swift in Sources */, + 7701F8231CCD5E2C0013898B /* PriorityQueue.swift in Sources */, + 7770ED601CCB0BCF00C543EC /* S2LatLng.swift in Sources */, + 779A2CEB1D0C8D8B0067EB78 /* S2Cube.swift in Sources */, + 771EE8FD1CCBDDDC00BE111F /* S2CellId.swift in Sources */, + 7770ED411CCAC21C00C543EC /* R3Vector.swift in Sources */, + 771EE8FF1CCBF83F00BE111F /* S2Shape.swift in Sources */, + 7770ED401CCAC21C00C543EC /* R2Rect.swift in Sources */, + 7770ED671CCB0BE700C543EC /* S2Metric.swift in Sources */, + 771EE9031CCC03FD00BE111F /* S2EdgeUtility.swift in Sources */, + 77A3E5911E347823004DB619 /* S2Polyline.swift in Sources */, + 7770ED5F1CCB0BCF00C543EC /* S1Interval.swift in Sources */, + 7770ED691CCB0C0000C543EC /* S2Cap.swift in Sources */, + 7770ED641CCB0BCF00C543EC /* S2Rect.swift in Sources */, + 7770ED5D1CCB0BCF00C543EC /* S1Angle.swift in Sources */, + 772EBA381CCC4FAC005D39AC /* S2Matrix.swift in Sources */, + 7770ED611CCB0BCF00C543EC /* S2Point.swift in Sources */, + 771EE9011CCC000300BE111F /* S2CellUnion.swift in Sources */, + 7770ED631CCB0BCF00C543EC /* S2Loop.swift in Sources */, + 771EE8FB1CCBD7FF00BE111F /* S2Cell.swift in Sources */, + 7770ED651CCB0BCF00C543EC /* S2Region.swift in Sources */, + 7770ED621CCB0BCF00C543EC /* S2Polygon.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + 7700AE731CCFD15200606F25 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + DEVELOPMENT_TEAM = V89SMR8J9T; + INFOPLIST_FILE = Sphere2GoLibTests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = com.here.Sphere2GoLibTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + }; + name = Debug; + }; + 7700AE741CCFD15200606F25 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + DEVELOPMENT_TEAM = V89SMR8J9T; + INFOPLIST_FILE = Sphere2GoLibTests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = com.here.Sphere2GoLibTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; + SWIFT_VERSION = 5.0; + }; + name = Release; + }; + 7770ED251CCAB33000C543EC /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_VERSION = 4.0; + }; + name = Debug; + }; + 7770ED261CCAB33000C543EC /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SWIFT_VERSION = 4.0; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 7770ED3D1CCAC21100C543EC /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_IDENTITY = "iPhone Developer"; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; + "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = V89SMR8J9T; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + INFOPLIST_FILE = Sphere2GoLib/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = com.here.Sphere2GoLib; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Debug; + }; + 7770ED3E1CCAC21100C543EC /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_IDENTITY = "iPhone Developer"; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; + "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = V89SMR8J9T; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + INFOPLIST_FILE = Sphere2GoLib/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = com.here.Sphere2GoLib; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 7700AE751CCFD15200606F25 /* Build configuration list for PBXNativeTarget "Sphere2GoLibTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 7700AE731CCFD15200606F25 /* Debug */, + 7700AE741CCFD15200606F25 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 7770ED191CCAB33000C543EC /* Build configuration list for PBXProject "Sphere2Go" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 7770ED251CCAB33000C543EC /* Debug */, + 7770ED261CCAB33000C543EC /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 7770ED3C1CCAC21100C543EC /* Build configuration list for PBXNativeTarget "Sphere2GoLib" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 7770ED3D1CCAC21100C543EC /* Debug */, + 7770ED3E1CCAC21100C543EC /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 7770ED161CCAB33000C543EC /* Project object */; +} diff --git a/Sphere2Go.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/Sphere2Go.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..0e4c156 --- /dev/null +++ b/Sphere2Go.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/Sphere2Go.xcodeproj/project.xcworkspace/xcuserdata/axel.xcuserdatad/UserInterfaceState.xcuserstate b/Sphere2Go.xcodeproj/project.xcworkspace/xcuserdata/axel.xcuserdatad/UserInterfaceState.xcuserstate new file mode 100644 index 0000000..9998ba3 Binary files /dev/null and b/Sphere2Go.xcodeproj/project.xcworkspace/xcuserdata/axel.xcuserdatad/UserInterfaceState.xcuserstate differ diff --git a/Sphere2Go.xcodeproj/xcuserdata/axel.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist b/Sphere2Go.xcodeproj/xcuserdata/axel.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist new file mode 100644 index 0000000..f607aeb --- /dev/null +++ b/Sphere2Go.xcodeproj/xcuserdata/axel.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist @@ -0,0 +1,127 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Sphere2Go.xcodeproj/xcuserdata/axel.xcuserdatad/xcschemes/Sphere2.xcscheme b/Sphere2Go.xcodeproj/xcuserdata/axel.xcuserdatad/xcschemes/Sphere2.xcscheme new file mode 100644 index 0000000..bff3ba1 --- /dev/null +++ b/Sphere2Go.xcodeproj/xcuserdata/axel.xcuserdatad/xcschemes/Sphere2.xcscheme @@ -0,0 +1,76 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Sphere2Go.xcodeproj/xcuserdata/axel.xcuserdatad/xcschemes/Sphere2Lib.xcscheme b/Sphere2Go.xcodeproj/xcuserdata/axel.xcuserdatad/xcschemes/Sphere2Lib.xcscheme new file mode 100644 index 0000000..4e825b4 --- /dev/null +++ b/Sphere2Go.xcodeproj/xcuserdata/axel.xcuserdatad/xcschemes/Sphere2Lib.xcscheme @@ -0,0 +1,95 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Sphere2Go.xcodeproj/xcuserdata/axel.xcuserdatad/xcschemes/Sphere2LibTests.xcscheme b/Sphere2Go.xcodeproj/xcuserdata/axel.xcuserdatad/xcschemes/Sphere2LibTests.xcscheme new file mode 100644 index 0000000..484b828 --- /dev/null +++ b/Sphere2Go.xcodeproj/xcuserdata/axel.xcuserdatad/xcschemes/Sphere2LibTests.xcscheme @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/Sphere2Go.xcodeproj/xcuserdata/axel.xcuserdatad/xcschemes/xcschememanagement.plist b/Sphere2Go.xcodeproj/xcuserdata/axel.xcuserdatad/xcschemes/xcschememanagement.plist new file mode 100644 index 0000000..99c7368 --- /dev/null +++ b/Sphere2Go.xcodeproj/xcuserdata/axel.xcuserdatad/xcschemes/xcschememanagement.plist @@ -0,0 +1,42 @@ + + + + + SchemeUserState + + Sphere2.xcscheme + + orderHint + 0 + + Sphere2Lib.xcscheme + + orderHint + 1 + + Sphere2LibTests.xcscheme + + orderHint + 2 + + + SuppressBuildableAutocreation + + 7700AE6A1CCFD15200606F25 + + primary + + + 7770ED1D1CCAB33000C543EC + + primary + + + 7770ED361CCAC21100C543EC + + primary + + + + + diff --git a/Sphere2Go/PriorityQueue.swift b/Sphere2Go/PriorityQueue.swift new file mode 100644 index 0000000..67568e3 --- /dev/null +++ b/Sphere2Go/PriorityQueue.swift @@ -0,0 +1,107 @@ +// +// PriorityQueue.swift +// Sphere2 +// + +// This code was inspired by Section 2.4 of Algorithms by Sedgewick & Wayne, 4th Edition + +public struct PriorityQueue { + + fileprivate var heap = [T]() + private let ordered: (T, T) -> Bool + + public init(ascending: Bool = false, startingValues: [T] = []) { + if ascending { + ordered = { $0 > $1 } + } else { + ordered = { $0 < $1 } + } + for value in startingValues { push(value) } + } + + // MARK: the usual suspects + + public var count: Int { return heap.count } + + public var isEmpty: Bool { return heap.isEmpty } + + // insert new element into queue O(lg n) + public mutating func push(_ element: T) { + heap.append(element) + swim(heap.count - 1) + } + + // remove and return the element with the highest priority (or lowest if ascending) O(lg n) + public mutating func pop() -> T? { + if heap.isEmpty { return nil } + if heap.count == 1 { return heap.removeFirst() } + heap.swapAt(0, heap.count - 1) + let temp = heap.removeLast() + sink(0) + return temp + } + + // like pop() without removing it. O(1) + public func peek() -> T? { + return heap.first + } + + // remove all elements + public mutating func clear() { + heap.removeAll(keepingCapacity: false) + } + + // MARK: private sink and swim + // based on Sedgewick p. 316 + + private mutating func sink(_ index: Int) { + var index = index + while 2 * index + 1 < heap.count { + var j = 2 * index + 1 + if j < (heap.count - 1) && ordered(heap[j], heap[j + 1]) { j += 1 } + if !ordered(heap[index], heap[j]) { break } + heap.swapAt(index, j) + index = j + } + } + + private mutating func swim(_ index: Int) { + var index = index + while index > 0 && ordered(heap[(index - 1) / 2], heap[index]) { + heap.swapAt((index - 1) / 2, index) + index = (index - 1) / 2 + } + } + +} + +// MARK: IteratorProtocol + +extension PriorityQueue: IteratorProtocol { + public typealias Element = T + mutating public func next() -> Element? { return pop() } +} + +// MARK: Sequence + +extension PriorityQueue: Sequence { + public typealias Generator = PriorityQueue + public func generate() -> Generator { return self } +} + +// MARK: Collection + +extension PriorityQueue: Collection { + public typealias Index = Int + public var startIndex: Int { return heap.startIndex } + public var endIndex: Int { return heap.endIndex } + public subscript(i: Int) -> T { return heap[i] } + public func index(after: Int) -> Int { return after+1 } +} + +// MARK: CustomStringConvertible, CustomDebugStringConvertible + +extension PriorityQueue: CustomStringConvertible, CustomDebugStringConvertible { + public var description: String { return heap.description } + public var debugDescription: String { return heap.debugDescription } +} diff --git a/Sphere2Go/R1Interval.swift b/Sphere2Go/R1Interval.swift new file mode 100644 index 0000000..35e834c --- /dev/null +++ b/Sphere2Go/R1Interval.swift @@ -0,0 +1,178 @@ +// +// R1Interval.swift +// Sphere2 +// + +import Foundation + +// package r1 +// import fmt, math + +// R1Interval represents a closed interval on ℝ. +// Zero-length intervals (where Lo == Hi) represent single points. +// If Lo > Hi then the interval is empty. +// The major differences from the C++ version are: +// - a few other miscellaneous operations +struct R1Interval: Equatable, CustomStringConvertible { + + // + let lo: Double + let hi: Double + + // epsilon is a small number that represents a reasonable level of noise between two + // values that can be considered to be equal. + static let epsilon = 1e-14 + + // MARK: inits / factory + + init(lo: Double, hi: Double) { + self.lo = lo + self.hi = hi + } + + init(point: Double) { + self.lo = point + self.hi = point + } + + static let empty = R1Interval(lo:1.0, hi: 0.0) + + // MARK: protocols + + static func ==(lhs: R1Interval, rhs: R1Interval) -> Bool { + // returns true iff the intervals contains the same points + return lhs.lo == rhs.lo && lhs.hi == rhs.hi || (lhs.isEmpty() && rhs.isEmpty()) + } + + var description: String { + let l = String(format: "%.7f", lo) + let h = String(format: "%.7f", hi) + return "[\(l), \(h)]" + } + + // MARK: tests + + // IsEmpty reports whether the interval is empty. + func isEmpty() -> Bool { + return lo > hi + } + + // Contains returns true iff the interval contains p. + func contains(_ point: Double) -> Bool { + return lo <= point && point <= hi + } + + // ContainsInterval returns true iff the interval contains other. + func contains(_ interval: R1Interval) -> Bool { + if interval.isEmpty() { + return true + } + return lo <= interval.lo && interval.hi <= hi + } + + // InteriorContains returns true iff the the interval strictly contains p. + func interiorContains(_ point: Double) -> Bool { + return lo < point && point < hi + } + + // InteriorContainsInterval returns true iff the interval strictly contains other. + func interiorContains(_ interval: R1Interval) -> Bool { + if interval.isEmpty() { + return true + } + return lo < interval.lo && interval.hi < hi + } + + // Intersects returns true iff the interval contains any points in common with other. + func intersects(_ interval: R1Interval) -> Bool { + if lo <= interval.lo { + return interval.lo <= hi && interval.lo <= interval.hi // interval.lo ∈ i and interval is not empty + } + return lo <= interval.hi && lo <= hi // lo ∈ interval and i is not empty + } + + // InteriorIntersects returns true iff the interior of the interval contains any points in common with other, including the latter's boundary. + func interiorIntersects(_ interval: R1Interval) -> Bool { + return interval.lo < hi && lo < interval.hi && lo < hi && interval.lo <= hi + } + + // Intersection returns the interval containing all points common to i and j. + func intersection(_ interval: R1Interval) -> R1Interval { + // Empty intervals do not need to be special-cased. + return R1Interval(lo: max(lo, interval.lo), hi: min(hi, interval.hi)) + } + + // ApproxEqual reports whether the interval can be transformed into the + // given interval by moving each endpoint a small distance. + // The empty interval is considered to be positioned arbitrarily on the + // real line, so any interval with a small enough length will match + // the empty interval. + func approxEquals(_ interval: R1Interval) -> Bool { + if isEmpty() { + return interval.length() <= 2 * R1Interval.epsilon + } + if interval.isEmpty() { + return length() <= 2 * R1Interval.epsilon + } + return fabs(interval.lo-lo) <= R1Interval.epsilon && fabs(interval.hi-hi) <= R1Interval.epsilon + } + + // MARK: computed members + + // Center returns the midpoint of the interval. + // It is undefined for empty intervals. + func center() -> Double { + return 0.5 * (lo + hi) + } + + // Length returns the length of the interval. + // The length of an empty interval is negative. + func length() -> Double { + return hi - lo + } + + // MARK: arithmetic + + // AddPoint returns the interval expanded so that it contains the given point. + func add(_ point: Double) -> R1Interval { + if isEmpty() { + return R1Interval(lo: point, hi: point) + } + if point < lo { + return R1Interval(lo: point, hi: hi) + } + if point > hi { + return R1Interval(lo: lo, hi: point) + } + return self + } + + // ClampPoint returns the closest point in the interval to the given point "p". + // The interval must be non-empty. + func clamp(_ point: Double) -> Double { + return max(lo, min(hi, point)) + } + + // Expanded returns an interval that has been expanded on each side by margin. + // If margin is negative, then the function shrinks the interval on + // each side by margin instead. The resulting interval may be empty. Any + // expansion of an empty interval remains empty. + func expanded(_ margin: Double) -> R1Interval { + if isEmpty() { + return self + } + return R1Interval(lo: lo - margin, hi: hi + margin) + } + + // Union returns the smallest interval that contains this interval and the given interval. + func union(_ interval: R1Interval) -> R1Interval { + if isEmpty() { + return interval + } + if interval.isEmpty() { + return self + } + return R1Interval(lo: min(lo, interval.lo), hi: max(hi, interval.hi)) + } + +} diff --git a/Sphere2Go/R2Rect.swift b/Sphere2Go/R2Rect.swift new file mode 100644 index 0000000..cc5d472 --- /dev/null +++ b/Sphere2Go/R2Rect.swift @@ -0,0 +1,234 @@ +// +// Rect.swift +// Sphere2 +// + +import Foundation + +// package r2 +// import fmt, r1 + + +// R2Point represents a point in ℝ². +struct R2Point: Equatable, CustomStringConvertible { + + // + let x: Double + let y: Double + + init(x: Double, y: Double) { + self.x = x + self.y = y + } + + // MARK: protocols + + static func ==(lhs: R2Point, rhs: R2Point) -> Bool { + return lhs.x == rhs.x && lhs.y == rhs.y + } + + var description: String { + return String(format: "[%f, %f]", x, y) + } + +} + +// R2Rect represents a closed axis-aligned rectangle in the (x,y) plane. + +struct R2Rect: Equatable, CustomStringConvertible { + let x: R1Interval + let y: R1Interval + + // MARK: inits / factory + + init(x: R1Interval, y: R1Interval) { + self.x = x + self.y = y + } + + // RectFromPoints constructs a rect that contains the given points. + init(p0: R2Point, p1: R2Point) { + let x = R1Interval(lo: p0.x, hi: p1.x) + let y = R1Interval(lo: p0.y, hi: p1.y) + self.init(x: x, y: y) + } + +// // RectFromPoints constructs a rect that contains the given points. +// init(ps: [R2Point]) { +// // Because the default value on interval is 0,0, we need to manually +// // define the interval from the first point passed in as our starting +// // interval, otherwise we end up with the case of passing in +// // R2Point{0.2, 0.3} and getting the starting Rect of {0, 0.2}, {0, 0.3} +// // instead of the Rect {0.2, 0.2}, {0.3, 0.3} which is not correct. +// if ps.count == 0 { +// return R2Rect.empty +// } +// +// let x = R1Interval(lo: p[0].x, hi: p[0].x) +// let y = R1Interval(lo: p[1].y, hi: p[1].y) +// self.init(x: x, y: y) +// +//// for _, p := range pts[1:] { +//// r = AddPoint(p) +//// } +// } + + // RectFromCenterSize constructs a rectangle with the given center and size. + // Both dimensions of size must be non-negative. + init(center: R2Point, size: R2Point) { + let x = R1Interval(lo: center.x - size.x/2, hi: center.x + size.x/2) + let y = R1Interval(lo: center.y - size.y/2, hi: center.y + size.y/2) + self.init(x: x, y: y) + } + + // EmptyRect constructs the canonical empty rectangle. Use IsEmpty() to test + // for empty rectangles, since they have more than one representation. A Rect + // is not the same as the EmptyRect. + static let empty = R2Rect(x: R1Interval.empty, y: R1Interval.empty) + + // MARK: protocols + + static func ==(lhs: R2Rect, rhs: R2Rect) -> Bool { + return lhs.x == rhs.x && lhs.y == rhs.y + } + + var description: String { + return "X \(x), Y \(y)" + } + + // MARK: tests + + // IsValid reports whether the rectangle is valid. + // This requires the width to be empty iff the height is empty. + func isValid() -> Bool { + return x.isEmpty() == y.isEmpty() + } + + // IsEmpty reports whether the rectangle is empty. + func isEmpty() -> Bool { + return x.isEmpty() + } + + // ApproxEquals returns true if the x- and y-intervals of the two rectangles are + // the same up to the given tolerance. + func approxEquals(_ rect: R2Rect) -> Bool { + return x.approxEquals(rect.x) && y.approxEquals(rect.y) + } + + // MARK: computed members + + // Vertices returns all four vertices of the rectangle. Vertices are returned in + // CCW direction starting with the lower left corner. + func vertices() -> [R2Point] { + return [ + R2Point(x: x.lo, y: y.lo), + R2Point(x: x.hi, y: y.lo), + R2Point(x: x.hi, y: y.hi), + R2Point(x: x.lo, y: y.hi)] + } + + // Center returns the center of the rectangle in (x,y)-space + func center() -> R2Point { + return R2Point(x: x.center(), y: y.center()) + } + + // Size returns the width and height of this rectangle in (x,y)-space. Empty + // rectangles have a negative width and height. + func size() -> R2Point { + return R2Point(x: x.length(), y: y.length()) + } + + // MARK: tests + + // ContainsPoint reports whether the rectangle contains the given point. + // Rectangles are closed regions, i.e. they contain their boundary. + func contains(_ point: R2Point) -> Bool { + return x.contains(point.x) && y.contains(point.y) + } + + // InteriorContainsPoint returns true iff the given point is contained in the interior + // of the region (i.e. the region excluding its boundary). + func interiorContains(_ point: R2Point) -> Bool { + return x.interiorContains(point.x) && y.interiorContains(point.y) + } + + // Contains reports whether the rectangle contains the given rectangle. + func contains(_ rect: R2Rect) -> Bool { + return x.contains(rect.x) && y.contains(rect.y) + } + + // InteriorContains reports whether the interior of this rectangle contains all of the + // points of the given other rectangle (including its boundary). + func interiorContains(_ rect: R2Rect) -> Bool { + return x.interiorContains(rect.x) && y.interiorContains(rect.y) + } + + // Intersects reports whether this rectangle and the other rectangle have any points in common. + func intersects(_ rect: R2Rect) -> Bool { + return x.intersects(rect.x) && y.intersects(rect.y) + } + + // InteriorIntersects reports whether the interior of this rectangle intersects + // any point (including the boundary) of the given other rectangle. + func interiorIntersects(_ rect: R2Rect) -> Bool { + return x.interiorIntersects(rect.x) && y.interiorIntersects(rect.y) + } + + //MARK: arithmetic + + // AddPoint expands the rectangle to include the given point. The rectangle is + // expanded by the minimum amount possible. + func add(_ point: R2Point) -> R2Rect { + return R2Rect(x: x.add(point.x), y: y.add(point.y)) + } + + // AddRect expands the rectangle to include the given rectangle. This is the + // same as replacing the rectangle by the union of the two rectangles, but + // is more efficient. + func add(_ rect: R2Rect) -> R2Rect { + return R2Rect(x: x.union(rect.x), y: y.union(rect.y)) + } + + // ClampPoint returns the closest point in the rectangle to the given point. + // The rectangle must be non-empty. + func clamp(_ point: R2Point) -> R2Point { + return R2Point(x: x.clamp(point.x), y: y.clamp(point.y)) + } + + // Expanded returns a rectangle that has been expanded in the x-direction + // by margin.x, and in y-direction by margin.y. If either margin is empty, + // then shrink the interval on the corresponding sides instead. The resulting + // rectangle may be empty. Any expansion of an empty rectangle remains empty. + func expanded(_ margin: R2Point) -> R2Rect { + let xx = x.expanded(margin.x) + let yy = y.expanded(margin.y) + if xx.isEmpty() || yy.isEmpty() { + return R2Rect.empty + } + return R2Rect(x: xx, y: yy) + } + + // ExpandedByMargin returns a R2Rect that has been expanded by the amount on all sides. + func expanded(_ margin: Double) -> R2Rect { + return expanded(R2Point(x: margin, y: margin)) + } + + // Union returns the smallest rectangle containing the union of this rectangle and + // the given rectangle. + func union(_ rect: R2Rect) -> R2Rect { + return R2Rect(x: x.union(rect.x), y: y.union(rect.y)) + } + + // Intersection returns the smallest rectangle containing the intersection of this + // rectangle and the given rectangle. + func intersection(_ rect: R2Rect) -> R2Rect { + let xx = x.intersection(rect.x) + let yy = y.intersection(rect.y) + if xx.isEmpty() || yy.isEmpty() { + return R2Rect.empty + } + + return R2Rect(x: xx, y: yy) + } + +} diff --git a/Sphere2Go/R3Vector.swift b/Sphere2Go/R3Vector.swift new file mode 100644 index 0000000..f749d02 --- /dev/null +++ b/Sphere2Go/R3Vector.swift @@ -0,0 +1,131 @@ +// +// R3Vector.swift +// Sphere2 +// + +import Foundation + +// package r3 +// import fmt, math, s1 + +// R3Vector represents a point in ℝ³. +struct R3Vector: Equatable, CustomStringConvertible, Hashable { + + static let epsilon = 1e-14 + + // + let x: Double + let y: Double + let z: Double + + // MARK: inits + + init(x: Double, y: Double, z: Double) { + self.x = x + self.y = y + self.z = z + } + + // MARK: protocols + + static func ==(lhs: R3Vector, rhs: R3Vector) -> Bool { + return lhs.x == rhs.x && lhs.y == rhs.y && lhs.z == rhs.z + } + + var description: String { + return "(\(x), \(y), \(z))" + } + + // MARK: tests + + // ApproxEqual reports whether v and other are equal within a small epsilon. + func approxEquals(_ vector: R3Vector) -> Bool { + return fabs(x-vector.x) < R3Vector.epsilon && fabs(y-vector.y) < R3Vector.epsilon && fabs(z-vector.z) < R3Vector.epsilon + } + + // IsUnit returns whether this vector is of approximately unit length. + func isUnit() -> Bool { + return fabs(norm2()-1) <= R3Vector.epsilon + } + + // MARK: computed members + + // Norm returns the vector's norm. + func norm() -> Double { + return sqrt(dot(self)) + } + + // Norm2 returns the square of the norm. + func norm2() -> Double { + return dot(self) + } + + // Normalize returns a unit vector in the same direction as + func normalize() -> R3Vector { + if x == 0.0 && y == 0.0 && z == 0.0 { + return self + } + return mul(1.0 / norm()) + } + + // Abs returns the vector with nonnegative components. + func abs() -> R3Vector { + return R3Vector(x: fabs(x), y: fabs(y), z: fabs(z)) + } + + // Ortho returns a unit vector that is orthogonal to + // Ortho(-v) = -Ortho(v) for all + func ortho() -> R3Vector { + // Grow a component other than the largest in v, to guarantee that they aren't + // parallel (which would make the cross product zero). + let vector: R3Vector + if fabs(x) > fabs(y) { + vector = R3Vector(x: 0.012, y: 1.0, z: 0.00457) + } else { + vector = R3Vector(x: 1.0, y: 0.0053, z: 0.00457) + } + return cross(vector).normalize() + } + + var s2: S2Point { + return S2Point(raw: self) + } + + // MARK: arithmetic + + // Add returns the standard vector sum of v and other. + func add(_ vector: R3Vector) -> R3Vector { + return R3Vector(x: x + vector.x, y: y + vector.y, z: z + vector.z) + } + + // Sub returns the standard vector difference of v and other. + func sub(_ vector: R3Vector) -> R3Vector { + return R3Vector(x: x - vector.x, y: y - vector.y, z: z - vector.z) + } + + // Mul returns the standard scalar product of v and m. + func mul(_ m: Double) -> R3Vector { + return R3Vector(x: m * x, y: m * y, z: m * z) + } + + // Dot returns the standard dot product of v and other. + func dot(_ vector: R3Vector) -> Double { + return x*vector.x + y*vector.y + z*vector.z + } + + // Cross returns the standard cross product of v and other. + func cross(_ vector: R3Vector) -> R3Vector { + return R3Vector(x: y*vector.z - z*vector.y, y: z*vector.x - x*vector.z, z: x*vector.y - y*vector.x) + } + + // Distance returns the Euclidean distance between v and other. + func distance(_ vector: R3Vector) -> Double { + return sub(vector).norm() + } + + // Angle returns the angle between v and vector. + func angle(_ vector: R3Vector) -> Double { + return atan2(cross(vector).norm(), dot(vector)) + } + +} diff --git a/Sphere2Go/S1Angle.swift b/Sphere2Go/S1Angle.swift new file mode 100644 index 0000000..273ae21 --- /dev/null +++ b/Sphere2Go/S1Angle.swift @@ -0,0 +1,80 @@ +// +// Angle.swift +// Sphere2 +// + +import Foundation + +// Angle represents a 1D angle. +// The major differences from the C++ version are: +// - no unsigned E5/E6/E7 methods +// - no S2Point or S2LatLng constructors +// - no comparison or arithmetic operators +typealias S1Angle = Double + +let toRadians = .pi / 180.0 +let toDegrees = 180.0 / .pi + +extension S1Angle { + +// init(degrees: Double) { +// self = degrees * toRadians +// } +// +// init(radians: Double) { +// self = radians +// } +// + // Normalized returns an equivalent angle in [0, 2π). + func normalize() -> S1Angle { + var rad = fmod(self, 2.0 * .pi) + if rad < 0.0 { + rad += 2.0 * .pi + } + return rad + } + +} + + +// ChordAngle represents the angle subtended by a chord (i.e., the straight +// line segment connecting two points on the sphere). Its representation +// makes it very efficient for computing and comparing distances, but unlike +// Angle it is only capable of representing angles between 0 and π radians. +// Generally, ChordAngle should only be used in loops where many angles need +// to be calculated and compared. Otherwise it is simpler to use Angle. +// +// ChordAngles are represented by the squared chord length, which can +// range from 0 to 4. Positive infinity represents an infinite squared length. +struct ChordAngle { + + let value: Double + + // NegativeChordAngle represents a chord angle smaller than the zero angle. + // The only valid operations on a NegativeChordAngle are comparisons and + // Angle conversions. + static let NegativeChordAngle = ChordAngle(value: -1) + + // StraightChordAngle represents a chord angle of 180 degrees (a "straight angle"). + // This is the maximum finite chord angle. + static let StraightChordAngle = ChordAngle(value: 4) + + // InfChordAngle represents a chord angle larger than any finite chord angle. + // The only valid operations on an InfChordAngle are comparisons and Angle conversions. + static let InfChordAngle = ChordAngle(value: Double.greatestFiniteMagnitude) + + init(value: Double) { + self.value = value + } + + // isInf reports whether this ChordAngle is infinite. + func isInf() -> Bool { + return value == Double.greatestFiniteMagnitude + } + + // isSpecial reports whether this ChordAngle is one of the special cases. + func isSpecial() -> Bool { + return value < 0.0 || isInf() + } + +} diff --git a/Sphere2Go/S1Interval.swift b/Sphere2Go/S1Interval.swift new file mode 100644 index 0000000..3f9d8d9 --- /dev/null +++ b/Sphere2Go/S1Interval.swift @@ -0,0 +1,326 @@ +// +// S2Interval.swift +// Sphere2 +// + +import Foundation + +// package s1 +// import math, strconv + + +// S1Interval represents a closed interval on a unit circle. +// Zero-length intervals (where Lo == Hi) represent single points. +// If Lo > Hi then the interval is "inverted". +// The point at (-1, 0) on the unit circle has two valid representations, +// [π,π] and [-π,-π]. We normalize the latter to the former in S1IntervalFromEndpoints. +// There are two special intervals that take advantage of that: +// - the full interval, [-π,π], and +// - the empty interval, [π,-π]. +// Treat the exported fields as read-only. +// The major differences from the C++ version are: +// - no validity checking on construction, etc. (not a bug?) +// - a few operations +struct S1Interval { + + // + let lo: Double + let hi: Double + + // + static let epsilon = 1e-14 + + // MARK: inits / factory + + init(lo: Double, hi: Double) { + self.lo = lo + self.hi = hi + } + + // S1IntervalFromEndpoints constructs a new interval from endpoints. + // Both arguments must be in the range [-π,π]. This function allows inverted intervals + // to be created. + init(lo_endpoint: Double, hi_endpoint: Double) { + self.lo = lo_endpoint == -.pi && hi_endpoint != .pi ? .pi : lo_endpoint + self.hi = hi_endpoint == -.pi && lo_endpoint != .pi ? .pi : hi_endpoint + } + + // EmptyInterval returns an empty interval. + static let empty = S1Interval(lo: .pi, hi: -.pi) + + // FullInterval returns a full interval. + static let full = S1Interval(lo: -.pi, hi: .pi) + + // MARK: protocols + + var description: String { + return "[\(lo), \(hi)]" + // like "[%.7f, %.7f]" +// return "[" + strconv.FormatFloat(Lo, 'f', 7, 64) + ", " + strconv.FormatFloat(hi, 'f', 7, 64) + "]" + } + + // MARK: tests + + // isValid reports whether the interval is valid. + func isValid() -> Bool { + return fabs(lo) <= .pi && fabs(hi) <= .pi && !(lo == -.pi && hi != .pi) && !(hi == -.pi && lo != .pi) + } + + // isFull reports whether the interval is full. + func isFull() -> Bool { + return hi - lo == 2 * .pi + } + + // isEmpty reports whether the interval is empty. + func isEmpty() -> Bool { + return lo - hi == 2 * .pi + } + + // isInverted reports whether the interval is inverted; that is, whether Lo > Hi. + func isInverted() -> Bool { + return lo > hi + } + + // Center returns the midpoint of the interval. + // It is undefined for full and empty intervals. + func center() -> Double { + let c = 0.5 * (lo + hi) + if !isInverted() { + return c + } + if c <= 0 { + return c + .pi + } + return c - .pi + } + + // Assumes p ∈ (-π,π]. + func fastContains(_ point: Double) -> Bool { + if isInverted() { + return (point >= lo || point <= hi) && !isEmpty() + } + return point >= lo && point <= hi + } + + // Contains returns true iff the interval contains p. + // Assumes p ∈ [-π,π]. + func contains(_ point: Double) -> Bool { + if point == -.pi { + return fastContains(.pi) + } + return fastContains(point) + } + + // ContainsInterval returns true iff the interval contains other. + func contains(_ interval: S1Interval) -> Bool { + if isInverted() { + if interval.isInverted() { + return interval.lo >= lo && interval.hi <= hi + } + return (interval.lo >= lo || interval.hi <= hi) && !isEmpty() + } + if interval.isInverted() { + return isFull() || interval.isEmpty() + } + return interval.lo >= lo && interval.hi <= hi + } + + // InteriorContains returns true iff the interior of the interval contains p. + // Assumes p ∈ [-π,π]. + func interiorContains(_ point: Double) -> Bool { + var point = point + if point == -.pi { + point = .pi + } + if isInverted() { + return point > lo || point < hi + } + return (point > lo && point < hi) || isFull() + } + + // InteriorContainsInterval returns true iff the interior of the interval contains other. + func interiorContains(_ interval: S1Interval) -> Bool { + if isInverted() { + if interval.isInverted() { + return (interval.lo > lo && interval.hi < hi) || interval.isEmpty() + } + return interval.lo > lo || interval.hi < hi + } + if interval.isInverted() { + return isFull() || interval.isEmpty() + } + return (interval.lo > lo && interval.hi < hi) || isFull() + } + + // Intersects returns true iff the interval contains any points in common with interval. + func intersects(_ interval: S1Interval) -> Bool { + if isEmpty() || interval.isEmpty() { + return false + } + if isInverted() { + return interval.isInverted() || interval.lo <= hi || interval.hi >= lo + } + if interval.isInverted() { + return interval.lo <= hi || interval.hi >= lo + } + return interval.lo <= hi && interval.hi >= lo + } + + // InteriorIntersects returns true iff the interior of the interval contains any points in common with other, including the latter's boundary. + func interiorIntersects(_ interval: S1Interval) -> Bool { + if isEmpty() || interval.isEmpty() || lo == hi { + return false + } + if isInverted() { + return interval.isInverted() || interval.lo < hi || interval.hi > lo + } + if interval.isInverted() { + return interval.lo < hi || interval.hi > lo + } + return (interval.lo < hi && interval.hi > lo) || isFull() + } + + // MARK: computed members + + // Length returns the length of the interval. + // The length of an empty interval is negative. + func length() -> Double { + var l = hi - lo + if l >= 0.0 { + return l + } + l += 2 * .pi + if l > 0 { + return l + } + return -1 + } + + // MARK: arithmetic + + // Compute distance from a to b in [0,2π], in a numerically stable way. + static func positiveDistance(_ a: Double, _ b: Double) -> Double { + let d = b - a + if d >= 0 { + return d + } + return (b + .pi) - (a - .pi) + } + + // Union returns the smallest interval that contains both the interval and other. + func union(_ interval: S1Interval) -> S1Interval { + if interval.isEmpty() { + return self + } + if fastContains(interval.lo) { + if fastContains(interval.hi) { + // Either interval ⊂ i, or i ∪ interval is the full interval. + if contains(interval) { + return self + } + return S1Interval.full + } + return S1Interval(lo: lo, hi: interval.hi) + } + if fastContains(interval.hi) { + return S1Interval(lo: interval.lo, hi: hi) + } + + // Neither endpoint of interval is in Either i ⊂ other, or i and other are disjoint. + if isEmpty() || interval.fastContains(lo) { + return interval + } + + // This is the only hard case where we need to find the closest pair of endpoints. + if S1Interval.positiveDistance(interval.hi, lo) < S1Interval.positiveDistance(hi, interval.lo) { + return S1Interval(lo: interval.lo, hi: hi) + } + return S1Interval(lo: lo, hi: interval.hi) + } + + // Intersection returns the smallest interval that contains the intersection of the interval and other. + func intersection(_ interval: S1Interval) -> S1Interval { + if interval.isEmpty() { + return S1Interval.empty + } + if fastContains(interval.lo) { + if fastContains(interval.hi) { + // Either other ⊂ i, or i and other intersect twice. Neither are empty. + // In the first case we want to return i (which is shorter than other). + // In the second case one of them is inverted, and the smallest interval + // that covers the two disjoint pieces is the shorter of i and other. + // We thus want to pick the shorter of i and other in both cases. + if interval.length() < length() { + return interval + } + return self + } + return S1Interval(lo: interval.lo, hi: hi) + } + if fastContains(interval.hi) { + return S1Interval(lo: lo, hi: interval.hi) + } + + // Neither endpoint of other is in Either i ⊂ other, or i and other are disjoint. + if interval.fastContains(lo) { + return self + } + return S1Interval.empty + } + + // AddPoint returns the interval expanded by the minimum amount necessary such + // that it contains the given point "p" (an angle in the range [-Pi, Pi]). + func add(_ point: Double) -> S1Interval { + var point = point + if fabs(point) > .pi { + return self + } + if point == -.pi { + point = .pi + } + if fastContains(point) { + return self + } + if isEmpty() { + return S1Interval(lo: point, hi: point) + } + if S1Interval.positiveDistance(point, lo) < S1Interval.positiveDistance(hi, point) { + return S1Interval(lo: point, hi: hi) + } + return S1Interval(lo: lo, hi: point) + } + + // Expanded returns an interval that has been expanded on each side by margin. + // If margin is negative, then the function shrinks the interval on + // each side by margin instead. The resulting interval may be empty or + // full. Any expansion (positive or negative) of a full interval remains + // full, and any expansion of an empty interval remains empty. + func expanded(_ margin: Double) -> S1Interval { + if margin >= 0 { + if isEmpty() { + return self + } + // Check whether this interval will be full after expansion, allowing + // for a 1-bit rounding error when computing each endpoint. + if length() + 2 * margin + 2 * S1Interval.epsilon >= 2.0 * .pi { + return S1Interval.full + } + } else { + if isFull() { + return self + } + // Check whether this interval will be empty after expansion, allowing + // for a 1-bit rounding error when computing each endpoint. + if length() + 2 * margin - 2 * S1Interval.epsilon <= 0 { + return S1Interval.empty + } + } + let l = (lo-margin).truncatingRemainder(dividingBy: 2.0 * .pi) + let h = (hi+margin).truncatingRemainder(dividingBy: 2.0 * .pi) + if l <= -.pi { + return S1Interval(lo_endpoint: .pi, hi_endpoint: h) + } + return S1Interval(lo_endpoint: l, hi_endpoint: h) + } + +} diff --git a/Sphere2Go/S2Cap.swift b/Sphere2Go/S2Cap.swift new file mode 100644 index 0000000..d7a65de --- /dev/null +++ b/Sphere2Go/S2Cap.swift @@ -0,0 +1,398 @@ +// +// S2Cap.swift +// Sphere2 +// + +import Foundation + +// package s2 +// import fmt, math, r1, s1 + +// Cap represents a disc-shaped region defined by a center and radius. +// Technically this shape is called a "spherical cap" (rather than disc) +// because it is not planar; the cap represents a portion of the sphere that +// has been cut off by a plane. The boundary of the cap is the circle defined +// by the intersection of the sphere and the plane. For containment purposes, +// the cap is a closed set, i.e. it contains its boundary. +// +// For the most part, you can use a spherical cap wherever you would use a +// disc in planar geometry. The radius of the cap is measured along the +// surface of the sphere (rather than the straight-line distance through the +// interior). Thus a cap of radius π/2 is a hemisphere, and a cap of radius +// π covers the entire sphere. +// +// The center is a point on the surface of the unit sphere. (Hence the need for +// it to be of unit length.) +// +// Internally, the cap is represented by its center and "height". The height +// is the distance from the center point to the cutoff plane. This +// representation is much more efficient for containment tests than the +// (center, radius) representation. There is also support for "empty" and +// "full" caps, which contain no points and all points respectively. +// +// The zero value of Cap is an invalid cap. Use EmptyCap to get a valid empty cap. +// Differences from C++ +// Centroid, Union +struct S2Cap: S2Region { + + // with radius 1.0, 0.0 means 0.0 size, and 2.0 means the entire globe + static let zeroHeight = 0.0 + static let fullHeight = 2.0 + // negative is invalid and used as a marker for empty, which is different from zero + static let emptyHeight = -1.0 + + // TODO check if this is a good replacement value +// static let roundUp = 1.0 + 1.0 / Double(1 << 52) + static let roundUp = nextafter(1.0, 2.0) + + // centerPoint is the default center for S2Caps + static let centerPoint = S2Point(x: 1.0, y: 0, z: 0) + + // center is a unit vector + // height is the distance from the plane that creates the cap + let center: S2Point + let height: Double + + // MARK: inits / factory + + // CapFromCenterHeight constructs a cap with the given center and height. A + // negative height yields an empty cap; a height of 2 or more yields a full cap. + init(centerNormalized: S2Point, height: Double) { + self.center = centerNormalized + self.height = height + } + + // CapFromCenterHeight constructs a cap with the given center and height. A + // negative height yields an empty cap; a height of 2 or more yields a full cap. + init(center: S2Point, height: Double) { + self.center = center + self.height = height + } + + // CapFromPoint constructs a cap containing a single point. + init(point: S2Point) { + self.init(center: point, height: S2Cap.zeroHeight) + } + + // CapFromCenterAngle constructs a cap with the given center and angle. + init(centerNormalized: S2Point, angle: Double) { + let height = S2Cap.radiusToHeight(angle) + self.init(centerNormalized: centerNormalized, height: height) + } + + // CapFromCenterAngle constructs a cap with the given center and angle. + init(center: S2Point, angle: Double) { + let height = S2Cap.radiusToHeight(angle) + self.init(center: center, height: height) + } + + // CapFromCenterArea constructs a cap with the given center and surface area. + // Note that the area can also be interpreted as the solid angle subtended by the + // cap (because the sphere has unit radius). A negative area yields an empty cap; + // an area of 4*π or more yields a full cap. + init(center: S2Point, area: Double) { + let height = area / (.pi*2.0) + self.init(center: center, height: height) + } + + // EmptyCap returns a cap that contains no points. + static let empty = S2Cap(center: centerPoint, height: emptyHeight) + + // FullCap returns a cap that contains all points. + static let full = S2Cap(center: centerPoint, height: fullHeight) + + // MARK: protocols + + var description: String { + return "[Center=\(center), Radius=\(radius() * toDegrees)]" + } + + // MARK: tests + + // IsValid reports whether the Cap is considered valid. + // Heights are normalized so that they do not exceed 2. + func isValid() -> Bool { + return center.isUnit() && height <= S2Cap.fullHeight + } + + // IsEmpty reports whether the cap is empty, i.e. it contains no points. + func isEmpty() -> Bool { + return height < S2Cap + .zeroHeight + } + + // IsFull reports whether the cap is full, i.e. it contains all points. + func isFull() -> Bool { + return height == S2Cap.fullHeight + } + + // MARK: contain / intersect + + // Contains reports whether this cap contains the other. + func contains(_ cap: S2Cap) -> Bool { + // In a set containment sense, every cap contains the empty cap. + if isFull() || cap.isEmpty() { + return true + } + return radius() >= center.distance(cap.center) + cap.radius() + } + + // Intersects reports whether this cap intersects the other cap. + // i.e. whether they have any points in common. + func intersects(_ cap: S2Cap) -> Bool { + if isEmpty() || cap.isEmpty() { + return false + } + return radius() + cap.radius() >= center.distance(cap.center) + } + + // InteriorIntersects reports whether this caps interior intersects the other cap. + func interiorIntersects(_ cap: S2Cap) -> Bool { + // Make sure this cap has an interior and the other cap is non-empty. + if height <= S2Cap.zeroHeight || cap.isEmpty() { + return false + } + return radius() + cap.radius() > center.distance(cap.center) + } + + // ContainsPoint reports whether this cap contains the point. + func contains(_ point: S2Point) -> Bool { + return center.v.sub(point.v).norm2() <= 2 * height + } + + // InteriorContainsPoint reports whether the point is within the interior of this cap. + func interiorContains(_ point: S2Point) -> Bool { + return isFull() || center.v.sub(point.v).norm2() < 2 * height + } + + // ContainsCell reports whether the cap contains the given cell. + func contains(_ cell: Cell) -> Bool { + // If the cap does not contain all cell vertices, return false. + var vertices = [S2Point]() + for k in 0..<4 { + let vertex = cell.vertex(k) + vertices.append(vertex) + if !contains(vertex) { + return false + } + } + // Otherwise, return true if the complement of the cap does not intersect the cell. + return !complement().intersects(cell, vertices: vertices) + } + + // IntersectsCell reports whether the cap intersects the cell. + func intersects(_ cell: Cell) -> Bool { + // If the cap contains any cell vertex, return true. + var vertices = [S2Point]() + for k in 0..<4 { + let vertex = cell.vertex(k) + vertices.append(vertex) + if contains(vertex) { + return true + } + } + return intersects(cell, vertices: vertices) + } + + // intersects reports whether the cap intersects any point of the cell excluding + // its vertices (which are assumed to already have been checked). + func intersects(_ cell: Cell, vertices: [S2Point]) -> Bool { + // If the cap is a hemisphere or larger, the cell and the complement of the cap + // are both convex. Therefore since no vertex of the cell is contained, no other + // interior point of the cell is contained either. + if height >= 1 { + return false + } + // We need to check for empty caps due to the center check just below. + if isEmpty() { + return false + } + // Optimization: return true if the cell contains the cap center. This allows half + // of the edge checks below to be skipped. + if cell.contains(center) { + return true + } + // At this point we know that the cell does not contain the cap center, and the cap + // does not contain any cell vertex. The only way that they can intersect is if the + // cap intersects the interior of some edge. + let sin2Angle = height * (2 - height) + for k in 0..<4 { + let edge = cell.edge(k) + let dot = center.v.dot(edge.v) + if dot > 0.0 { + // The center is in the interior half-space defined by the edge. We do not need + // to consider these edges, since if the cap intersects this edge then it also + // intersects the edge on the opposite side of the cell, because the center is + // not contained with the cell. + continue + } + // The Norm2() factor is necessary because "edge" is not normalized. + if dot * dot > sin2Angle * edge.v.norm2() { + return false + } + // Otherwise, the great circle containing this edge intersects the interior of the cap. We just + // need to check whether the point of closest approach occurs between the two edge endpoints. + let dir = edge.v.cross(center.v) + if dir.dot(vertices[k].v) < 0 && dir.dot(vertices[(k+1) & 3].v) > 0 { + return true + } + } + return false + } + + // MARK: computed members + + // Radius returns the cap's radius. + func radius() -> Double { + if isEmpty() { + return S2Cap.emptyHeight + } + // This could also be computed as acos(1 - height_), but the following + // formula is much more accurate when the cap height is small. It + // follows from the relationship h = 1 - cos(r) = 2 sin^2(r/2). + return 2.0 * asin(sqrt(0.5 * height)) + } + + // Area returns the surface area of the Cap on the unit sphere. + func area() -> Double { + return 2.0 * .pi * max(S2Cap.zeroHeight, height) + } + + // Complement returns the complement of the interior of the cap. A cap and its + // complement have the same boundary but do not share any interior points. + // The complement operator is not a bijection because the complement of a + // singleton cap (containing a single point) is the same as the complement + // of an empty cap. + func complement() -> S2Cap { + let height: Double + if isFull() { + height = S2Cap.emptyHeight + } else { + height = S2Cap.fullHeight - max(self.height, S2Cap.zeroHeight) + } + let antiCenter = center.inverse() + return S2Cap(center: antiCenter, height: height) + } + + // CapBound returns a bounding spherical cap. This is not guaranteed to be exact. + func capBound() -> S2Cap { + return self + } + + // RectBound returns a bounding latitude-longitude rectangle. + // The bounds are not guaranteed to be tight. + func rectBound() -> S2Rect { + if isEmpty() { + return S2Rect.empty + } + // + let capAngle = radius() + let midAngle = center.latitude() + var allLongitudes = false + // Check whether cap includes the south pole. + var latLo = midAngle - capAngle + if latLo <= -.pi/2 { + latLo = -.pi / 2 + allLongitudes = true + } + // Check whether cap includes the north pole. + var latHi = midAngle + capAngle + if latHi >= .pi/2 { + latHi = .pi / 2 + allLongitudes = true + } + let lat = R1Interval(lo: latLo, hi: latHi) + // + var lng = S1Interval.full + if !allLongitudes { + // Compute the range of longitudes covered by the cap. We use the law + // of sines for spherical triangles. Consider the triangle ABC where + // A is the north pole, B is the center of the cap, and C is the point + // of tangency between the cap boundary and a line of longitude. Then + // C is a right angle, and letting a,b,c denote the sides opposite A,B,C, + // we have sin(a)/sin(A) = sin(c)/sin(C), or sin(A) = sin(a)/sin(c). + // Here "a" is the cap angle, and "c" is the colatitude (90 degrees + // minus the latitude). This formula also works for negative latitudes. + // + // The formula for sin(a) follows from the relationship h = 1 - cos(a). + let sinA = sqrt(height * (2 - height)) + let sinC = cos(center.latitude()) + if sinA <= sinC { + let angleA = asin(sinA / sinC) + let lngLo = (center.longitude() - angleA).truncatingRemainder(dividingBy: .pi*2) + let lngHi = (center.longitude() + angleA).truncatingRemainder(dividingBy: .pi*2) + lng = S1Interval(lo: lngLo, hi: lngHi) + } + } + return S2Rect(lat: lat, lng: lng) + } + + static let epsilon = 1e-14 + + // ApproxEqual reports if this caps' center and height are within + // a reasonable epsilon from the other cap. + func approxEquals(_ cap: S2Cap) -> Bool { + return center.approxEquals(cap.center) && + fabs(height-cap.height) <= S2Cap.epsilon || + isEmpty() && cap.height <= S2Cap.epsilon || + cap.isEmpty() && height <= S2Cap.epsilon || + isFull() && cap.height >= 2 - S2Cap.epsilon || + cap.isFull() && height >= 2 - S2Cap.epsilon + } + + // AddPoint increases the cap if necessary to include the given point. If this cap is empty, + // then the center is set to the point with a zero height. p must be unit-length. + func add(_ point: S2Point) -> S2Cap { + if isEmpty() { + return S2Cap(center: point, height: S2Cap.zeroHeight) + } + // To make sure that the resulting cap actually includes this point, + // we need to round up the distance calculation. That is, after + // calling cap.AddPoint(p), cap.Contains(p) should be true. + let dist2 = center.v.sub(point.v).norm2() + let height = max(self.height, S2Cap.roundUp * 0.5 * dist2) + return S2Cap(centerNormalized: center, height: height) + } + + // MARK: arithmetic + + // AddCap increases the cap height if necessary to include the other cap. If this cap is empty, + // it is set to the other cap. + func add(_ cap: S2Cap) -> S2Cap { + if isEmpty() { + return cap + } + if cap.isEmpty() { + return self + } + // + let radius = center.angle(cap.center) + cap.radius() + let height = max(self.height, S2Cap.roundUp * S2Cap.radiusToHeight(radius)) + return S2Cap(centerNormalized: center, height: height) + } + + // Expanded returns a new cap expanded by the given angle. If the cap is empty, + // it returns an empty cap. + func expanded(_ distance: Double) -> S2Cap { + if isEmpty() { + return S2Cap.empty + } + return S2Cap(centerNormalized: center, angle: radius() + distance) + } + + // radiusToHeight converts an s1.Angle into the height of the cap. + static func radiusToHeight(_ radius: Double) -> Double { + if radius < 0 { + return S2Cap.emptyHeight + } + if radius >= .pi { + return S2Cap.fullHeight + } + // The height of the cap can be computed as 1 - cos(r), but this isn't very + // accurate for angles close to zero (where cos(r) is almost 1). The + // formula below has much better precision. + let d = sin(0.5 * radius) + return 2 * d * d + } + +} diff --git a/Sphere2Go/S2Cell.swift b/Sphere2Go/S2Cell.swift new file mode 100644 index 0000000..f26c211 --- /dev/null +++ b/Sphere2Go/S2Cell.swift @@ -0,0 +1,317 @@ +// +// S2Cell.swift +// Sphere2 +// + +import Foundation + + +// package s2 +// import math, r1, r2, s1 + +// Cell is an S2 region object that represents a cell. Unlike CellIDs, +// it supports efficient containment and intersection tests. However, it is +// also a more expensive representation. +struct Cell: S2Region { + + // TODO(akashagrawal): move these package private variables to a more appropriate location. + static let dblEpsilon = nextafter(1.0, 2.0) - 1.0 + static let poleMinLat = asin(sqrt(1.0 / 3.0)) - 0.5 * dblEpsilon + + // + let face: UInt8 + let level: UInt8 +// let orientation: UInt8 + let id: CellId + let uv: R2Rect + + // MARK: inits / factory + +// init(face: UInt8, level: UInt8, orientation: UInt8, id: CellId, uv: R2Rect) { +// face: UInt8 +// let level: UInt8 +// let orientation: UInt8 +// let id: CellId +// let uv: R2Rect +// } + + // CellFromCellID constructs a Cell corresponding to the given CellID. + init(id: CellId) { + self.id = id + level = UInt8(id.level()) + let (f, i, j, _) = id.faceIJOrientation() + face = UInt8(f) +// orientation = UInt8(o) + uv = CellId.ijLevelToBoundUV(i: i, j: j, level: Int(level)) + } + + // CellFromPoint constructs a cell for the given Point. + init(point: S2Point) { + let cellId = CellId(point: point) + self.init(id: cellId) + } + + // CellFromLatLng constructs a cell for the given LatLng. + init (latLng: LatLng) { + let cellId = CellId(latLng: latLng) + self.init(id: cellId) + } + + // MARK: tests + + // IsLeaf returns whether this Cell is a leaf or not. + func isLeaf() -> Bool { + return level == UInt8(CellId.maxLevel) + } + + // MARK: computed members + + // SizeIJ returns the CellID value for the cells level. + func sizeIJ() -> Int { + return CellId.sizeIJ(Int(level)) + } + + // Vertex returns the k-th vertex of the cell (k = [0,3]) in CCW order + // (lower left, lower right, upper right, upper left in the UV plane). + func vertex(_ k: Int) -> S2Point { + let face = Int(self.face) + let u = uv.vertices()[k].x + let v = uv.vertices()[k].y + return S2Point(raw: S2Cube(face: face, u: u, v: v).vector()) + } + + // Edge returns the inward-facing normal of the great circle passing through + // the CCW ordered edge from vertex k to vertex k+1 (mod 4). + func edge(_ k: Int) -> S2Point { + switch k { + case 0: + return S2Cube.vNorm(face: Int(face), v: uv.y.lo, invert: false) // Bottom + case 1: + return S2Cube.uNorm(face: Int(face), u: uv.x.hi, invert: false) // Right + case 2: + return S2Cube.vNorm(face: Int(face), v: uv.y.hi, invert: true) // Top + default: + return S2Cube.uNorm(face: Int(face), u: uv.x.lo, invert: true) // Left + } + } + + // ExactArea returns the area of this cell as accurately as possible. + func exactArea() -> Double { + let (v0, v1, v2, v3) = (vertex(0), vertex(1), vertex(2), vertex(3)) + return S2Point.pointArea(v0, v1, v2) + S2Point.pointArea(v0, v2, v3) + } + + // ApproxArea returns the approximate area of this cell. This method is accurate + // to within 3% percent for all cell sizes and accurate to within 0.1% for cells + // at level 5 or higher (i.e. squares 350km to a side or smaller on the Earth's + // surface). It is moderately cheap to compute. + func approxArea() -> Double { + // All cells at the first two levels have the same area. + if level < 2 { + return averageArea() + } + + // First, compute the approximate area of the cell when projected + // perpendicular to its normal. The cross product of its diagonals gives + // the normal, and the length of the normal is twice the projected area. + let flatArea = 0.5 * (vertex(2).v.sub(vertex(0).v).cross(vertex(3).v.sub(vertex(1).v)).norm()) + + // Now, compensate for the curvature of the cell surface by pretending + // that the cell is shaped like a spherical cap. The ratio of the + // area of a spherical cap to the area of its projected disc turns out + // to be 2 / (1 + sqrt(1 - r*r)) where r is the radius of the disc. + // For example, when r=0 the ratio is 1, and when r=1 the ratio is 2. + // Here we set Pi*r*r == flatArea to find the equivalent disc. + return flatArea * 2 / (1.0 + sqrt(1 - min(1.0 / .pi * flatArea, 1))) + } + + // AverageArea returns the average area of cells at the level of this cell. + // This is accurate to within a factor of 1.7. + func averageArea() -> Double { + return Metric.avgArea.value(Int(level)) + } + + // MARK: derive lat/lng from uv + + // latitude returns the latitude of the cell vertex given by (i,j), where "i" and "j" are either 0 or 1. + func latitude(i: Int, j: Int) -> Double { + var u: Double + var v: Double + switch (i, j) { + case (0, 0): + u = uv.x.lo + v = uv.y.lo + case (0, 1): + u = uv.x.lo + v = uv.y.hi + case (1, 0): + u = uv.x.hi + v = uv.y.lo + case (1, 1): + u = uv.x.hi + v = uv.y.hi + default: + fatalError("i and/or j is out of bound") + } + let p = S2Point(raw: S2Cube(face: Int(face), u: u, v: v).vector()) + return p.latitude() + } + + // longitude returns the longitude of the cell vertex given by (i,j), where "i" and "j" are either 0 or 1. + func longitude(i: Int, j: Int) -> Double { + var u: Double + var v: Double + switch (i, j) { + case (0, 0): + u = uv.x.lo + v = uv.y.lo + case (0, 1): + u = uv.x.lo + v = uv.y.hi + case (1, 0): + u = uv.x.hi + v = uv.y.lo + case (1, 1): + u = uv.x.hi + v = uv.y.hi + default: + fatalError("i and/or j is out of bound") + } + let p = S2Point(raw: S2Cube(face: Int(face), u: u, v: v).vector()) + return p.longitude() + } + + // RectBound returns the bounding rectangle of this cell. + func rectBound() -> S2Rect { + if level > 0 { + // Except for cells at level 0, the latitude and longitude extremes are + // attained at the vertices. Furthermore, the latitude range is + // determined by one pair of diagonally opposite vertices and the + // longitude range is determined by the other pair. + // + // We first determine which corner (i,j) of the cell has the largest + // absolute latitude. To maximize latitude, we want to find the point in + // the cell that has the largest absolute z-coordinate and the smallest + // absolute x- and y-coordinates. To do this we look at each coordinate + // (u and v), and determine whether we want to minimize or maximize that + // coordinate based on the axis direction and the cell's (u,v) quadrant. + let u = uv.x.lo + uv.x.hi + let v = uv.y.lo + uv.y.hi + var i = 0 + var j = 0 + if S2Cube.uAxis(face: Int(face)).z == 0 { + if u < 0 { + i = 1 + } + } else if u > 0 { + i = 1 + } + if S2Cube.vAxis(face: Int(face)).z == 0 { + if v < 0 { + j = 1 + } + } else if v > 0 { + j = 1 + } + let lat = R1Interval(point: latitude(i: i, j: j)).add(latitude(i: 1-i, j: 1-j)) + let lng = S1Interval.empty.add(longitude(i: i, j: 1-j)).add(longitude(i: 1-i, j: j)) + + // We grow the bounds slightly to make sure that the bounding rectangle + // contains LatLngFromPoint(P) for any point P inside the loop L defined by the + // four *normalized* vertices. Note that normalization of a vector can + // change its direction by up to 0.5 * dblEpsilon radians, and it is not + // enough just to add Normalize calls to the code above because the + // latitude/longitude ranges are not necessarily determined by diagonally + // opposite vertex pairs after normalization. + // + // We would like to bound the amount by which the latitude/longitude of a + // contained point P can exceed the bounds computed above. In the case of + // longitude, the normalization error can change the direction of rounding + // leading to a maximum difference in longitude of 2 * dblEpsilon. In + // the case of latitude, the normalization error can shift the latitude by + // up to 0.5 * dblEpsilon and the other sources of error can cause the + // two latitudes to differ by up to another 1.5 * dblEpsilon, which also + // leads to a maximum difference of 2 * dblEpsilon. + return S2Rect(lat: lat, lng: lng).expanded(LatLng(lat: 2 * Cell.dblEpsilon, lng: 2 * Cell.dblEpsilon)).polarClosure() + } + + // The 4 cells around the equator extend to +/-45 degrees latitude at the + // midpoints of their top and bottom edges. The two cells covering the + // poles extend down to +/-35.26 degrees at their vertices. The maximum + // error in this calculation is 0.5 * dblEpsilon. + let bound: S2Rect + switch face { + case 0: + bound = S2Rect(lat: R1Interval(lo: -.pi / 4, hi: .pi / 4), lng: S1Interval(lo: -.pi / 4, hi: .pi / 4)) + case 1: + bound = S2Rect(lat: R1Interval(lo: -.pi / 4, hi: .pi / 4), lng: S1Interval(lo: .pi / 4, hi: 3 * .pi / 4)) + case 2: + bound = S2Rect(lat: R1Interval(lo: Cell.poleMinLat, hi: .pi / 2), lng: S1Interval.full) + case 3: + bound = S2Rect(lat: R1Interval(lo: -.pi / 4, hi: .pi / 4), lng: S1Interval(lo: 3 * .pi / 4, hi: -3 * .pi / 4)) + case 4: + bound = S2Rect(lat: R1Interval(lo: -.pi / 4, hi: .pi / 4), lng: S1Interval(lo: -3 * .pi / 4, hi: -.pi / 4)) + default: + bound = S2Rect(lat: R1Interval(lo: -.pi / 2, hi: -Cell.poleMinLat), lng: S1Interval.full) + } + + // Finally, we expand the bound to account for the error when a point P is + // converted to an LatLng to test for containment. (The bound should be + // large enough so that it contains the computed LatLng of any contained + // point, not just the infinite-precision version.) We don't need to expand + // longitude because longitude is calculated via a single call to math.Atan2, + // which is guaranteed to be semi-monotonic. + return bound.expanded(LatLng(lat: Cell.dblEpsilon, lng: 0.0)) + } + + // CapBound returns the bounding cap of this cell. + func capBound() -> S2Cap { + // We use the cell center in (u,v)-space as the cap axis. This vector is very close + // to GetCenter() and faster to compute. Neither one of these vectors yields the + // bounding cap with minimal surface area, but they are both pretty close. + let p = S2Point(raw: S2Cube(face: Int(face), u: uv.center().x, v: uv.center().y).vector().normalize()) + var cap = S2Cap(point: p) + for k in 0..<4 { + cap = cap.add(vertex(k)) + } + return cap + } + + // MARK: contains / intersects + + // IntersectsCell reports whether the intersection of this cell and the other cell is not nil. + func intersects(_ cell: Cell) -> Bool { + return id.intersects(cell.id) + } + + // ContainsCell reports whether this cell contains the other cell. + func contains(_ cell: Cell) -> Bool { + return id.contains(cell.id) + } + + // ContainsPoint reports whether this cell contains the given point. Note that + // unlike Loop/Polygon, a Cell is considered to be a closed set. This means + // that a point on a Cell's edge or vertex belong to the Cell and the relevant + // adjacent Cells too. + // + // If you want every point to be contained by exactly one Cell, + // you will need to convert the Cell to a Loop. + func contains(_ point: S2Point) -> Bool { + guard let cube = S2Cube(point: point, face: Int(face)) else { + return false + } + let uv2 = R2Point(x: cube.u, y: cube.v) + + // Expand the (u,v) bound to ensure that + // + // CellFromPoint(p).ContainsPoint(p) + // + // is always true. To do this, we need to account for the error when + // converting from (u,v) coordinates to (s,t) coordinates. In the + // normal case the total error is at most dblEpsilon. + return uv.expanded(Cell.dblEpsilon).contains(uv2) + } + + // TODO(roberts, or $SOMEONE): Differences from C++, almost everything else still. + // Implement the accessor methods on the internal fields. +} diff --git a/Sphere2Go/S2CellId.swift b/Sphere2Go/S2CellId.swift new file mode 100644 index 0000000..6e87781 --- /dev/null +++ b/Sphere2Go/S2CellId.swift @@ -0,0 +1,727 @@ +// +// S2CellId.swift +// Sphere2 +// + +import Foundation + +// package s2 +// import bytes, fmt, math, strconv, strings, r1, r2, r3 + +prefix func -(id: UInt64) -> UInt64 { + return UInt64(bitPattern: -Int64(bitPattern: id)) +} + +prefix func -(id: UInt32) -> UInt32 { + return UInt32(bitPattern: -Int32(bitPattern: id)) +} + +// CellId uniquely identifies a cell in the S2 cell decomposition. +// The most significant 3 bits encode the face number (0-5). The +// remaining 61 bits encode the position of the center of this cell +// along the Hilbert curve on that face. The zero value and the value +// (1<<64)-1 are invalid cell IDs. The first compares less than any +// valid cell ID, the second as greater than any valid cell ID. +// The major differences from the C++ version is that barely anything is implemented. +struct CellId: Equatable, Hashable { + + // + static let faceBits = 3 + static let numFaces = 6 + static let maxLevel = 30 + static let posBits = 2 * maxLevel + 1 + static let maxSize = 1 << maxLevel + static let wrapOffset = UInt64(numFaces) << UInt64(posBits) + + // Constants related to the bit mangling in the Cell ID. + static let lookupBits = 4 + static let swapMask = 0x01 + static let invertMask = 0x02 + + // + let id: UInt64 + + // MARK: inits / factory + + init(id: UInt64) { + self.id = id + } + + // CellIdFromFacePosLevel returns a cell given its face in the range + // [0,5], the 61-bit Hilbert curve position pos within that face, and + // the level in the range [0,maxLevel]. The position in the cell ID + // will be truncated to correspond to the Hilbert curve position at + // the center of the returned cell. + init(face: Int, pos: UInt64, level: Int) { + let id = UInt64(face) << UInt64(CellId.posBits) + pos | 1 + self.init(id: id) + self = self.parent(level) + } + + // CellIdFromFace returns the cell corresponding to a given S2 cube face. + init(face: Int) { + let id = UInt64(face) << UInt64(CellId.posBits) + CellId.lsb(0) + self.init(id: id) + } + + // CellIdFromLatLng returns the leaf cell containing ll. + init(latLng: LatLng) { + let point = latLng.toPoint() + self.init(point: point) + } + + init(token: String) { + // add trailing zeros + let token2 = token.padding(toLength: 16, withPad: "0", startingAt: 0) + // deal with failure + guard let id = UInt64(token2, radix: 16) else { + self.init(id: 0) + return + } + self.init(id: id) + } + + // MARK: protocols + + static func ==(lhs:CellId, rhs: CellId) -> Bool { + return lhs.id == rhs.id + } + + // String returns the string representation of the cell ID in the form "1/3210". + var description: String { + if !isValid() { + return "Invalid: " + String(id, radix: 16) + } + var s = "" + for level in 1...self.level() { + s += "\(childPosition(level))" + } + return "\(face())/\(s)" + } + + // MARK: string token + + func toToken() -> String { + // pad leading zeros + var pad = "" + for i in 0..<16 { + if id & (UInt64(0xf) << UInt64(60 - i * 4)) == 0 { + pad += "0" + } else { + break + } + } + // + if id == 0 { + return "X" + } + // strip trailing zeros + var id2 = id + while id2 & 0xf == 0 { + id2 >>= 4 + } + // hex encoded string + let s = String(format: "%llx", id2) + // return hex encoded string + return pad + s + } + + // MARK: tests + + // isValid reports whether ci represents a valid cell. + func isValid() -> Bool { + return face() < CellId.numFaces && (lsb() & 0x1555555555555555 != 0) + } + + // isLeaf returns whether this cell ID is at the deepest level; + // that is, the level at which the cells are smallest. + func isLeaf() -> Bool { + return id & 1 != 0 + } + + // isFace returns whether this is a top-level (face) cell. + func isFace() -> Bool { + return id & (CellId.lsb(0) - 1) == 0 + } + + // MARK: computed members + + // Face returns the cube face for this cell ID, in the range [0,5]. + func face() -> Int { + return Int(id >> UInt64(CellId.posBits)) + } + + // Pos returns the position along the Hilbert curve of this cell ID, in the range [0,2^posBits-1]. + func pos() -> UInt64 { + return id & (UInt64.max >> UInt64(CellId.faceBits)) + } + + // Level returns the subdivision level of this cell ID, in the range [0, maxLevel]. + func level() -> Int { + // Fast path for leaf cells. + if isLeaf() { + return CellId.maxLevel + } + var x = UInt32(id & 0xffffffff) + var level = -1 + if x != 0 { + level += 16 + } else { + x = UInt32(id >> 32) + } + // Only need to look at even-numbered bits for valid cell IDs. + x &= -x // remove all but the LSB. + if x&0x00005555 != 0 { + level += 8 + } + if x&0x00550055 != 0 { + level += 4 + } + if x&0x05050505 != 0 { + level += 2 + } + if x&0x11111111 != 0 { + level += 1 + } + return level + } + + // lsb returns the least significant bit that is set. + func lsb() -> UInt64 { + return id & -id + } + + // lsbForLevel returns the lowest-numbered bit that is on for cells at the given level. + static func lsb(_ level: Int) -> UInt64 { + return 1 << UInt64(2 * (CellId.maxLevel - level)) + } + + // MARK: edge and vertex neighbors + + static func sizeIJ(_ level: Int) -> Int { + return 1 << Int(CellId.maxLevel - level) + } + + // EdgeNeighbors returns the four cells that are adjacent across the cell's four edges. + // Edges 0, 1, 2, 3 are in the down, right, up, left directions in the face space. + // All neighbors are guaranteed to be distinct. + func edgeNeighbors() -> [CellId] { + let level = self.level() + let size = CellId.sizeIJ(level) + let (f, i, j, _) = faceIJOrientation() + return [ + CellId(face: f, i: i, j: j-size, wrapped: true).parent(level), + CellId(face: f, i: i+size, j: j, wrapped: true).parent(level), + CellId(face: f, i: i, j: j+size, wrapped: true).parent(level), + CellId(face: f, i: i-size, j: j, wrapped: true).parent(level)] + } + + // VertexNeighbors returns the neighboring cellIds with vertex closest to this cell at the given level. + // (Normally there are four neighbors, but the closest vertex may only have three neighbors if it is one of + // the 8 cube vertices.) + func vertexNeighbors(_ level: Int) -> [CellId] { + let halfSize = CellId.sizeIJ(level + 1) + let size = halfSize << 1 + let (f, i, j, _) = faceIJOrientation() + // + var isame: Bool + var jsame: Bool + var ioffset: Int + var joffset: Int + if i & halfSize != 0 { + ioffset = size + isame = (i + size) < CellId.maxSize + } else { + ioffset = -size + isame = (i - size) >= 0 + } + if j & halfSize != 0 { + joffset = size + jsame = (j + size) < CellId.maxSize + } else { + joffset = -size + jsame = (j - size) >= 0 + } + // + var results = [ + parent(level), + CellId(face: f, i: i+ioffset, j: j, sameFace: isame).parent(level), + CellId(face: f, i: i, j: j+joffset, sameFace: jsame).parent(level)] + // + if isame || jsame { + results.append(CellId(face: f, i: i+ioffset, j: j+joffset, sameFace: isame && jsame).parent(level)) + } + // + return results + } + + // MARK: contain / intersect + + // RangeMin returns the minimum CellId that is contained within this cell. + func rangeMin() -> CellId { + return CellId(id: id - (lsb() - 1)) + } + + // RangeMax returns the maximum CellId that is contained within this cell. + func rangeMax() -> CellId { + return CellId(id: id + (lsb() - 1)) + } + + // Contains returns true iff the CellId contains oci. + func contains(_ cellId: CellId) -> Bool { + return rangeMin().id <= cellId.id && cellId.id <= rangeMax().id + } + + // Intersects returns true iff the CellId intersects oci. + func intersects(_ cellId: CellId) -> Bool { + return cellId.rangeMin().id <= rangeMax().id && cellId.rangeMax().id >= rangeMin().id + } + + // MARK: conversions + + // Point returns the center of the s2 cell on the sphere as a Point. + func point() -> S2Point { + return S2Point(raw: rawPoint()) + } + + // LatLng returns the center of the s2 cell on the sphere as a LatLng. + func latLng() -> LatLng { + let point = self.point() + return LatLng(point: point) + } + + // MARK: cell hierarchy + + // Parent returns the cell at the given level, which must be no greater than the current level. + func parent(_ level: Int) -> CellId { + let lsb = CellId.lsb(level) + return CellId(id: (id & -lsb) | lsb) + } + + // immediateParent is cheaper than Parent, but assumes !isFace(). + func immediateParent() -> CellId { + assert(!isFace()) + let nlsb = lsb() << 2 + return CellId(id: (id & -nlsb) | nlsb) + } + + // ChildPosition returns the child position (0..3) of this cell's + // ancestor at the given level, relative to its parent. The argument + // should be in the range 1..kMaxLevel. For example, + // ChildPosition(1) returns the position of this cell's level-1 + // ancestor within its top-level face cell. + func childPosition(_ level: Int) -> Int { + return Int(id >> UInt64(2 * (CellId.maxLevel-level) + 1)) & 3 + } + + // MARK: enumerate + + // Children returns the four immediate children of this cell. + // If ci is a leaf cell, it returns four identical cells that are not the children. + func children() -> [CellId] { + // var ch = [CellId]() + // var lsb = self.lsb() + // ch.append(CellId(id: id - lsb + lsb>>2)) + // lsb >>= 1 + // ch.append(CellId(id: ch[0].id + lsb)) + // ch.append(CellId(id: ch[1].id + lsb)) + // ch.append(CellId(id: ch[2].id + lsb)) + // return ch + let lsb = self.lsb() + let lsb1 = lsb >> 1 + let id0 = id &- lsb &+ lsb >> 2 + let id1 = id0 + lsb1 + let id2 = id1 + lsb1 + let id3 = id2 + lsb1 + return [CellId(id: id0), CellId(id: id1), CellId(id: id2), CellId(id: id3)] + } + +// func generate(level: Int) -> AnyGenerator { +// let levelLsb = CellId.lsbForLevel(level) +// var cellId = CellId(id: id - lsb() + levelLsb) +// let end = CellId(id: id + lsb() + levelLsb) +// return AnyGenerator { +// if cellId == end { return nil } +// let n = cellId +// let id = cellId.id +// cellId = CellId(id: id + (id & -id)<<1) +// return n +// } +// } +// +// func generate() -> AnyGenerator { +// let ol = lsb() +// var cellId = CellId(id: id - ol + ol>>2) +// let end = CellId(id: id + ol + ol>>2) +// return AnyGenerator { +// if cellId == end { return nil } +// let n = cellId +// let id = cellId.id +// cellId = CellId(id: id + (id & -id)<<1) +// return n +// } +// } + + // ChildBegin returns the first child in a traversal of the children of this cell, in Hilbert curve order. + // + // for ci := c.ChildBegin(); ci != c.ChildEnd(); ci = Next() { + // ... + // } + func childBegin() -> CellId { + let ol = lsb() + return CellId(id: id - ol + ol>>2) + } + + // ChildEnd returns the first cell after a traversal of the children of this cell in Hilbert curve order. + // The returned cell may be invalid. + func childEnd() -> CellId { + let ol = lsb() + return CellId(id: id + ol + ol>>2) + } + + // ChildBeginAtLevel returns the first cell in a traversal of children a given level deeper than this cell, in + // Hilbert curve order. The given level must be no smaller than the cell's level. + func childBegin(_ level: Int) -> CellId { + return CellId(id: id - lsb() + CellId.lsb(level)) + } + + // ChildEndAtLevel returns the first cell after the last child in a traversal of children a given level deeper + // than this cell, in Hilbert curve order. + // The given level must be no smaller than the cell's level. + // The returned cell may be invalid. + func childEnd(_ level: Int) -> CellId { + return CellId(id: id + lsb() + CellId.lsb(level)) + } + + // Next returns the next cell along the Hilbert curve. + // This is expected to be used with ChildStart and ChildEnd. + func next() -> CellId { + return CellId(id: id + lsb()<<1) + } + + // Prev returns the previous cell along the Hilbert curve. + func prev() -> CellId { + return CellId(id: id - lsb()<<1) + } + + func children(level: Int?) -> AnyIterator { + let lsbParent = lsb() + let lsbChild = (level != nil) ? CellId.lsb(level!) : lsbParent >> 2 + var current = CellId(id: id - lsbParent + lsbChild) + let end = CellId(id: id + lsbParent + lsbChild) + return AnyIterator { + if current == end { return nil } + let id = current.id + let n = current + current = CellId(id: id + (id & -id) << 1) + return n + } + } + +// func children(level: Int) -> CellIdSequence { +// return CellIdSequence(parent: self, level: level) +// } + + // rawPoint returns an unnormalized r3 vector from the origin through the center + // of the s2 cell on the sphere. + func rawPoint() -> R3Vector { + let (face, si, ti) = faceSiTi() + let u = S2Cube.stToUV((0.5 / Double(CellId.maxSize)) * Double(si)) + let v = S2Cube.stToUV((0.5 / Double(CellId.maxSize)) * Double(ti)) + return S2Cube(face: face, u: u, v: v).vector() + } + + // faceSiTi returns the Face/Si/Ti coordinates of the center of the cell. + func faceSiTi() -> (Int, Int, Int) { + let (face, i, j, _) = faceIJOrientation() + // patching with delta values + var delta = 0 + if isLeaf() { + delta = 1 + } else { + if (i^(Int(id>>2)))&1 != 0 { + delta = 2 + } + } + // + return (face, 2*i + delta, 2*j + delta) + } + + // faceIJOrientation uses the global lookupIJ table to unfiddle the bits of ci. + func faceIJOrientation() -> (Int, Int, Int, Int) { + let f = face() + let (i, j, orientation) = CellId.ijOrientation(face: f, id: id, lsb: lsb()) + return (f, i, j, orientation) + } + + // faceIJOrientation uses the global lookupIJ table to unfiddle the bits of ci. + static func ijOrientation(face: Int, id: UInt64, lsb: UInt64) -> (Int, Int, Int) { + var i = 0 + var j = 0 + var bits = face & CellId.swapMask + var nbits = CellId.maxLevel - 7 * CellId.lookupBits // first iteration + for k_ in 0...7 { + let k = 7 - k_ + bits += (Int(id >> UInt64(k * 2 * CellId.lookupBits + 1)) & ((1 << (2 * nbits)) - 1)) << 2 + bits = CellId.lookupIJ[bits] + i += (bits >> (CellId.lookupBits + 2)) << (k * CellId.lookupBits) + j += ((bits >> 2) & ((1 << CellId.lookupBits) - 1)) << (k * CellId.lookupBits) + bits &= (CellId.swapMask | CellId.invertMask) + nbits = CellId.lookupBits // following iterations + } + if lsb & 0x1111111111111110 != 0 { + bits ^= CellId.swapMask + } + return (i, j, bits) + } + + // cellIdFromFaceIJ returns a leaf cell given its cube face (range 0..5) and IJ coordinates. + init(face: Int, i: Int, j: Int) { + // Note that this value gets shifted one bit to the left at the end + // of the function. + var n = face << (CellId.posBits - 1) + // Alternating faces have opposite Hilbert curve orientations; this + // is necessary in order for all faces to have a right-handed + // coordinate system. + var bits = face & CellId.swapMask + // Each iteration maps 4 bits of "i" and "j" into 8 bits of the Hilbert + // curve position. The lookup table transforms a 10-bit key of the form + // "iiiijjjjoo" to a 10-bit value of the form "ppppppppoo", where the + // letters [ijpo] denote bits of "i", "j", Hilbert curve position, and + // Hilbert curve orientation respectively. + for k_ in 0...7 { + let k = 7 - k_ + let mask = (1 << CellId.lookupBits) - 1 + bits += ((i >> (k * CellId.lookupBits)) & mask) << (CellId.lookupBits + 2) + bits += ((j >> (k * CellId.lookupBits)) & mask) << 2 + bits = CellId.lookupPos[bits] + n |= (bits >> 2) << (k * 2 * CellId.lookupBits) + bits &= CellId.swapMask | CellId.invertMask + } + self.init(id: UInt64(n) * 2 + 1) + } + + // cellIdFromFaceIJ returns a leaf cell given its cube face (range 0..5) and IJ coordinates. + static func idFrom(_ face: Int, i: Int, j: Int) -> UInt64 { + // Note that this value gets shifted one bit to the left at the end + // of the function. + var n = face << (CellId.posBits - 1) + // Alternating faces have opposite Hilbert curve orientations; this + // is necessary in order for all faces to have a right-handed + // coordinate system. + var bits = face & CellId.swapMask + // Each iteration maps 4 bits of "i" and "j" into 8 bits of the Hilbert + // curve position. The lookup table transforms a 10-bit key of the form + // "iiiijjjjoo" to a 10-bit value of the form "ppppppppoo", where the + // letters [ijpo] denote bits of "i", "j", Hilbert curve position, and + // Hilbert curve orientation respectively. + for k_ in 0...7 { + let k = 7 - k_ + let mask = (1 << CellId.lookupBits) - 1 + bits += ((i >> (k * CellId.lookupBits)) & mask) << (CellId.lookupBits + 2) + bits += ((j >> (k * CellId.lookupBits)) & mask) << 2 + bits = CellId.lookupPos[bits] + n |= (bits >> 2) << (k * 2 * CellId.lookupBits) + bits &= CellId.swapMask | CellId.invertMask + } + return UInt64(n) * 2 + 1 + } + + init(face: Int, i: Int, j: Int, wrapped: Bool) { + // Convert i and j to the coordinates of a leaf cell just beyond the + // boundary of this face. This prevents 32-bit overflow in the case + // of finding the neighbors of a face cell. + let i_ = CellId.clamp(i, min: -1, max: CellId.maxSize) + let j_ = CellId.clamp(j, min: -1, max: CellId.maxSize) + // We want to wrap these coordinates onto the appropriate adjacent face. + // The easiest way to do this is to convert the (i,j) coordinates to (x,y,z) + // (which yields a point outside the normal face boundary), and then call + // xyzToFaceUV to project back onto the correct face. + // + // The code below converts (i,j) to (si,ti), and then (si,ti) to (u,v) using + // the linear projection (u=2*s-1 and v=2*t-1). (The code further below + // converts back using the inverse projection, s=0.5*(u+1) and t=0.5*(v+1). + // Any projection would work here, so we use the simplest.) We also clamp + // the (u,v) coordinates so that the point is barely outside the + // [-1,1]x[-1,1] face rectangle, since otherwise the reprojection step + // (which divides by the new z coordinate) might change the other + // coordinates enough so that we end up in the wrong leaf cell. + let scale = 1.0 / Double(CellId.maxSize) + let limit = nextafter(1.0, 2.0) + let u = max(-limit, min(limit, scale * Double((i_ << 1) + 1 - CellId.maxSize))) + let v = max(-limit, min(limit, scale * Double((j_ << 1) + 1 - CellId.maxSize))) + // Find the leaf cell coordinates on the adjacent face, and convert + // them to a cell id at the appropriate level. + let raw = S2Cube(face: face, u: u, v: v).vector() + + let cube = S2Cube(point: S2Point(raw: raw)) + let i__ = CellId.stToIJ(0.5 * (cube.u+1)) + let j__ = CellId.stToIJ(0.5 * (cube.v+1)) + // + self.init(face: cube.face, i: i__, j: j__) + } + + init(face: Int, i: Int, j: Int, sameFace: Bool) { + if sameFace { + self.init(face: face, i: i, j: j) + } else { + self.init(face: face, i: i, j: j, wrapped: true) + } + } + + // clamp returns number closest to x within the range min..max. + static func clamp(_ x: Int, min: Int, max: Int) -> Int { + if x < min { + return min + } + if x > max { + return max + } + return x + } + + // ijToSTMin converts the i- or j-index of a leaf cell to the minimum corresponding + // s- or t-value contained by that cell. The argument must be in the range + // [0..2**30], i.e. up to one position beyond the normal range of valid leaf + // cell indices. + static func ijToSTMin(_ i: Int) -> Double { + return Double(i) / Double(CellId.maxSize) + } + + // stToIJ converts value in ST coordinates to a value in IJ coordinates. + static func stToIJ(_ s: Double) -> Int { + let ij = Int(floor(Double(CellId.maxSize) * s)) + return CellId.clamp(ij, min: 0, max: CellId.maxSize - 1) + } + + // cellIdFromPoint returns a leaf cell containing point p. Usually there is + // exactly one such cell, but for points along the edge of a cell, any + // adjacent cell may be (deterministically) chosen. This is because + // s2.CellIds are considered to be closed sets. The returned cell will + // always contain the given point, i.e. + // + // CellFromPoint(p).ContainsPoint(p) + // + // is always true. + init(point: S2Point) { + let cube = S2Cube(point: point) + let i = CellId.stToIJ(S2Cube.uvToST(cube.u)) + let j = CellId.stToIJ(S2Cube.uvToST(cube.v)) + self.init(face: cube.face, i: i, j: j) + } + + // ijLevelToBoundUV returns the bounds in (u,v)-space for the cell at the given + // level containing the leaf cell with the given (i,j)-coordinates. + static func ijLevelToBoundUV(i: Int, j: Int, level: Int) -> R2Rect { + let cellSize = sizeIJ(level) + let xLo = i & -cellSize + let yLo = j & -cellSize + let x = R1Interval(lo: S2Cube.stToUV(CellId.ijToSTMin(xLo)), hi: S2Cube.stToUV(CellId.ijToSTMin(xLo + cellSize))) + let y = R1Interval(lo: S2Cube.stToUV(CellId.ijToSTMin(yLo)), hi: S2Cube.stToUV(CellId.ijToSTMin(yLo + cellSize))) + return R2Rect(x: x, y: y) + } + + // MARK: Hilbert curve + + static let posToIJ = [ + [0, 1, 3, 2], // canonical order: (0,0), (0,1), (1,1), (1,0) + [0, 2, 3, 1], // axes swapped: (0,0), (1,0), (1,1), (0,1) + [3, 2, 0, 1], // bits inverted: (1,1), (1,0), (0,0), (0,1) + [3, 1, 0, 2]] // swapped & inverted: (1,1), (0,1), (0,0), (1,0) + static let posToOrientation = [CellId.swapMask, 0, 0, invertMask | swapMask] + static var lookupIJ = [Int](repeating: 0, count: 1 << (2*CellId.lookupBits + 2)) + static var lookupPos = [Int](repeating: 0, count: 1 << (2*CellId.lookupBits + 2)) + + // TODO call this once + static func setup() { + if lookupIJ[0] != 0 { + return + } + initLookupCell(level: 0, i: 0, j: 0, origOrientation: 0, pos: 0, orientation: 0) + initLookupCell(level: 0, i: 0, j: 0, origOrientation: swapMask, pos: 0, orientation: swapMask) + initLookupCell(level: 0, i: 0, j: 0, origOrientation: invertMask, pos: 0, orientation: invertMask) + initLookupCell(level: 0, i: 0, j: 0, origOrientation: swapMask|invertMask, pos: 0, orientation: swapMask|invertMask) + } + + // initLookupCell initializes the lookupIJ table at init time. + static func initLookupCell( + level: Int, i: Int, j: Int, origOrientation: Int, pos: Int, orientation: Int) { + if level == lookupBits { + let ij = (i << lookupBits) + j + lookupPos[(ij<<2)+origOrientation] = (pos << 2) + orientation + lookupIJ[(pos<<2)+origOrientation] = (ij << 2) + orientation + return + } + let level = level + 1 + let i = i << 1 + let j = j << 1 + let pos = pos << 2 + let r = posToIJ[orientation] + initLookupCell(level: level, i: i+(r[0]>>1), j: j+(r[0]&1), origOrientation: origOrientation, pos: pos, orientation: orientation^posToOrientation[0]) + initLookupCell(level: level, i: i+(r[1]>>1), j: j+(r[1]&1), origOrientation: origOrientation, pos: pos+1, orientation: orientation^posToOrientation[1]) + initLookupCell(level: level, i: i+(r[2]>>1), j: j+(r[2]&1), origOrientation: origOrientation, pos: pos+2, orientation: orientation^posToOrientation[2]) + initLookupCell(level: level, i: i+(r[3]>>1), j: j+(r[3]&1), origOrientation: origOrientation, pos: pos+3, orientation: orientation^posToOrientation[3]) + } + + // CommonAncestorLevel returns the level of the common ancestor of the two S2 CellIds. + func commonAncestorLevel(_ cellId: CellId) -> Int? { + var bits = UInt64(id ^ cellId.id) + if bits < lsb() { + bits = lsb() + } + if bits < cellId.lsb() { + bits = cellId.lsb() + } + let msbPos = CellId.findMSBSetNonZero64(bits) + if msbPos > 60 { + return nil + } + return (60 - msbPos) >> 1 + } + + // findMSBSetNonZero64 returns the index (between 0 and 63) of the most + // significant set bit. + static func findMSBSetNonZero64(_ bits: UInt64) -> Int { + let val: [UInt64] = [0x2, 0xC, 0xF0, 0xFF00, 0xFFFF0000, 0xFFFFFFFF00000000] + let shift: [UInt64] = [1, 2, 4, 8, 16, 32] + var msbPos = UInt64(0) + var bits_ = bits + for i_ in 0...5 { + let i = 5 - i_ + if bits_ & val[i] != 0 { + bits_ >>= shift[i] + msbPos |= shift[i] + } + } + return Int(msbPos) + } + + // Advance advances or retreats the indicated number of steps along the + // Hilbert curve at the current level, and returns the new position. The + // position is never advanced past End() or before Begin(). + func advance(_ steps: Int64) -> CellId { + if steps == 0 { + return self + } + var steps_ = steps + // We clamp the number of steps if necessary to ensure that we do not + // advance past the End() or before the Begin() of this level. Note that + // minSteps and maxSteps always fit in a signed 64-bit integer. + let stepShift = UInt64(2 * (CellId.maxLevel - level()) + 1) + if steps_ < 0 { + let minSteps = -Int64(id >> stepShift) + if steps_ < minSteps { + steps_ = minSteps + } + } else { + let maxSteps = Int64((CellId.wrapOffset + lsb() - id) >> stepShift) + if steps_ > maxSteps { + steps_ = maxSteps + } + } + // + let s = UInt64(bitPattern: steps_ << Int64(stepShift)) + return CellId(id: id &+ s) + } + +} diff --git a/Sphere2Go/S2CellUnion.swift b/Sphere2Go/S2CellUnion.swift new file mode 100644 index 0000000..2ec9281 --- /dev/null +++ b/Sphere2Go/S2CellUnion.swift @@ -0,0 +1,229 @@ +// +// S2CellUnion.swift +// Sphere2 +// + +import Foundation + +// package s2 +// import sort + +// A CellUnion is a collection of CellIDs. +// +// It is normalized if it is sorted and does not contain redundancy. +// Specifically, it may not contain the same CellId twice, nor a CellId that is contained by another, +// nor the four sibling CellIds that are children of a single higher level CellId. +// Differences from C++, almost everything. +class CellUnion: S2Region, Equatable { + + // internal collection + var cellIds: [CellId] + + // MARK: inits + + init(cellIds: [CellId]) { + self.cellIds = cellIds + } + + init(ids: [UInt64]) { + cellIds = ids.map { CellId(id: $0) } + } + + // MARK: protocols + + static func ==(lhs: CellUnion, rhs: CellUnion) -> Bool { + return lhs.cellIds == rhs.cellIds + } + + subscript(i: Int) -> CellId { + get { + return cellIds[i] + } + set(newValue) { + cellIds[i] = newValue + } + } + + // MARK: arithmetic + + func add(_ cellId: CellId) { + cellIds.append(cellId) + } + + func truncate(_ length: Int) -> CellUnion { + let s = Array(cellIds[0.. 0 && output[output.count - 1].contains(ci) { + continue + } + // Discard any previously accepted cells contained by this one. + // This could be any contiguous trailing subsequence, but it can't be + // a discontiguous subsequence because of the containment property of + // sorted S2 cells mentioned above. + var j = output.count - 1 // last index to keep + while j >= 0 { + if !ci.contains(output[j]) { + break + } + j -= 1 + } + output = Array(output.prefix(j+1)) + // See if the last three cells plus this one can be collapsed. + // We loop because collapsing three accepted cells and adding a higher level cell + // could cascade into previously accepted cells. + var ci_ = ci + while output.count >= 3 { + // last three + let fin = Array(output.suffix(3)) + // fast XOR test; a necessary but not sufficient condition + if fin[0].id^fin[1].id^fin[2].id^ci_.id != 0 { + break + } + // more expensive test; exact. + // Compute the two bit mask for the encoded child position, + // then see if they all agree. + var mask = ci_.lsb() << 1 + mask = ~(mask + mask<<1) + let should = ci_.id & mask + if fin[0].id & mask != should || fin[1].id & mask != should || fin[2].id & mask != should || ci_.isFace() { + break + } + output = Array(output.prefix(output.count-3)) + ci_ = ci_.immediateParent() // checked !ci.isFace above + } + output.append(ci_) + } + cellIds = output + } + +// func less(i: Int, j: Int) -> Bool { +// return cellIds[i].id < cellIds[j].id +// } +// +// func swap(i: Int, j: Int) { +// let tmp = cellIds[i] +// cellIds[i] = cellIds[j] +// cellIds[j] = tmp +// } + + // Denormalize replaces this CellUnion with an expanded version of the + // CellUnion where any cell whose level is less than minLevel or where + // (level - minLevel) is not a multiple of levelMod is replaced by its + // children, until either both of these conditions are satisfied or the + // maximum level is reached. + func denormalize(minLevel: Int, levelMod: Int) { + var denorm = [CellId]() + for id in cellIds { + let level = id.level() + var newLevel = level + if newLevel < minLevel { + newLevel = minLevel + } + if levelMod > 1 { + newLevel += (CellId.maxLevel - (newLevel - minLevel)) % levelMod + if newLevel > CellId.maxLevel { + newLevel = CellId.maxLevel + } + } + if newLevel == level { + denorm.append(id) + } else { + let end = id.childEnd(newLevel) + var ci = id.childBegin(newLevel) + while ci.id != end.id { + denorm.append(ci) + ci = ci.next() + } + } + } + cellIds = denorm + } + + // MARK: computed members + + var count: Int { + return cellIds.count + } + + // RectBound returns a Rect that bounds this entity. + func rectBound() -> S2Rect { + var bound = S2Rect.empty + for cellId in cellIds { + let c = Cell(id: cellId) + bound = bound.union(c.rectBound()) + } + return bound + } + + func capBound() -> S2Cap { + return rectBound().capBound() + } + + // MARK: tests + + // IntersectsCellID reports whether this cell union intersects the given cell ID. + // + // This method assumes that the CellUnion has been normalized. + func intersects(_ cellId: CellId) -> Bool { + // Find index of array item that occurs directly after our probe cell: + var i = cellIds.count + for (index, ci) in cellIds.enumerated() { + if cellId.id < ci.id { + i = index + break + } + } + + if i != cellIds.count && cellIds[i].rangeMin().id <= cellId.rangeMax().id { + return true + } + return i != 0 && cellIds[i-1].rangeMax().id >= cellId.rangeMin().id + } + + // ContainsCellID reports whether the cell union contains the given cell ID. + // Containment is defined with respect to regions, e.g. a cell contains its 4 children. + // + // This method assumes that the CellUnion has been normalized. + func contains(_ cellId: CellId) -> Bool { + // Find index of array item that occurs directly after our probe cell: + var i = cellIds.count + for (index, ci) in cellIds.enumerated() { + if cellId.id < ci.id { + i = index + break + } + } + + if i != cellIds.count && cellIds[i].rangeMin().id <= cellId.id { + return true + } + return i != 0 && cellIds[i-1].rangeMax().id >= cellId.id + } + + // ContainsCell reports whether this cell union contains the given cell. + func contains(_ cell: Cell) -> Bool { + return contains(cell.id) + } + + // ContainsCell reports whether this cell union contains the given cell. + func intersects(_ cell: Cell) -> Bool { + // TODO implement + return false + } + +} diff --git a/Sphere2Go/S2Cube.swift b/Sphere2Go/S2Cube.swift new file mode 100644 index 0000000..77f5929 --- /dev/null +++ b/Sphere2Go/S2Cube.swift @@ -0,0 +1,242 @@ +// +// S2Cube.swift +// Sphere2 +// + +import Foundation + +struct S2Cube { + + // projection of a point on the sphere onto a bounding cube. + // faces 0,1,3,4 are around the equator, 2,5 are at the poles + let face: Int + // u and v are the coordinates on the respective cube face, and should be in [-1, 1]x[-1, 1] + let u: Double + let v: Double + + // MARK: inits/ factory + + init(face: Int, u: Double, v: Double) { + self.face = face + self.u = u + self.v = v + } + + init(point: S2Point) { + let face = S2Cube.face(point: point) + let (u, v) = S2Cube.validFaceXYZToUV(face: face, point: point) + self.init(face: face, u: u, v: v) + } + + init?(point: S2Point, face: Int) { + let toCheck: Double + switch face { + case 0: toCheck = point.x + case 1: toCheck = point.y + case 2: toCheck = point.z + case 3: toCheck = -point.x + case 4: toCheck = -point.y + case 5: toCheck = -point.z + default: return nil + } + if toCheck <= 0 { + return nil + } + let (u, v) = S2Cube.validFaceXYZToUV(face: face, point: point) + self.init(face: face, u: u, v: v) + } + + // MARK: non-linear projection + + // stToUV converts an s or t value to the corresponding u or v value. + // This is a non-linear transformation from [-1,1] to [-1,1] that + // attempts to make the cell sizes more uniform. + // This uses what the C++ version calls 'the quadratic transform'. + static func stToUV(_ s: Double) -> Double { + if s >= 0.5 { + return (1.0 / 3.0) * (4*s*s - 1) + } + return (1.0 / 3.0) * (1 - 4*(1-s)*(1-s)) + } + + // uvToST is the inverse of the stToUV transformation. Note that it + // is not always true that uvToST(stToUV(x)) == x due to numerical + // errors. + static func uvToST(_ u: Double) -> Double { + if u >= 0 { + return 0.5 * sqrt(1 + 3 * u) + } + return 1 - 0.5 * sqrt(1 - 3 * u) + } + + // MARK: face + + // face returns face ID from 0 to 5 containing the r. For points on the + // boundary between faces, the result is arbitrary but deterministic. + static func face(point r: S2Point) -> Int { + // find largest component + var id = 0 + var value = r.x + if fabs(r.y) > fabs(value) { + id = 1 + value = r.y + } + if fabs(r.z) > fabs(value) { + id = 2 + value = r.z + } + // negative means the opposite face + if value < 0.0 { + id += 3 + } + // + return id + } + + // validFaceXYZToUV given a valid face for the given point r (meaning that + // dot product of r with the face normal is positive), returns + // the corresponding u and v values, which may lie outside the range [-1,1]. + static func validFaceXYZToUV(face: Int, point r: S2Point) -> (Double, Double) { + switch face { + case 0: return (r.y / r.x, r.z / r.x) + case 1: return (-r.x / r.y, r.z / r.y) + case 2: return (-r.x / r.z, -r.y / r.z) + case 3: return (r.z / r.x, r.y / r.x) + case 4: return (r.z / r.y, -r.x / r.y) + default: return (-r.y / r.z, -r.x / r.z) + } + } + + // xyzToFaceUV converts a direction S2Point (not necessarily unit length) to + // (face, u, v) coordinates. + static func xyzToFaceUV(r: S2Point) -> (Int, Double, Double) { + let f = face(point: r) + let (u, v) = validFaceXYZToUV(face: f, point: r) + return (f, u, v) + } + + // faceXYZToUV returns the u and v values (which may lie outside the range + // [-1, 1]) if the dot product of the point p with the given face normal is positive. + static func faceXYZToUV(face: Int, p: S2Point) -> (Double, Double)? { + let toCheck: Double + switch face { + case 0: toCheck = p.x + case 1: toCheck = p.y + case 2: toCheck = p.z + case 3: toCheck = -p.x + case 4: toCheck = -p.y + case 5: toCheck = -p.z + default: return nil + } + if toCheck <= 0 { + return nil + } + return validFaceXYZToUV(face: face, point: p) + } + + // faceUVToXYZ turns face and UV coordinates into an unnormalized 3 R3Vector. + func vector() -> R3Vector { + switch face { + case 0: return R3Vector(x: 1, y: u, z: v) + case 1: return R3Vector(x: -u, y: 1, z: v ) + case 2: return R3Vector(x: -u, y: -v, z: 1) + case 3: return R3Vector(x: -1, y: -v, z: -u) + case 4: return R3Vector(x: v, y: -1, z: -u) + default: return R3Vector(x: v, y: u, z: -1) + } + } + + // faceUVToXYZ turns face and UV coordinates into an unnormalized 3 R3Vector. + static func faceUVToXYZ(face: Int, u: Double, v: Double) -> R3Vector { + switch face { + case 0: return R3Vector(x: 1, y: u, z: v) + case 1: return R3Vector(x: -u, y: 1, z: v ) + case 2: return R3Vector(x: -u, y: -v, z: 1) + case 3: return R3Vector(x: -1, y: -v, z: -u) + case 4: return R3Vector(x: v, y: -1, z: -u) + default: return R3Vector(x: v, y: u, z: -1) + } + } + + // faceXYZtoUVW transforms the given point P to the (u,v,w) coordinate frame of the given + // face where the w-axis represents the face normal. + static func faceXYZtoUVW(face: Int, point p: S2Point) -> S2Point { + // The result coordinates are simply the dot products of P with the (u,v,w) + // axes for the given face (see faceUVWAxes). + switch face { + case 0: return S2Point(x: p.y, y: p.z, z: p.x) + case 1: return S2Point(x: -p.x, y: p.z, z: p.y) + case 2: return S2Point(x: -p.x, y: -p.y, z: p.z) + case 3: return S2Point(x: -p.z, y: -p.y, z: -p.x) + case 4: return S2Point(x: -p.z, y: p.x, z: -p.y) + default: return S2Point(x: p.y, y: p.x, z: -p.z) + } + } + + // MARK: u and v normals + + // uNorm returns the right-handed normal (not necessarily unit length) for an + // edge in the direction of the positive v-axis at the given u-value on + // the given face. (This S2Point is perpendicular to the plane through + // the sphere origin that contains the given edge.) + static func uNorm(face: Int, u: Double, invert: Bool) -> S2Point { + let one = invert ? -1.0 : 1.0 + let u = invert ? -u : u + switch face { + case 0: return S2Point(x: u, y: -one, z: 0) + case 1: return S2Point(x: one, y: u, z: 0) + case 2: return S2Point(x: one, y: 0, z: u) + case 3: return S2Point(x: -u, y: 0, z: one) + case 4: return S2Point(x: 0, y: -u, z: one) + default: return S2Point(x: 0, y: -one, z: -u) + } + } + + // vNorm returns the right-handed normal (not necessarily unit length) for an + // edge in the direction of the positive u-axis at the given v-value on + // the given face. + static func vNorm(face: Int, v: Double, invert: Bool) -> S2Point { + let one = invert ? -1.0 : 1.0 + let v = invert ? -v : v + switch face { + case 0: return S2Point(x: -v, y: 0, z: one) + case 1: return S2Point(x: 0, y: -v, z: one) + case 2: return S2Point(x: 0, y: -one, z: -v) + case 3: return S2Point(x: v, y: -one, z: 0) + case 4: return S2Point(x: one, y: v, z: 0) + default: return S2Point(x: one, y: 0, z: v) + } + } + + // MARK: face axes + + // faceUVWAxes are the U, V, and W axes for each face. + static var faceUVWAxes = [ + [S2Point(x: 0, y: +1, z: 0), S2Point(x: 0, y: 0, z: +1), S2Point(x: +1, y: 0, z: 0)], + [S2Point(x: -1, y: 0, z: 0), S2Point(x: 0, y: 0, z: +1), S2Point(x: 0, y: +1, z: 0)], + [S2Point(x: -1, y: 0, z: 0), S2Point(x: 0, y: -1, z: 0), S2Point(x: 0, y: 0, z: +1)], + [S2Point(x: 0, y: 0, z: -1), S2Point(x: 0, y: -1, z: 0), S2Point(x: -1, y: 0, z: 0)], + [S2Point(x: 0, y: 0, z: -1), S2Point(x: +1, y: 0, z: 0), S2Point(x: 0, y: -1, z: 0)], + [S2Point(x: 0, y: +1, z: 0), S2Point(x: +1, y: 0, z: 0), S2Point(x: 0, y: 0, z: -1)]] + + // uvwAxis returns the given axis of the given face. + static func uvwAxis(face: Int, axis: Int) -> S2Point { + return faceUVWAxes[face][axis] + } + + // uAxis returns the u-axis for the given face. + static func uAxis(face: Int) -> S2Point { + return uvwAxis(face: face, axis: 0) + } + + // vAxis returns the v-axis for the given face. + static func vAxis(face: Int) -> S2Point { + return uvwAxis(face: face, axis: 1) + } + + // Return the unit-length normal for the given face. + static func unitNorm(face: Int) -> S2Point { + return uvwAxis(face: face, axis: 2) + } + +} diff --git a/Sphere2Go/S2EdgeUtility.swift b/Sphere2Go/S2EdgeUtility.swift new file mode 100644 index 0000000..3e8d83b --- /dev/null +++ b/Sphere2Go/S2EdgeUtility.swift @@ -0,0 +1,777 @@ +// +// S2EdgeUtility.swift +// Sphere2 +// + +import Foundation + + +// edgeClipErrorUVCoord is the maximum error in a u- or v-coordinate +// compared to the exact result, assuming that the points A and B are in +// the rectangle [-1,1]x[1,1] or slightly outside it (by 1e-10 or less). +let edgeClipErrorUVCoord = 2.25 * Cell.dblEpsilon + +// edgeClipErrorUVDist is the maximum distance from a clipped point to +// the corresponding exact result. It is equal to the error in a single +// coordinate because at most one coordinate is subject to error. +let edgeClipErrorUVDist = 2.25 * Cell.dblEpsilon + +// faceClipErrorRadians is the maximum angle between a returned vertex +// and the nearest point on the exact edge AB. It is equal to the +// maximum directional error in PointCross, plus the error when +// projecting points onto a cube face. +let faceClipErrorRadians = 3 * Cell.dblEpsilon + +// faceClipErrorDist is the same angle expressed as a maximum distance +// in (u,v)-space. In other words, a returned vertex is at most this far +// from the exact edge AB projected into (u,v)-space. +let faceClipErrorUVDist = 9 * Cell.dblEpsilon + +// faceClipErrorUVCoord is the maximum angle between a returned vertex +// and the nearest point on the exact edge AB expressed as the maximum error +// in an individual u- or v-coordinate. In other words, for each +// returned vertex there is a point on the exact edge AB whose u- and +// v-coordinates differ from the vertex by at most this amount. +let faceClipErrorUVCoord = 9.0 * (1.0 / sqrt(2.0)) * Cell.dblEpsilon + +// intersectsRectErrorUVDist is the maximum error when computing if a point +// intersects with a given Rect. If some point of AB is inside the +// rectangle by at least this distance, the result is guaranteed to be true; +// if all points of AB are outside the rectangle by at least this distance, +// the result is guaranteed to be false. This bound assumes that rect is +// a subset of the rectangle [-1,1]x[-1,1] or extends slightly outside it +// (e.g., by 1e-10 or less). +let intersectsRectErrorUVDist = 3 * sqrt(2.0) * Cell.dblEpsilon + +// intersectionError can be set somewhat arbitrarily, because the algorithm +// uses more precision if necessary in order to achieve the specified error. +// The only strict requirement is that intersectionError >= dblEpsilon +// radians. However, using a larger error tolerance makes the algorithm more +// efficient because it reduces the number of cases where exact arithmetic is +// needed. +let intersectionError = 4 * Cell.dblEpsilon + +// intersectionMergeRadius is used to ensure that intersection points that +// are supposed to be coincident are merged back together into a single +// vertex. This is required in order for various polygon operations (union, +// intersection, etc) to work correctly. It is twice the intersection error +// because two coincident intersection points might have errors in +// opposite directions. +let intersectionMergeRadius = 2 * intersectionError + +// SimpleCrossing reports whether edge AB crosses CD at a point that is interior +// to both edges. Properties: +// +// (1) SimpleCrossing(b,a,c,d) == SimpleCrossing(a,b,c,d) +// (2) SimpleCrossing(c,d,a,b) == SimpleCrossing(a,b,c,d) +func simpleCrossing(_ a: S2Point, b: S2Point, c: S2Point, d: S2Point) -> Bool { + // We compute the equivalent of Sign for triangles ACB, CBD, BDA, + // and DAC. All of these triangles need to have the same orientation + // (CW or CCW) for an intersection to exist. + + let ab = a.v.cross(b.v) + let acb = -(ab.dot(c.v)) + let bda = ab.dot(d.v) + if acb * bda <= 0 { + return false + } + + let cd = c.v.cross(d.v) + let cbd = -(cd.dot(b.v)) + let dac = cd.dot(a.v) + return (acb * cbd > 0) && (acb * dac > 0) +} + +// VertexCrossing reports whether two edges "cross" in such a way that point-in-polygon +// containment tests can be implemented by counting the number of edge crossings. +// +// Given two edges AB and CD where at least two vertices are identical +// (i.e. CrossingSign(a,b,c,d) == 0), the basic rule is that a "crossing" +// occurs if AB is encountered after CD during a CCW sweep around the shared +// vertex starting from a fixed reference point. +// +// Note that according to this rule, if AB crosses CD then in general CD +// does not cross AB. However, this leads to the correct result when +// counting polygon edge crossings. For example, suppose that A,B,C are +// three consecutive vertices of a CCW polygon. If we now consider the edge +// crossings of a segment BP as P sweeps around B, the crossing number +// changes parity exactly when BP crosses BA or BC. +// +// Useful properties of VertexCrossing (VC): +// +// (1) VC(a,a,c,d) == VC(a,b,c,c) == false +// (2) VC(a,b,a,b) == VC(a,b,b,a) == true +// (3) VC(a,b,c,d) == VC(a,b,d,c) == VC(b,a,c,d) == VC(b,a,d,c) +// (3) If exactly one of a,b equals one of c,d, then exactly one of +// VC(a,b,c,d) and VC(c,d,a,b) is true +// +// It is an error to call this method with 4 distinct vertices. +func vertexCrossing(_ a: S2Point, _ b: S2Point, _ c: S2Point, _ d: S2Point) -> Bool { + // If A == B or C == D there is no intersection. We need to check this + // case first in case 3 or more input points are identical. + if a.approxEquals(b) || c.approxEquals(d) { + return false + } + + // If any other pair of vertices is equal, there is a crossing if and only + // if OrderedCCW indicates that the edge AB is further CCW around the + // shared vertex O (either A or B) than the edge CD, starting from an + // arbitrary fixed reference point. + if a.approxEquals(d) { + return S2Point.orderedCCW(S2Point(raw: a.v.ortho()), c, b, a) + } else if b.approxEquals(c) { + return S2Point.orderedCCW(S2Point(raw: b.v.ortho()), d, a, b) + } else if a.approxEquals(c) { + return S2Point.orderedCCW(S2Point(raw: a.v.ortho()), d, b, a) + } else if b.approxEquals(d){ + return S2Point.orderedCCW(S2Point(raw: b.v.ortho()), c, a, b) + } + + return false +} + +// Interpolate returns the point X along the line segment AB whose distance from A +// is the given fraction "t" of the distance AB. Does NOT require that "t" be +// between 0 and 1. Note that all distances are measured on the surface of +// the sphere, so this is more complicated than just computing (1-t)*a + t*b +// and normalizing the result. +func interpolate(t: Double, a: S2Point, b: S2Point) -> S2Point { + if t == 0.0 { + return a + } + if t == 1.0 { + return b + } + let ab = a.angle(b) + return interpolateAtDistance(t * ab, a: a, b: b) +} + +// InterpolateAtDistance returns the point X along the line segment AB whose +// distance from A is the angle ax. +func interpolateAtDistance(_ ax: Double, a: S2Point, b: S2Point) -> S2Point { + let aRad = ax + + // Use PointCross to compute the tangent vector at A towards B. The + // result is always perpendicular to A, even if A=B or A=-B, but it is not + // necessarily unit length. (We effectively normalize it below.) + let normal = a.pointCross(b) + let tangent = normal.v.cross(a.v) + + // Now compute the appropriate linear combination of A and "tangent". With + // infinite precision the result would always be unit length, but we + // normalize it anyway to ensure that the error is within acceptable bounds. + // (Otherwise errors can build up when the result of one interpolation is + // fed into another interpolation.) + let v = a.v.mul(cos(aRad)).add(tangent.mul(sin(aRad) / tangent.norm())) + return S2Point(raw: v) +} + +// RectBounder is used to compute a bounding rectangle that contains all edges +// defined by a vertex chain (v0, v1, v2, ...). All vertices must be unit length. +// Note that the bounding rectangle of an edge can be larger than the bounding +// rectangle of its endpoints, e.g. consider an edge that passes through the North Pole. +// +// The bounds are calculated conservatively to account for numerical errors +// when points are converted to LatLngs. More precisely, this function +// guarantees the following: +// Let L be a closed edge chain (Loop) such that the interior of the loop does +// not contain either pole. Now if P is any point such that L.ContainsPoint(P), +// then RectBound(L).ContainsPoint(LatLngFromPoint(P)). +struct RectBounder { + + // The previous vertex in the chain. + var a: S2Point + // The previous vertex latitude longitude. + var aLL: LatLng + var bound: S2Rect + + // + init() { + a = S2Point.origin + aLL = LatLng(lat: 0.0, lng: 0.0) + bound = S2Rect.empty + } + + // AddPoint adds the given point to the chain. The Point must be unit length. + mutating func add(point b: S2Point) { + let bLL = LatLng(point: b) + + if bound.isEmpty() { + a = b + aLL = bLL + bound = bound.add(bLL) + return + } + + // 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 RobustSign + // 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. + let n = a.v.sub(b.v).cross(a.v.add(b.v)) // 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 add up to at most 1.16 * dblEpsilon, and it + // is desirable to have the total error be a multiple of dblEpsilon, we + // have chosen the maximum error threshold here to be 3.84 * dblEpsilon. + // It is possible to show that the error is less than this when + // + // n.Norm() >= 8 * sqrt(3) / (3.84 - 0.5 - sqrt(3)) * dblEpsilon + // = 1.91346e-15 (about 8.618 * dblEpsilon) + let nNorm = n.norm() + if nNorm < 1.91346e-15 { + // A and B are either nearly identical or nearly antipodal (to within + // 4.309 * dblEpsilon, or about 6 nanometers on the earth's surface). + if a.v.dot(b.v) < 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 = S2Rect.full + } else { + // The two points are nearly identical (to within 4.309 * dblEpsilon). + // In this case we can just use the bounding rectangle of the points, + // since after the expansion done by GetBound this Rect is + // guaranteed to include the (lat,lng) values of all points along AB. + bound = bound.union(S2Rect(latLng: aLL).add(bLL)) + } + a = b + aLL = bLL + return + } + + // Compute the longitude range spanned by AB. + var lngAB = S1Interval.empty.add(aLL.lng).add(bLL.lng) + if lngAB.length() >= .pi - 2 * Cell.dblEpsilon { + // The points lie on nearly opposite lines of longitude to within the + // maximum error of the calculation. The easiest solution is to assume + // that AB could go on either side of the pole. + lngAB = 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 latAB = R1Interval(point: aLL.lat).add(bLL.lat) + + // 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. + let m = n.cross(R3Vector(x: 0, y: 0, z: 1)) + let mA = m.dot(a.v) + let mB = m.dot(b.v) + + // We want to test the signs of "mA" and "mB", so we need to bound + // the error in these calculations. It is possible to show that the + // total error is bounded by + // + // (1 + sqrt(3)) * dblEpsilon * nNorm + 8 * sqrt(3) * (dblEpsilon**2) + // = 6.06638e-16 * nNorm + 6.83174e-31 + + let mError = 6.06638e-16 * nNorm + 6.83174e-31 + if mA * mB < 0 || fabs(mA) <= mError || fabs(mB) <= mError { + // 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 * dblEpsilon) + // - 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 * dblEpsilon + // individually, but it is possible to show by a more complex analysis + // that together they can add up to at most 1.16 * dblEpsilon, for a + // total error of 5 * dblEpsilon. + // + // We add 3 * dblEpsilon to the bound here, and GetBound() will pad + // the bound by another 2 * dblEpsilon. + let maxLat = min(atan2(sqrt(n.x*n.x + n.y*n.y), fabs(n.z)) + 3 * Cell.dblEpsilon, .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. + let latBudget = 2 * asin(0.5 * (a.v.sub(b.v)).norm() * sin(maxLat)) + let maxDelta = 0.5 * (latBudget-latAB.length()) + Cell.dblEpsilon + + // 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 mA <= mError && mB >= -mError { + latAB = R1Interval(lo: latAB.lo, hi: min(maxLat, latAB.hi+maxDelta)) + } + if mB <= mError && mA >= -mError { + latAB = R1Interval(lo: max(-maxLat, latAB.lo-maxDelta), hi:latAB.hi) + } + } + a = b + aLL = bLL + bound = bound.union(S2Rect(lat: latAB, lng: lngAB)) + } + + // RectBound returns the bounding rectangle of the edge chain that connects the + // vertices defined so far. This bound satisfies the guarantee made + // above, i.e. if the edge chain defines a Loop, then the bound contains + // the LatLng coordinates of all Points contained by the loop. + func rectBound() -> S2Rect { + return bound.expanded(LatLng(lat: 2 * Cell.dblEpsilon, lng: 0)).polarClosure() + } + +} + +// ExpandForSubregions expands a bounding Rect so that it is guaranteed to +// contain the bounds of any subregion whose bounds are computed using +// ComputeRectBound. For example, consider a loop L that defines a square. +// GetBound ensures that if a point P is contained by this square, then +// LatLngFromPoint(P) is contained by the bound. But now consider a diamond +// shaped loop S contained by L. It is possible that GetBound returns a +// *larger* bound for S than it does for L, due to rounding errors. This +// method expands the bound for L so that it is guaranteed to contain the +// bounds of any subregion S. +// +// More precisely, if L is a loop that does not contain either pole, and S +// is a loop such that L.Contains(S), then +// +// ExpandForSubregions(L.RectBound).Contains(S.RectBound). +// +extension S2Rect { + + func expandForSubregions() -> S2Rect { + // Empty bounds don't need expansion. + if isEmpty() { + return self + } + // First we need to check whether the bound B contains any nearly-antipodal + // points (to within 4.309 * dblEpsilon). If so then we need to return + // FullRect, since the subregion might have an edge between two + // such points, and AddPoint returns Full for such edges. Note that + // this can happen even if B is not Full for example, consider a loop + // that defines a 10km strip straddling the equator extending from + // longitudes -100 to +100 degrees. + // + // It is easy to check whether B contains any antipodal points, but checking + // for nearly-antipodal points is trickier. Essentially we consider the + // original bound B and its reflection through the origin B', and then test + // whether the minimum distance between B and B' is less than 4.309 * dblEpsilon. + // lngGap is a lower bound on the longitudinal distance between B and its + // reflection B'. (2.5 * dblEpsilon is the maximum combined error of the + // endpoint longitude calculations and the Length call.) + let lngGap = max(0, .pi - lng.length() - 2.5 * Cell.dblEpsilon) + // minAbsLat is the minimum distance from B to the equator (if zero or + // negative, then B straddles the equator). + let minAbsLat = max(lat.lo, -lat.hi) + // latGapSouth and latGapNorth measure the minimum distance from B to the + // south and north poles respectively. + let latGapSouth = .pi/2 + lat.lo + let latGapNorth = .pi/2 - lat.hi + if minAbsLat >= 0 { + // The bound B does not straddle the equator. In this case the minimum + // distance is between one endpoint of the latitude edge in B closest to + // the equator and the other endpoint of that edge in B'. The latitude + // distance between these two points is 2*minAbsLat, and the longitude + // distance is lngGap. We could compute the distance exactly using the + // Haversine formula, but then we would need to bound the errors in that + // calculation. Since we only need accuracy when the distance is very + // small (close to 4.309 * dblEpsilon), we substitute the Euclidean + // distance instead. This gives us a right triangle XYZ with two edges of + // length x = 2*minAbsLat and y ~= lngGap. The desired distance is the + // length of the third edge z, and we have + // + // z ~= sqrt(x^2 + y^2) >= (x + y) / sqrt(2) + // + // Therefore the region may contain nearly antipodal points only if + // + // 2*minAbsLat + lngGap < sqrt(2) * 4.309 * dblEpsilon + // ~= 1.354e-15 + // + // Note that because the given bound B is conservative, minAbsLat and + // lngGap are both lower bounds on their true values so we do not need + // to make any adjustments for their errors. + if 2 * minAbsLat + lngGap < 1.354e-15 { + return S2Rect.full + } + } else if lngGap >= .pi/2 { + // B spans at most Pi/2 in longitude. The minimum distance is always + // between one corner of B and the diagonally opposite corner of B'. We + // use the same distance approximation that we used above; in this case + // we have an obtuse triangle XYZ with two edges of length x = latGapSouth + // and y = latGapNorth, and angle Z >= Pi/2 between them. We then have + // + // z >= sqrt(x^2 + y^2) >= (x + y) / sqrt(2) + // + // Unlike the case above, latGapSouth and latGapNorth are not lower bounds + // (because of the extra addition operation, and because math.Pi/2 is not + // exactly equal to Pi/2); they can exceed their true values by up to + // 0.75 * dblEpsilon. Putting this all together, the region may contain + // nearly antipodal points only if + // + // latGapSouth + latGapNorth < (sqrt(2) * 4.309 + 1.5) * dblEpsilon + // ~= 1.687e-15 + if latGapSouth+latGapNorth < 1.687e-15 { + return S2Rect.full + } + } else { + // Otherwise we know that (1) the bound straddles the equator and (2) its + // width in longitude is at least Pi/2. In this case the minimum + // distance can occur either between a corner of B and the diagonally + // opposite corner of B' (as in the case above), or between a corner of B + // and the opposite longitudinal edge reflected in B'. It is sufficient + // to only consider the corner-edge case, since this distance is also a + // lower bound on the corner-corner distance when that case applies. + // + // Consider the spherical triangle XYZ where X is a corner of B with + // minimum absolute latitude, Y is the closest pole to X, and Z is the + // point closest to X on the opposite longitudinal edge of B'. This is a + // right triangle (Z = Pi/2), and from the spherical law of sines we have + // + // sin(z) / sin(Z) = sin(y) / sin(Y) + // sin(maxLatGap) / 1 = sin(dMin) / sin(lngGap) + // sin(dMin) = sin(maxLatGap) * sin(lngGap) + // + // where "maxLatGap" = max(latGapSouth, latGapNorth) and "dMin" is the + // desired minimum distance. Now using the facts that sin(t) >= (2/Pi)*t + // for 0 <= t <= Pi/2, that we only need an accurate approximation when + // at least one of "maxLatGap" or lngGap is extremely small (in which + // case sin(t) ~= t), and recalling that "maxLatGap" has an error of up + // to 0.75 * dblEpsilon, we want to test whether + // + // maxLatGap * lngGap < (4.309 + 0.75) * (Pi/2) * dblEpsilon + // ~= 1.765e-15 + if max(latGapSouth, latGapNorth) * lngGap < 1.765e-15 { + return S2Rect.full + } + } + // Next we need to check whether the subregion might contain any edges that + // span (math.Pi - 2 * dblEpsilon) radians or more in longitude, since AddPoint + // sets the longitude bound to Full in that case. This corresponds to + // testing whether (lngGap <= 0) in lngExpansion below. + // Otherwise, the maximum latitude error in AddPoint is 4.8 * dblEpsilon. + // In the worst case, the errors when computing the latitude bound for a + // subregion could go in the opposite direction as the errors when computing + // the bound for the original region, so we need to double this value. + // (More analysis shows that it's okay to round down to a multiple of + // dblEpsilon.) + // + // For longitude, we rely on the fact that atan2 is correctly rounded and + // therefore no additional bounds expansion is necessary. + let latExpansion = 9 * Cell.dblEpsilon + var lngExpansion = 0.0 + if lngGap <= 0 { + lngExpansion = .pi + } + return expanded(LatLng(lat: latExpansion, lng: lngExpansion)).polarClosure() + } +} + +// A Crossing indicates how edges cross. +enum Crossing: Int { + // Cross means the edges cross. + case cross = 0 + // MaybeCross means two vertices from different edges are the same. + case maybeCross = 1 + // DoNotCross means the edges do not cross. + case doNotCross = 2 +} + + +// EdgeCrosser allows edges to be efficiently tested for intersection with a +// given fixed edge AB. It is especially efficient when testing for +// intersection with an edge chain connecting vertices v0, v1, v2, ... +struct EdgeCrosser { + let a: S2Point + let b: S2Point + let aXb: S2Point + + // To reduce the number of calls to expensiveSign, we compute an + // outward-facing tangent at A and B if necessary. If the plane + // perpendicular to one of these tangents separates AB from CD (i.e., one + // edge on each side) then there is no intersection. + let aTangent: S2Point // Outward-facing tangent at A. + let bTangent: S2Point // Outward-facing tangent at B. + + // The fields below are updated for each vertex in the chain. + var c: S2Point // Previous vertex in the vertex chain. + var acb: Direction // The orientation of triangle ACB. + + // EdgeCrosser with the fixed edge AB + init(a: S2Point, b: S2Point, c: S2Point, acb: Direction) { + let norm = a.pointCross(b).v + self.a = a + self.b = b + aXb = a.pointCross(b) + aTangent = S2Point(raw: a.v.cross(norm)) + bTangent = S2Point(raw: norm.cross(b.v)) + // + self.c = c + self.acb = acb + } + +} + +extension EdgeCrosser { + + // NewChainEdgeCrosser is a convenience constructor that uses AB as the fixed edge, + // and C as the first vertex of the vertex chain (equivalent to calling RestartAt(c)). + // + // You don't need to use this or any of the chain functions unless you're trying to + // squeeze out every last drop of performance. Essentially all you are saving is a test + // whether the first vertex of the current edge is the same as the second vertex of the + // previous edge. + init(a: S2Point, b: S2Point, c: S2Point) { + let acb = -S2Point.triageSign(a, b, c) + self.init(a: a, b: b, c: c, acb: acb) + } + + // CrossingSign reports whether the edge AB intersects the edge CD. + // If any two vertices from different edges are the same, returns MaybeCross. + // If either edge is degenerate (A == B or C == D), returns DoNotCross or MaybeCross. + // + // Properties of CrossingSign: + // + // (1) CrossingSign(b,a,c,d) == CrossingSign(a,b,c,d) + // (2) CrossingSign(c,d,a,b) == CrossingSign(a,b,c,d) + // (3) CrossingSign(a,b,c,d) == MaybeCross if a==c, a==d, b==c, b==d + // (3) CrossingSign(a,b,c,d) == DoNotCross or MaybeCross if a==b or c==d + // + // Note that if you want to check an edge against a chain of other edges, + // it is slightly more efficient to use the single-argument version + // ChainCrossingSign below. + mutating func crossingSign(_ c: S2Point, d: S2Point) -> Crossing { + if c != self.c { + restartAt(c) + } + return chainCrossingSign(d) + } + + // EdgeOrVertexCrossing reports whether if CrossingSign(c, d) > 0, or AB and + // CD share a vertex and VertexCrossing(a, b, c, d) is true. + // + // This method extends the concept of a "crossing" to the case where AB + // and CD have a vertex in common. The two edges may or may not cross, + // according to the rules defined in VertexCrossing above. The rules + // are designed so that point containment tests can be implemented simply + // by counting edge crossings. Similarly, determining whether one edge + // chain crosses another edge chain can be implemented by counting. + mutating func edgeOrVertexCrossing(_ c: S2Point, d: S2Point) -> Bool { + if c != self.c { + restartAt(c) + } + return edgeOrVertexChainCrossing(d) + } + + // RestartAt sets the current point of the edge crosser to be c. + // Call this method when your chain 'jumps' to a new place. + // The argument must point to a value that persists until the next call. + mutating func restartAt(_ c: S2Point) { + self.c = c + acb = -S2Point.triageSign(a, b, self.c) + } + + // ChainCrossingSign is like CrossingSign, but uses the last vertex passed to one of + // the crossing methods (or RestartAt) as the first vertex of the current edge. + mutating func chainCrossingSign(_ d: S2Point) -> Crossing { + // For there to be an edge crossing, the triangles ACB, CBD, BDA, DAC must + // all be oriented the same way (CW or CCW). We keep the orientation of ACB + // as part of our state. When each new point D arrives, we compute the + // orientation of BDA and check whether it matches ACB. This checks whether + // the points C and D are on opposite sides of the great circle through AB. + + // Recall that triageSign is invariant with respect to rotating its + // arguments, i.e. ABD has the same orientation as BDA. + let bda = S2Point.triageSign(a, b, d) + if acb == -bda && bda != .indeterminate { + // The most common case -- triangles have opposite orientations. Save the + // current vertex D as the next vertex C, and also save the orientation of + // the new triangle ACB (which is opposite to the current triangle BDA). + c = d + acb = -bda + return .doNotCross + } + return crossingSign(d, bda: bda) + } + + // EdgeOrVertexChainCrossing is like EdgeOrVertexCrossing, but uses the last vertex + // passed to one of the crossing methods (or RestartAt) as the first vertex of the current edge. + mutating func edgeOrVertexChainCrossing(_ d: S2Point) -> Bool { + // We need to copy e.c since it is clobbered by ChainCrossingSign. + let c = self.c + switch chainCrossingSign(d) { + case .doNotCross: return false + case .cross: return true + default: break + } + return vertexCrossing(a, b, c, d) + } + + // crossingSign handle the slow path of CrossingSign. + mutating func crossingSign(_ d: S2Point, bda: Direction) -> Crossing { + // Compute the actual result, and then save the current vertex D as the next + // vertex C, and save the orientation of the next triangle ACB (which is + // opposite to the current triangle BDA). + defer { + c = d + acb = -bda + } + + // RobustSign is very expensive, so we avoid calling it if at all possible. + // First eliminate the cases where two vertices are equal. + if a == c || a == d || b == c || b == d { + return .maybeCross + } + + // At this point, a very common situation is that A,B,C,D are four points on + // a line such that AB does not overlap CD. (For example, this happens when + // a line or curve is sampled finely, or when geometry is constructed by + // computing the union of S2CellIds.) Most of the time, we can determine + // that AB and CD do not intersect using the two outward-facing + // tangents at A and B (parallel to AB) and testing whether AB and CD are on + // opposite sides of the plane perpendicular to one of these tangents. This + // is moderately expensive but still much cheaper than expensiveSign. + + // The error in RobustCrossProd is insignificant. The maximum error in + // the call to CrossProd (i.e., the maximum norm of the error vector) is + // (0.5 + 1/sqrt(3)) * dblEpsilon. The maximum error in each call to + // DotProd below is dblEpsilon. (There is also a small relative error + // term that is insignificant because we are comparing the result against a + // constant that is very close to zero.) + let maxError = (1.5 + 1/sqrt(3)) * Cell.dblEpsilon + if (c.v.dot(aTangent.v) > maxError && d.v.dot(aTangent.v) > maxError) || (c.v.dot(bTangent.v) > maxError && d.v.dot(bTangent.v) > maxError) { + return .doNotCross + } + + // Otherwise it's time to break out the big guns. + if acb == .indeterminate { + acb = -S2Point.expensiveSign(a, b, c) + } + var bda = bda + if bda == .indeterminate { + bda = S2Point.expensiveSign(a, b, d) + } + if bda != acb { + return .doNotCross + } + let cbd = -S2Point.robustSign(c, d, b) + if cbd != acb { + return .doNotCross + } + let dac = S2Point.robustSign(c, d, a) + if dac == acb { + return .cross + } + return .doNotCross + } + +} + +// pointUVW represents a Point in (u,v,w) coordinate space of a cube face. +struct PointUVW { + + // + let p: S2Point + + // intersectsFace reports whether a given directed line L intersects the cube face F. + // The line L is defined by its normal N in the (u,v,w) coordinates of F. + func intersectsFace() -> Bool { + // L intersects the [-1,1]x[-1,1] square in (u,v) if and only if the dot + // products of N with the four corner vertices (-1,-1,1), (1,-1,1), (1,1,1), + // and (-1,1,1) do not all have the same sign. This is true exactly when + // |Nu| + |Nv| >= |Nw|. The code below evaluates this expression exactly. + let u = fabs(p.x) + let v = fabs(p.y) + let w = fabs(p.z) + // We only need to consider the cases where u or v is the smallest value, + // since if w is the smallest then both expressions below will have a + // positive LHS and a negative RHS. + return (v >= w - u) && (u >= w - v) + } + + // intersectsOppositeEdges reports whether a directed line L intersects two + // opposite edges of a cube face F. This includs the case where L passes + // exactly through a corner vertex of F. The directed line L is defined + // by its normal N in the (u,v,w) coordinates of F. + func intersectsOppositeEdges() -> Bool { + // The line L intersects opposite edges of the [-1,1]x[-1,1] (u,v) square if + // and only exactly two of the corner vertices lie on each side of L. This + // is true exactly when ||Nu| - |Nv|| >= |Nw|. The code below evaluates this + // expression exactly. + let u = fabs(p.x) + let v = fabs(p.y) + let w = fabs(p.z) + // If w is the smallest, the following line returns an exact result. + if fabs(u - v) != w { + return fabs(u - v) >= w + } + // Otherwise u - v = w exactly, or w is not the smallest value. In either + // case the following returns the correct result. + if u >= v { + return u - w >= v + } + return v - w >= u + } + + // axis represents the possible results of exitAxis. + enum UVAxis: Int { + case u = 0 + case v = 1 + } + + // exitAxis reports which axis the directed line L exits the cube face F on. + // The directed line L is represented by its CCW normal N in the (u,v,w) coordinates + // of F. It returns axisU if L exits through the u=-1 or u=+1 edge, and axisV if L exits + // through the v=-1 or v=+1 edge. Either result is acceptable if L exits exactly + // through a corner vertex of the cube face. + func exitAxis() -> UVAxis { + if intersectsOppositeEdges() { + // The line passes through through opposite edges of the face. + // It exits through the v=+1 or v=-1 edge if the u-component of N has a + // larger absolute magnitude than the v-component. + if fabs(p.x) >= fabs(p.y) { + return .v + } + return .u + } + // The line passes through through two adjacent edges of the face. + // It exits the v=+1 or v=-1 edge if an even number of the components of N + // are negative. We test this using signbit() rather than multiplication + // to avoid the possibility of underflow. + let x = p.x.sign == .plus ? 1 : 0 + let y = p.y.sign == .plus ? 1 : 0 + let z = p.z.sign == .plus ? 1 : 0 + if x ^ y ^ z == 0 { + return .v + } + return .u + } + + // exitPoint returns the UV coordinates of the point where a directed line L (represented + // by the CCW normal of this point), exits the cube face this point is derived from along + // the given axis. + func exitPoint(axis a: UVAxis) -> R2Point { + switch a { + case .u: + var u = -1.0 + if p.y > 0 { + u = 1.0 + } + return R2Point(x: u, y: (-u*p.x - p.z) / p.y) + case .v: + var v = -1.0 + if p.x < 0 { + v = 1.0 + } + return R2Point(x: (-v * p.y - p.z) / p.x, y: v) + } + } + +} diff --git a/Sphere2Go/S2LatLng.swift b/Sphere2Go/S2LatLng.swift new file mode 100644 index 0000000..934701c --- /dev/null +++ b/Sphere2Go/S2LatLng.swift @@ -0,0 +1,98 @@ +// +// S2LatLng.swift +// Sphere2 +// + + +import Foundation + +//package s2 +// import fmt, math, s1 + +let northPoleLat = .pi / 2.0 +let southPoleLat = -.pi / 2.0 + +// LatLng represents a point on the unit sphere as a pair of angles. +struct LatLng { + let lat: Double + let lng: Double + + // MARK: inits / factory + + init(lat: Double, lng: Double) { + self.lat = lat + self.lng = lng + } + + init(latDegrees: Double, lngDegrees: Double) { + self.lat = latDegrees * toRadians + self.lng = lngDegrees * toRadians + } + + // LatLngFromPoint returns an LatLng for a given Point. + init(point: S2Point) { + self.init(lat: point.latitude(), lng: point.longitude()) + } + + // MARK: protocols + + var description: String { + let lat2 = String(format: "%.7f", lat * toDegrees) + let lng2 = String(format: "%.7f", lng * toDegrees) + return "[\(lat2), \(lng2)]" + } + + // MARK: computed members + + // Normalized returns the normalized version of the LatLng, + // with Lat clamped to [-π/2,π/2] and Lng wrapped in [-π,π]. + func normalize() -> LatLng { + var lat2 = lat + if lat2 > northPoleLat { + lat2 = northPoleLat + } else if lat2 < southPoleLat { + lat2 = southPoleLat + } + let lng2 = remainder(lng, 2.0 * .pi) + return LatLng(lat: lat2, lng: lng2) + } + + // MARK: tests + + // IsValid returns true iff the LatLng is normalized, with Lat ∈ [-π/2,π/2] and Lng ∈ [-π,π]. + func isValid() -> Bool { + return fabs(lat) <= .pi/2 && fabs(lng) <= .pi + } + + // MARK: arithmetic + + // Distance returns the angle between two LatLngs. + func distance(_ latLng: LatLng) -> Double { + // Haversine formula, as used in C++ S2LatLng::GetDistance. + let lat1 = lat + let lat2 = latLng.lat + let lng1 = lng + let lng2 = latLng.lng + let dlat = sin(0.5 * (lat2 - lat1)) + let dlng = sin(0.5 * (lng2 - lng1)) + let x = dlat * dlat + dlng * dlng * cos(lat1) * cos(lat2) + return 2 * atan2(sqrt(x), sqrt(max(0, 1-x))) + } + + // NOTE(mikeperrow): The C++ implementation publicly exposes latitude/longitude + // functions. Let's see if that's really necessary before exposing the same functionality. + + static func latitude(_ vector: R3Vector) -> Double { + return atan2(vector.z, sqrt(vector.x * vector.x + vector.y * vector.y)) + } + + static func longitude(_ vector: R3Vector) -> Double { + return atan2(vector.y, vector.x) + } + + // PointFromLatLng returns an Point for the given LatLng. + func toPoint() -> S2Point { + return S2Point(latLng: self) + } + +} diff --git a/Sphere2Go/S2Loop.swift b/Sphere2Go/S2Loop.swift new file mode 100644 index 0000000..f33674f --- /dev/null +++ b/Sphere2Go/S2Loop.swift @@ -0,0 +1,378 @@ +// +// S2Loop.swift +// Sphere2 +// + +import Foundation + +// package s2 +// import math, r1, r3, s1 + +// Loop represents a simple spherical polygon. It consists of a sequence +// of vertices where the first vertex is implicitly connected to the +// last. All loops are defined to have a CCW orientation, i.e. the interior of +// the loop is on the left side of the edges. This implies that a clockwise +// loop enclosing a small area is interpreted to be a CCW loop enclosing a +// very large area. +// +// Loops are not allowed to have any duplicate vertices (whether adjacent or +// not), and non-adjacent edges are not allowed to intersect. Loops must have +// at least 3 vertices (except for the "empty" and "full" loops discussed +// below). +// +// There are two special loops: the "empty" loop contains no points and the +// "full" loop contains all points. These loops do not have any edges, but to +// preserve the invariant that every loop can be represented as a vertex +// chain, they are defined as having exactly one vertex each (see EmptyLoop +// and FullLoop). +// The major differences from the C++ version is pretty much everything. +struct S2Loop: Shape, S2Region { + + // + let vertices: [S2Point] + + // originInside keeps a precomputed value whether this loop contains the origin + // versus computing from the set of vertices every time. + var originInside: Bool + + // bound is a conservative bound on all points contained by this loop. + // If l.ContainsPoint(P), then l.bound.ContainsPoint(P). + var bound: S2Rect + + // Since "bound" is not exact, it is possible that a loop A contains + // another loop B whose bounds are slightly larger. subregionBound + // has been expanded sufficiently to account for this error, i.e. + // if A.Contains(B), then A.subregionBound.Contains(B.bound). + var subregionBound: S2Rect + + // MARK: inits / factory + + init(vertices: [S2Point], originInside: Bool, bound: S2Rect, subregionBound: S2Rect) { + self.vertices = vertices + self.originInside = originInside + self.bound = bound + self.subregionBound = subregionBound + } + +// init(vertices: [S2Point], originInside: Bool) { +// self.vertices = vertices +// self.originInside = originInside +// self.bound = S2Rect.empty +// self.subregionBound = S2Rect.empty +// } + + // constructs a loop from the given points + init(points: [S2Point]) { + let vertices = points + // create preliminary loop object with empty bounds +// var l = + // figure out origin + let originInside: Bool + switch vertices.count { + case 1: + // This is the special empty or full loop, so the origin depends on if + // the vertex is in the southern hemisphere or not. + originInside = vertices[0].z < 0 + case 0, 2: + // these are incomplete loops + self.init(vertices: points, originInside: false, bound: S2Rect.empty, subregionBound: S2Rect.empty) + return + default: + // Point containment testing is done by counting edge crossings starting + // at a fixed point on the sphere (OriginPoint). We need to know whether + // the reference point (OriginPoint) is inside or outside the loop before + // we can construct the S2ShapeIndex. We do this by first guessing that + // it is outside, and then seeing whether we get the correct containment + // result for vertex 1. If the result is incorrect, the origin must be + // inside the loop. + // + // A loop with consecutive vertices A,B,C contains vertex B if and only if + // the fixed vector R = B.Ortho is contained by the wedge ABC. The + // wedge is closed at A and open at C, i.e. the point B is inside the loop + // if A = R but not if C = R. This convention is required for compatibility + // with VertexCrossing. (Note that we can't use OriginPoint + // as the fixed vector because of the possibility that B == OriginPoint.) + let ortho = S2Point(raw: vertices[1].v.ortho()) + let v1Inside = S2Point.orderedCCW(ortho, vertices[0], vertices[2], vertices[1]) + originInside = v1Inside != S2Loop.contains(vertices: vertices, originInside: false, point: vertices[1]) + } +// l = S2Loop(vertices: points, originInside: originInside, bound: S2Rect.empty, subregionBound: S2Rect.empty) + + // We *must* call initBound before initIndex, because initBound calls + // ContainsPoint(s2.Point), and ContainsPoint(s2.Point) does a bounds check whenever the + // index is not fresh (i.e., the loop has been added to the index but the + // index has not been updated yet). + // Check for the special "empty" and "full" loops. + if vertices.count == 1 { // empty of full + let bound = originInside ? S2Rect.full : S2Rect.empty + self.init(vertices: vertices, originInside: originInside, bound: bound, subregionBound: bound) + return + } + + // The bounding rectangle of a loop is not necessarily the same as the + // bounding rectangle of its vertices. First, the maximal latitude may be + // attained along the interior of an edge. Second, the loop may wrap + // entirely around the sphere (e.g. a loop that defines two revolutions of a + // candy-cane stripe). Third, the loop may include one or both poles. + // Note that a small clockwise loop near the equator contains both poles. + var bounder = RectBounder() + for p in vertices { + bounder.add(point: p) + } + bounder.add(point: vertices[0]) + var b = bounder.rectBound() + + if S2Loop.contains(vertices: vertices, originInside: originInside, point: S2Point(x: 0, y: 0, z: 1)) { + b = S2Rect(lat: R1Interval(lo: b.lat.lo, hi: .pi / 2.0), lng: S1Interval.full) + } + // If a loop contains the south pole, then either it wraps entirely + // around the sphere (full longitude range), or it also contains the + // north pole in which case b.Lng.IsFull() due to the test above. + // Either way, we only need to do the south pole containment test if + // b.Lng.IsFull(). + if b.lng.isFull() && S2Loop.contains(vertices: vertices, originInside: originInside, point: S2Point(x: 0, y: 0, z: -1)) { + b = S2Rect(lat: R1Interval(lo: -.pi / 2.0, hi: b.lat.hi), lng: S1Interval.full) + } + self.init(vertices: vertices, originInside: originInside, bound: b, subregionBound: b.expandForSubregions()) + // TODO(roberts): Depends on s2shapeindex being implemented. + // l.initIndex() + } + + // returns true if the loop contains the point + static func contains(vertices: [S2Point], originInside: Bool, point: S2Point) -> Bool { + // TODO(sbeckman): Move to bruteForceContains and update with ShapeIndex when available. + // Empty and full loops don't need a special case, but invalid loops with + // zero vertices do, so we might as well handle them all at once. + if vertices.count < 3 { + return originInside + } + let origin = S2Point.origin + var inside = originInside + var crosser = EdgeCrosser(a: origin, b: point, c: vertices[0]) + for i in 1.. S2Loop { + let vertices = points + // create preliminary loop object with empty bounds + var l = S2Loop(vertices: points, originInside: false, bound: S2Rect.empty, subregionBound: S2Rect.empty) + // figure out origin + let originInside: Bool + switch l.vertices.count { + case 1: + // This is the special empty or full loop, so the origin depends on if + // the vertex is in the southern hemisphere or not. + originInside = vertices[0].z < 0 + case 0, 2: + // these are incomplete loops + return l + default: + // Point containment testing is done by counting edge crossings starting + // at a fixed point on the sphere (OriginPoint). We need to know whether + // the reference point (OriginPoint) is inside or outside the loop before + // we can construct the S2ShapeIndex. We do this by first guessing that + // it is outside, and then seeing whether we get the correct containment + // result for vertex 1. If the result is incorrect, the origin must be + // inside the loop. + // + // A loop with consecutive vertices A,B,C contains vertex B if and only if + // the fixed vector R = B.Ortho is contained by the wedge ABC. The + // wedge is closed at A and open at C, i.e. the point B is inside the loop + // if A = R but not if C = R. This convention is required for compatibility + // with VertexCrossing. (Note that we can't use OriginPoint + // as the fixed vector because of the possibility that B == OriginPoint.) + let ortho = S2Point(raw: vertices[1].v.ortho()) + let v1Inside = S2Point.orderedCCW(ortho, vertices[0], vertices[2], vertices[1]) + originInside = v1Inside != l.contains(vertices[1]) + } + l = S2Loop(vertices: points, originInside: originInside, bound: S2Rect.empty, subregionBound: S2Rect.empty) + + // We *must* call initBound before initIndex, because initBound calls + // ContainsPoint(s2.Point), and ContainsPoint(s2.Point) does a bounds check whenever the + // index is not fresh (i.e., the loop has been added to the index but the + // index has not been updated yet). + // Check for the special "empty" and "full" loops. + if l.isEmptyOrFull() { + let bound = l.isEmpty() ? S2Rect.empty : S2Rect.full + return S2Loop(vertices: vertices, originInside: originInside, bound: bound, subregionBound: bound) + } + + // The bounding rectangle of a loop is not necessarily the same as the + // bounding rectangle of its vertices. First, the maximal latitude may be + // attained along the interior of an edge. Second, the loop may wrap + // entirely around the sphere (e.g. a loop that defines two revolutions of a + // candy-cane stripe). Third, the loop may include one or both poles. + // Note that a small clockwise loop near the equator contains both poles. + var bounder = RectBounder() + for p in vertices { + bounder.add(point: p) + } + bounder.add(point: vertices[0]) + var b = bounder.rectBound() + + if l.contains(S2Point(x: 0, y: 0, z: 1)) { + b = S2Rect(lat: R1Interval(lo: b.lat.lo, hi: .pi / 2.0), lng: S1Interval.full) + } + // If a loop contains the south pole, then either it wraps entirely + // around the sphere (full longitude range), or it also contains the + // north pole in which case b.Lng.IsFull() due to the test above. + // Either way, we only need to do the south pole containment test if + // b.Lng.IsFull(). + if b.lng.isFull() && l.contains(S2Point(x: 0, y: 0, z: -1)) { + b = S2Rect(lat: R1Interval(lo: -.pi / 2.0, hi: b.lat.hi), lng: S1Interval.full) + } + return S2Loop(vertices: vertices, originInside: originInside, bound: b, subregionBound: b.expandForSubregions()) + // TODO(roberts): Depends on s2shapeindex being implemented. + // l.initIndex() + } + + // EmptyLoop returns a special "empty" loop. + static let empty = S2Loop.loopFromPoints([S2Point(x: 0, y: 0, z: 1)]) + + // FullLoop returns a special "full" loop. + static let full = S2Loop.loopFromPoints([S2Point(x: 0, y: 0, z: -1)]) + + // MARK: tests + + // ContainsOrigin reports true if this loop contains s2.OriginPoint(). + func containsOrigin() -> Bool { + return originInside + } + + // HasInterior returns true because all loops have an interior. + func hasInterior() -> Bool { + return true + } + + // IsEmpty reports true if this is the special "empty" loop that contains no points. + func isEmpty() -> Bool { + return isEmptyOrFull() && !containsOrigin() + } + + // IsFull reports true if this is the special "full" loop that contains all points. + func isFull() -> Bool { + return isEmptyOrFull() && containsOrigin() + } + + // isEmptyOrFull reports true if this loop is either the "empty" or "full" special loops. + func isEmptyOrFull() -> Bool { + return vertices.count == 1 + } + + // MARK: computed members + + // NumEdges returns the number of edges in this shape. + func numEdges() -> Int { + if isEmptyOrFull() { + return 0 + } + return vertices.count + } + + // Edge returns the endpoints for the given edge index. + func edge(_ i: Int) -> (S2Point, S2Point) { + return (vertex(i), vertex(i + 1)) + } + + // RectBound returns a tight bounding rectangle. If the loop contains the point, + // the bound also contains it. + func rectBound() -> S2Rect { + return bound + } + + // CapBound returns a bounding cap that may have more padding than the corresponding + // RectBound. The bound is conservative such that if the loop contains a point P, + // the bound also contains it. + func capBound() -> S2Cap { + return bound.capBound() + } + + // Vertex returns the vertex for the given index. For convenience, the vertex indices + // wrap automatically for methods that do index math such as Edge. + // i.e., Vertex(NumEdges() + n) is the same as Vertex(n). + func vertex(_ i: Int) -> S2Point { + return vertices[i % vertices.count] + } + + // returns true if the loop contains the point + func contains(_ point: S2Point) -> Bool { + // TODO(sbeckman): Move to bruteForceContains and update with ShapeIndex when available. + // Empty and full loops don't need a special case, but invalid loops with + // zero vertices do, so we might as well handle them all at once. + if vertices.count < 3 { + return originInside + } + let origin = S2Point.origin + var inside = originInside + var crosser = EdgeCrosser(a: origin, b: point, c: vertices[0]) + for i in 1.. Bool { + let cellVertices = (0..<4).map { cell.vertex($0) } + // if the loop does not contain all cell vertices, return false + for k in 0..<4 { + if !contains(cellVertices[k]) { + return false + } + } + // if there are any edge crossing, it is not containing + for j in 0..<4 { + var crosser = EdgeCrosser(a: cellVertices[j], b: cellVertices[(j+1)&3], c: vertices[0]) + for i in 1.. Bool { + let cellVertices = (0..<4).map { cell.vertex($0) } + // intersects if the loop contains any cell vertex + for k in 0..<4 { + let vertex = cell.vertex(k) + if contains(vertex) { + return true + } + } + // intersects if any loop edge crosses with any cell edge + for j in 0..<4 { + var crosser = EdgeCrosser(a: cellVertices[j], b: cellVertices[(j+1)&3], c: vertices[0]) + for i in 1.. Bool { + return row >= 0 && row < rows && column >= 0 && column < columns + } + + subscript(row: Int, column: Int) -> Double { + get { + assert(indexIsValidForRow(row, column: column), "Index out of range") + return m[row * columns + column] + } + set { + assert(indexIsValidForRow(row, column: column), "Index out of range") + m[row * columns + column] = newValue + } + } + +} + +extension Matrix { + + // col returns the given column as a Point. + func col(_ col: Int) -> S2Point { + return S2Point(x: self[0, col], y: self[1, col], z: self[2, col]) + } + + // row returns the given row as a Point. + func row(_ row: Int) -> S2Point { + return S2Point(x: self[row, 0], y: self[row, 1], z: self[row, 2]) + } + + // setCol sets the specified column to the value in the given Point. + func setCol(_ col: Int, point p: S2Point) { + self[0, col] = p.x + self[1, col] = p.y + self[2, col] = p.z + } + + // setRow sets the specified row to the value in the given Point. + func setRow(_ row: Int, point p: S2Point) -> Matrix { + self[row, 0] = p.x + self[row, 1] = p.y + self[row, 2] = p.z + return self + } + + // scale multiplies the matrix by the given value. + func scale(scalar f: Double) -> Matrix { + return Matrix( + r1: [f * self[0, 0], f * self[0, 1], f * self[0, 2]], + r2: [f * self[1, 0], f * self[1, 1], f * self[1, 2]], + r3: [f * self[2, 0], f * self[2, 1], f * self[2, 2]]) + } + + // mul returns the multiplication of m by the Point p and converts the + // resulting 1x3 matrix into a Point. + func mul(point p: S2Point) -> S2Point { + let x = self[0, 0]*p.x+self[0, 1]*p.y+self[0, 2]*p.z + let y = self[1, 0]*p.x+self[1, 1]*p.y+self[1, 2]*p.z + let z = self[2, 0]*p.x+self[2, 1]*p.y+self[2, 2]*p.z + return S2Point(x: x, y: y, z: z) + } + + // det returns the determinant of this matrix. + func det() -> Double { + // | a b c | + // det | d e f | = aei + bfg + cdh - ceg - bdi - afh + // | g h i | + let aei = self[0, 0]*self[1, 1]*self[2, 2] + let bfg = self[0, 1]*self[1, 2]*self[2, 0] + let cdh = self[0, 2]*self[1, 0]*self[2, 1] + let ceg = self[0, 2]*self[1, 1]*self[2, 0] + let bdi = self[0, 1]*self[1, 0]*self[2, 2] + let afh = self[0, 0]*self[1, 2]*self[2, 1] + return aei + bfg + cdh - ceg - bdi - afh + } + + // transpose reflects the matrix along its diagonal and returns the result. + func transpose() -> Matrix { + let tmp1 = self[0, 1] + self[1, 0] = self[0, 1] + self[1, 0] = tmp1 + let tmp2 = self[0, 2] + self[0, 2] = self[2, 0] + self[2, 0] = tmp2 + let tmp3 = self[1, 2] + self[1, 2] = self[2, 1] + self[2, 1] = tmp3 + return self + } + + // MARK: protocols + + // String formats the matrix into an easier to read layout. + var description: String { + return String(format: "[ %0.4f %0.4f %0.4f ] [ %0.4f %0.4f %0.4f ] [ %0.4f %0.4f %0.4f ]", + self[0, 0], self[0, 1], self[0, 2], + self[1, 0], self[1, 1], self[1, 2], + self[2, 0], self[2, 1], self[2, 2]) + } + + // getFrame returns the orthonormal frame for the given point on the unit sphere. + static func getFrame(_ point: S2Point) -> Matrix { + // Given the point p on the unit sphere, extend this into a right-handed + // coordinate frame of unit-length column vectors m = (x,y,z). Note that + // the vectors (x,y) are an orthonormal frame for the tangent space at point p, + // while p itself is an orthonormal frame for the normal space at p. + let o = point.v.ortho() + let m = Matrix() + m.setCol(2, point: point) + m.setCol(1, point: S2Point(raw: o)) + m.setCol(0, point: S2Point(raw: o.cross(point.v))) + return m + } + + // toFrame returns the coordinates of the given point with respect to its orthonormal basis m. + // The resulting point q satisfies the identity (m * q == p). + static func toFrame(_ matrix: Matrix, point: S2Point) -> S2Point { + // The inverse of an orthonormal matrix is its transpose. + return matrix.transpose().mul(point: point) + } + + // fromFrame returns the coordinates of the given point in standard axis-aligned basis + // from its orthonormal basis m. + // The resulting point p satisfies the identity (p == m * q). + static func fromFrame(_ matrix: Matrix, point: S2Point) -> S2Point { + return matrix.mul(point: point) + } + +} diff --git a/Sphere2Go/S2Metric.swift b/Sphere2Go/S2Metric.swift new file mode 100644 index 0000000..d0f47c2 --- /dev/null +++ b/Sphere2Go/S2Metric.swift @@ -0,0 +1,75 @@ +// +// S2Metric.swift +// Sphere2 +// + +import Foundation + +// package s2 +// import math + +// This file implements functions for various S2 measurements. +// A Metric is a measure for cells. +struct Metric { + + // Dim is either 1 or 2, for a 1D or 2D metric respectively. + let dim: Int + // Deriv is the scaling factor for the metric. + let deriv: Double + + // MARK: inits / factory + + // default constructur is utomatic + + // Defined metrics. + // We only support the quadratic projection. + static let minWidth = Metric(dim: 1, deriv: 2 * sqrt(2.0) / 3) + static let maxWidth = Metric(dim: 1, deriv: 1.704897179199218452) + + static let minArea = Metric(dim: 2, deriv: 8 * sqrt(2.0) / 9) + static let avgArea = Metric(dim: 2, deriv: 4 * .pi / 6) + static let maxArea = Metric(dim: 2, deriv: 2.635799256963161491) + + // TODO: more metrics, as needed + // TODO: port GetValue, GetClosestLevel + + // Value returns the value of the metric at the given level. + func value(_ level: Int) -> Double { + return scalbn(deriv, -dim * level) + } + + // MinLevel returns the minimum level such that the metric is at most + // the given value, or maxLevel (30) if there is no such level. + func minLevel(_ val: Double) -> Int { + if val < 0 { + return CellId.maxLevel + } + + var level = -(Int(logb(val / deriv)) >> (dim - 1)) + if level > CellId.maxLevel { + level = CellId.maxLevel + } + if level < 0 { + level = 0 + } + return level + } + + // MaxLevel returns the maximum level such that the metric is at least + // the given value, or zero if there is no such level. + func maxLevel(_ val: Double) -> Int { + if val <= 0 { + return CellId.maxLevel + } + + var level = Int(logb(deriv / val)) >> (dim - 1) + if level > CellId.maxLevel { + level = CellId.maxLevel + } + if level < 0 { + level = 0 + } + return level + } + +} diff --git a/Sphere2Go/S2Point.swift b/Sphere2Go/S2Point.swift new file mode 100644 index 0000000..b23dc29 --- /dev/null +++ b/Sphere2Go/S2Point.swift @@ -0,0 +1,607 @@ +// +// S2Point.swift +// Sphere2 +// + +import Foundation + +// package s2 +// import math, r3, s1 + +// Direction is an indication of the ordering of a set of points +enum Direction: Int { + // These are the three options for the direction of a set of points. + case clockwise = -1 + case indeterminate = 0 + case counterClockwise = 1 +} + +prefix func -(d: Direction) -> Direction { + switch d { + case .clockwise: return .counterClockwise + case .indeterminate: return .indeterminate + case .counterClockwise: return .clockwise + } +} + +// maxDeterminantError is the maximum error in computing (AxB).C where all vectors +// are unit length. Using standard inequalities, it can be shown that +// +// fl(AxB) = AxB + D where |D| <= (|AxB| + (2/sqrt(3))*|A|*|B|) * e +// +// where "fl()" denotes a calculation done in floating-point arithmetic, +// |x| denotes either absolute value or the L2-norm as appropriate, and +// e is a reasonably small value near the noise level of floating point +// number accuracy. Similarly, +// +// fl(B.C) = B.C + d where |d| <= (|B.C| + 2*|B|*|C|) * e . +// +// Applying these bounds to the unit-length vectors A,B,C and neglecting +// relative error (which does not affect the sign of the result), we get +// +// fl((AxB).C) = (AxB).C + d where |d| <= (3 + 2/sqrt(3)) * e +let maxDeterminantError = 4.6125e-16 + +// detErrorMultiplier is the factor to scale the magnitudes by when checking +// for the sign of set of points with certainty. Using a similar technique to +// the one used for maxDeterminantError, the error is at most: +// +// |d| <= (3 + 6/sqrt(3)) * |A-C| * |B-C| * e +// +// If the determinant magnitude is larger than this value then we know its sign with certainty. +let detErrorMultiplier = 7.1767e-16 + +// Point represents a point on the unit sphere as a normalized 3D vector. +// +// Points are guaranteed to be close to normalized. +// +// Fields should be treated as read-only. Use one of the factory methods for creation. + +// S2Point represents a point in RxRxR. +struct S2Point: Equatable, CustomStringConvertible, Hashable { + + // + let x: Double + let y: Double + let z: Double + + // + static let epsilon = 1e-14 + + // MARK: inits / factory + + init(x: Double, y: Double, z: Double, normalize: Bool) { + // not normalized + if normalize { + fatalError("won't normalize this way") + } + self.x = x + self.y = y + self.z = z + } + + init(origin: Bool) { + self.init(x: 0, y: 0, z: 0) + } + + init(x: Double, y: Double, z: Double) { + // normalize explicitly to prevent needing recursive construction + let norm2 = x * x + y * y + z * z + if norm2 == 0.0 { + self.x = 0.00456762077230 + self.y = 0.99947476613078 + self.z = 0.03208315302933 + } else { + let norm = sqrt(norm2) + self.x = x / norm + self.y = y / norm + self.z = z / norm + } + } + + // PointFromLatLng returns an Point for the given LatLng. + init(latLng: LatLng) { + let phi = latLng.lat + let theta = latLng.lng + let cosphi = cos(phi) + self.init(x: cos(theta) * cosphi, y: sin(theta) * cosphi, z: sin(phi)) + } + + // PointFromCoords creates a new normalized point from coordinates. + // + // This always returns a valid point. If the given coordinates can not be normalized + // the origin point will be returned. + // + // This behavior is different from the C++ construction of a S2Point from coordinates + // (i.e. S2Point(x, y, z)) in that in C++ they do not Normalize. +// static func pointFromCoords(x: Double, y: Double, z: Double) -> S2Point { +// if x == 0.0 && y == 0.0 && z == 0.0 { +// return origin +// } +// return S2Point(x: x, y: y, z: z).normalize() +// } +// + init(raw: R3Vector) { + self.init(x: raw.x, y: raw.y, z: raw.z) + } + + // OriginPoint returns a unique "origin" on the sphere for operations that need a fixed + // reference point. In particular, this is the "point at infinity" used for + // point-in-polygon testing (by counting the number of edge crossings). + // + // It should *not* be a point that is commonly used in edge tests in order + // to avoid triggering code to handle degenerate cases (this rules out the + // north and south poles). It should also not be on the boundary of any + // low-level S2Cell for the same reason. + static let origin = S2Point(origin: true) + + // MARK: protocols + + var description: String { + return "(\(x), \(y), \(z))" + } + + static func ==(lhs: S2Point, rhs: S2Point) -> Bool { + return lhs.x == rhs.x && lhs.y == rhs.y && lhs.z == rhs.z + } + + // MARK: tests + + // ApproxEqual reports whether v and other are equal within a small epsilon. + func approxEquals(_ point: S2Point) -> Bool { + return angle(point) <= S2Point.epsilon + // as opposed to the vector implementation + // return fabs(x-point.x) < epsilon && fabs(y-point.y) < epsilon && fabs(z-point.z) < epsilon + } + + // IsUnit returns whether this S2Point is of approximately unit length. + func isUnit() -> Bool { + return fabs(v.norm2() - 1.0) <= S2Point.epsilon + } + + // MARK: computed members + + var v: R3Vector { + return R3Vector(x: x, y: y, z:z) + } + + // Norm returns the S2Point's norm. + func norm() -> Double { + return sqrt(dot(self)) + } + + // Norm2 returns the square of the norm. + func norm2() -> Double { + return dot(self) + } + + // Normalize returns a unit S2Point in the same direction as + func normalize() -> R3Vector { + if x == 0.0 && y == 0.0 && z == 0.0 { + return mul(1.0) //self + } + return mul(1.0 / norm()) + } + + // Abs returns the S2Point with nonnegative components. + func abs() -> S2Point { + return S2Point(x: fabs(x), y: fabs(y), z: fabs(z)) + } + + // Ortho returns a unit S2Point that is orthogonal to + // Ortho(-v) = -Ortho(v) for all + func ortho() -> R3Vector { + // Grow a component other than the largest in v, to guarantee that they aren't + // parallel (which would make the cross product zero). + let other: S2Point + if fabs(x) > fabs(y) { + other = S2Point(x: 0.012, y: 1.0, z: 0.00457) + } else { + other = S2Point(x: 1.0, y: 0.0053, z: 0.00457) + } + return cross(other).normalize() + } + + // MARK: arithmetic + + // + func inverse() -> S2Point { + return S2Point(x: -x, y: -y, z: -z) + } + + // Sub returns the standard S2Point difference of v and other. + func sub(_ point: S2Point) -> R3Vector { + return R3Vector(x: x - point.x, y: y - point.y, z: z - point.z) + } + + // Mul returns the standard scalar product of v and m. + func mul(_ m: Double) -> R3Vector { + return R3Vector(x: m * x, y: m * y, z: m * z) + } + + // Dot returns the standard dot product of v and other. + func dot(_ point: S2Point) -> Double { + return x * point.x + y * point.y + z * point.z + } + + // Cross returns the standard cross product of v and other. + func cross(_ point: S2Point) -> R3Vector { + return R3Vector(x: y * point.z - z * point.y, y: z * point.x - x * point.z, z: x * point.y - y * point.x) + } + + // EuclideanDistance returns the Euclidean distance between v and other. + func euclideanDistance(_ point: S2Point) -> Double { + return v.sub(point.v).norm() + } + + // Distance returns the angle between two points. + func distance(_ b: S2Point) -> Double { + return angle(b) + } + + // Angle returns the angle between v and other. + func angle(_ point: S2Point) -> Double { + return atan2(v.cross(point.v).norm(), v.dot(point.v)) + } + + // PointCross returns a Point that is orthogonal to both p and op. This is similar to + // p.Cross(op) (the true cross product) except that it does a better job of + // ensuring orthogonality when the Point is nearly parallel to op, it returns + // a non-zero result even when p == op or p == -op and the result is a Point, + // so it will have norm 1. + // + // It satisfies the following properties (f == PointCross): + // + // (1) f(p, op) != 0 for all p, op + // (2) f(op,p) == -f(p,op) unless p == op or p == -op + // (3) f(-p,op) == -f(p,op) unless p == op or p == -op + // (4) f(p,-op) == -f(p,op) unless p == op or p == -op + func pointCross(_ op: S2Point) -> S2Point { + // NOTE(dnadasi): In the C++ API the equivalent method here was known as "RobustCrossProd", + // but PointCross more accurately describes how this method is used. + let x = v.add(op.v).cross(op.v.sub(v)) + if x.approxEquals(R3Vector(x: 0, y: 0, z: 0)) { + // The only result that makes sense mathematically is to return zero, but + // we find it more convenient to return an arbitrary orthogonal vector. + return S2Point(raw: v.ortho()) + } + return S2Point(raw: x) + } + + // Sign returns true if the points A, B, C are strictly counterclockwise, + // and returns false if the points are clockwise or collinear (i.e. if they are all + // contained on some great circle). + // + // Due to numerical errors, situations may arise that are mathematically + // impossible, e.g. ABC may be considered strictly CCW while BCA is not. + // However, the implementation guarantees the following: + // + // If Sign(a,b,c), then !Sign(c,b,a) for all a,b,c. + static func sign(_ a: S2Point, b: S2Point, c: S2Point) -> Bool { + // NOTE(dnadasi): In the C++ API the equivalent method here was known as "SimpleSign". + + // We compute the signed volume of the parallelepiped ABC. The usual + // formula for this is (A ⨯ B) · C, but we compute it here using (C ⨯ A) · B + // in order to ensure that ABC and CBA are not both CCW. This follows + // from the following identities (which are true numerically, not just + // mathematically): + // + // (1) x ⨯ y == -(y ⨯ x) + // (2) -x · y == -(x · y) + return c.v.cross(a.v).dot(b.v) > 0 + } + + // RobustSign returns a Direction representing the ordering of the points. + // CounterClockwise is returned if the points are in counter-clockwise order, + // Clockwise for clockwise, and Indeterminate if any two points are the same (collinear), + // or the sign could not completely be determined. + // + // This function has additional logic to make sure that the above properties hold even + // when the three points are coplanar, and to deal with the limitations of + // floating-point arithmetic. + // + // RobustSign satisfies the following conditions: + // + // (1) RobustSign(a,b,c) == Indeterminate if and only if a == b, b == c, or c == a + // (2) RobustSign(b,c,a) == RobustSign(a,b,c) for all a,b,c + // (3) RobustSign(c,b,a) == -RobustSign(a,b,c) for all a,b,c + // + // In other words: + // + // (1) The result is Indeterminate if and only if two points are the same. + // (2) Rotating the order of the arguments does not affect the result. + // (3) Exchanging any two arguments inverts the result. + // + // On the other hand, note that it is not true in general that + // RobustSign(-a,b,c) == -RobustSign(a,b,c), or any similar identities + // involving antipodal points. + static func robustSign(_ a: S2Point, _ b: S2Point, _ c: S2Point) -> Direction { + let sign = triageSign(a, b, c) + if sign == .indeterminate { + return expensiveSign(a, b, c) + } + return sign + } + + // triageSign returns the direction sign of the points. It returns Indeterminate if two + // points are identical or the result is uncertain. Uncertain cases can be resolved, if + // desired, by calling expensiveSign. + // + // The purpose of this method is to allow additional cheap tests to be done without + // calling expensiveSign. + static func triageSign(_ a: S2Point, _ b: S2Point, _ c: S2Point) -> Direction { + let det = c.v.cross(a.v).dot(b.v) + if det > maxDeterminantError { + return .counterClockwise + } + if det < -maxDeterminantError { + return .clockwise + } + return .indeterminate + } + + // expensiveSign reports the direction sign of the points. It returns Indeterminate + // if two of the input points are the same. It uses multiple-precision arithmetic + // to ensure that its results are always self-consistent. + static func expensiveSign(_ a: S2Point, _ b: S2Point, _ c: S2Point) -> Direction { + // Return Indeterminate if and only if two points are the same. + // This ensures RobustSign(a,b,c) == Indeterminate if and only if a == b, b == c, or c == a. + // ie. Property 1 of RobustSign. + if a == b || b == c || c == a { + return .indeterminate + } + + // Next we try recomputing the determinant still using floating-point + // arithmetic but in a more precise way. This is more expensive than the + // simple calculation done by triageSign, but it is still *much* cheaper + // than using arbitrary-precision arithmetic. This optimization is able to + // compute the correct determinant sign in virtually all cases except when + // the three points are truly collinear (e.g., three points on the equator). + let detSign = stableSign(a, b, c) + if detSign != .indeterminate { + return detSign + } + + // Otherwise fall back to exact arithmetic and symbolic permutations. + return exactSign(a, b, c) + } + + // stableSign reports the direction sign of the points in a numerically stable way. + // Unlike triageSign, this method can usually compute the correct determinant sign even when all + // three points are as collinear as possible. For example if three points are + // spaced 1km apart along a random line on the Earth's surface using the + // nearest representable points, there is only a 0.4% chance that this method + // will not be able to find the determinant sign. The probability of failure + // decreases as the points get closer together; if the collinear points are + // 1 meter apart, the failure rate drops to 0.0004%. + // + // This method could be extended to also handle nearly-antipodal points (and + // in fact an earlier version of this code did exactly that), but antipodal + // points are rare in practice so it seems better to simply fall back to + // exact arithmetic in that case. + static func stableSign(_ a: S2Point, _ b: S2Point, _ c: S2Point) -> Direction { + let ab = a.v.sub(b.v) + let ab2 = ab.norm2() + let bc = b.v.sub(c.v) + let bc2 = bc.norm2() + let ca = c.v.sub(a.v) + let ca2 = ca.norm2() + + // Now compute the determinant ((A-C)x(B-C)).C, where the vertices have been + // cyclically permuted if necessary so that AB is the longest edge. (This + // minimizes the magnitude of cross product.) At the same time we also + // compute the maximum error in the determinant. + + // The two shortest edges, pointing away from their common point. + let e1: R3Vector + let e2: R3Vector + let op: S2Point + if ab2 >= bc2 && ab2 >= ca2 { + // AB is the longest edge. + e1 = ca + e2 = bc + op = c + } else if bc2 >= ca2 { + // BC is the longest edge. + e1 = ab + e2 = ca + op = a + } else { + // CA is the longest edge. + e1 = bc + e2 = ab + op = b + } + + let det = e1.cross(e2).dot(op.v) + let maxErr = detErrorMultiplier * sqrt(e1.norm2() * e2.norm2()) + + // If the determinant isn't zero, within maxErr, we know definitively the point ordering. + if det > maxErr { + return .counterClockwise + } + if det < -maxErr { + return .clockwise + } + return .indeterminate + } + + // exactSign reports the direction sign of the points using exact precision arithmetic. + static func exactSign(_ a: S2Point, _ b: S2Point, _ c: S2Point) -> Direction { + // In the C++ version, the final computation is performed using OpenSSL's + // Bignum exact precision math library. The existence of an equivalent + // library in Go is indeterminate. In C++, using the exact precision library + // to solve this stage is ~300x slower than the above checks. + // TODO(roberts): Select and incorporate an appropriate Go exact precision + // floating point library for the remaining calculations. + return .indeterminate + } + + // OrderedCCW returns true if the edges OA, OB, and OC are encountered in that + // order while sweeping CCW around the point O. + // + // You can think of this as testing whether A <= B <= C with respect to the + // CCW ordering around O that starts at A, or equivalently, whether B is + // contained in the range of angles (inclusive) that starts at A and extends + // CCW to C. Properties: + // + // (1) If OrderedCCW(a,b,c,o) && OrderedCCW(b,a,c,o), then a == b + // (2) If OrderedCCW(a,b,c,o) && OrderedCCW(a,c,b,o), then b == c + // (3) If OrderedCCW(a,b,c,o) && OrderedCCW(c,b,a,o), then a == b == c + // (4) If a == b or b == c, then OrderedCCW(a,b,c,o) is true + // (5) Otherwise if a == c, then OrderedCCW(a,b,c,o) is false + static func orderedCCW(_ a: S2Point, _ b: S2Point, _ c: S2Point, _ o: S2Point) -> Bool { + var sum = 0 + if robustSign(b, o, a) != .clockwise { + sum += 1 + } + if robustSign(c, o, b) != .clockwise { + sum += 1 + } + if robustSign(a, o, c) == .counterClockwise { + sum += 1 + } + return sum >= 2 + } + + // PointArea returns the area on the unit sphere for the triangle defined by the + // given points. + // + // This method is based on l'Huilier's theorem, + // + // tan(E/4) = sqrt(tan(s/2) tan((s-a)/2) tan((s-b)/2) tan((s-c)/2)) + // + // where E is the spherical excess of the triangle (i.e. its area), + // a, b, c are the side lengths, and + // s is the semiperimeter (a + b + c) / 2. + // + // The only significant source of error using l'Huilier's method is the + // cancellation error of the terms (s-a), (s-b), (s-c). This leads to a + // *relative* error of about 1e-16 * s / min(s-a, s-b, s-c). This compares + // to a relative error of about 1e-15 / E using Girard's formula, where E is + // the true area of the triangle. Girard's formula can be even worse than + // this for very small triangles, e.g. a triangle with a true area of 1e-30 + // might evaluate to 1e-5. + // + // So, we prefer l'Huilier's formula unless dmin < s * (0.1 * E), where + // dmin = min(s-a, s-b, s-c). This basically includes all triangles + // except for extremely long and skinny ones. + // + // Since we don't know E, we would like a conservative upper bound on + // the triangle area in terms of s and dmin. It's possible to show that + // E <= k1 * s * sqrt(s * dmin), where k1 = 2*sqrt(3)/Pi (about 1). + // Using this, it's easy to show that we should always use l'Huilier's + // method if dmin >= k2 * s^5, where k2 is about 1e-2. Furthermore, + // if dmin < k2 * s^5, the triangle area is at most k3 * s^4, where + // k3 is about 0.1. Since the best case error using Girard's formula + // is about 1e-15, this means that we shouldn't even consider it unless + // s >= 3e-4 or so. + static func pointArea(_ a: S2Point, _ b: S2Point, _ c: S2Point) -> Double { + let sa = b.angle(c) + let sb = c.angle(a) + let sc = a.angle(b) + let s = 0.5 * (sa + sb + sc) + if s >= 3e-4 { + // Consider whether Girard's formula might be more accurate. + let dmin = s - max(sa, max(sb, sc)) + if dmin < 1e-2*s*s*s*s*s { + // This triangle is skinny enough to use Girard's formula. + let ab = a.pointCross(b) + let bc = b.pointCross(c) + let ac = a.pointCross(c) + let area = max(0.0, ab.angle(ac)-ab.angle(bc)+bc.angle(ac)) + if dmin < s * 0.1 * area { + return area + } + } + } + // Use l'Huilier's formula. + return 4.0 * atan(sqrt(max(0.0, tan(0.5*s) * tan(0.5*(s-sa)) * tan(0.5*(s-sb)) * tan(0.5*(s-sc))))) + } + + // TrueCentroid returns the true centroid of the spherical triangle ABC multiplied by the + // signed area of spherical triangle ABC. The result is not normalized. + // The reasons for multiplying by the signed area are (1) this is the quantity + // that needs to be summed to compute the centroid of a union or difference of triangles, + // and (2) it's actually easier to calculate this way. All points must have unit length. + // + // The true centroid (mass centroid) is defined as the surface integral + // over the spherical triangle of (x,y,z) divided by the triangle area. + // This is the point that the triangle would rotate around if it was + // spinning in empty space. + // + // The best centroid for most purposes is the true centroid. Unlike the + // planar and surface centroids, the true centroid behaves linearly as + // regions are added or subtracted. That is, if you split a triangle into + // pieces and compute the average of their centroids (weighted by triangle + // area), the result equals the centroid of the original triangle. This is + // not true of the other centroids. + static func trueCentroid(_ a: S2Point, _ b: S2Point, _ c: S2Point) -> R3Vector { + var ra = 1.0 + let sa = b.distance(c) + if sa != 0 { + ra = sa / sin(sa) + } + var rb = 1.0 + let sb = c.distance(a) + if sb != 0 { + rb = sb / sin(sb) + } + var rc = 1.0 + let sc = a.distance(b) + if sc != 0 { + rc = sc / sin(sc) + } + + // Now compute a point M such that: + // + // [Ax Ay Az] [Mx] [ra] + // [Bx By Bz] [My] = 0.5 * det(A,B,C) * [rb] + // [Cx Cy Cz] [Mz] [rc] + // + // To improve the numerical stability we subtract the first row (A) from the + // other two rows; this reduces the cancellation error when A, B, and C are + // very close together. Then we solve it using Cramer's rule. + // + // This code still isn't as numerically stable as it could be. + // The biggest potential improvement is to compute B-A and C-A more + // accurately so that (B-A)x(C-A) is always inside triangle ABC. + let x = R3Vector(x: a.x, y: b.x - a.x, z: c.x - a.x) + let y = R3Vector(x: a.y, y: b.y - a.y, z: c.y - a.y) + let z = R3Vector(x: a.z, y: b.z - a.z, z: c.z - a.z) + let r = R3Vector(x: ra, y: rb - ra, z: rc - ra) + + return R3Vector(x: y.cross(z).dot(r), y: z.cross(x).dot(r), z: x.cross(y).dot(r)).mul(0.5) + } + + // PlanarCentroid returns the centroid of the planar triangle ABC, which is not normalized. + // It can be normalized to unit length to obtain the "surface centroid" of the corresponding + // spherical triangle, i.e. the intersection of the three medians. However, + // note that for large spherical triangles the surface centroid may be + // nowhere near the intuitive "center" (see example in TrueCentroid comments). + // + // Note that the surface centroid may be nowhere near the intuitive + // "center" of a spherical triangle. For example, consider the triangle + // with vertices A=(1,eps,0), B=(0,0,1), C=(-1,eps,0) (a quarter-sphere). + // The surface centroid of this triangle is at S=(0, 2*eps, 1), which is + // within a distance of 2*eps of the vertex B. Note that the median from A + // (the segment connecting A to the midpoint of BC) passes through S, since + // this is the shortest path connecting the two endpoints. On the other + // hand, the true centroid is at M=(0, 0.5, 0.5), which when projected onto + // the surface is a much more reasonable interpretation of the "center" of + // this triangle. + static func planarCentroid(_ a: S2Point, _ b: S2Point, _ c: S2Point) -> S2Point { + return S2Point(raw: a.v.add(b.v).add(c.v).mul(1.0 / 3.0)) + } + + // MARK: lat lng + + func latitude() -> Double { + return atan2(z, sqrt(x * x + y * y)) + } + + func longitude() -> Double { + return atan2(y, x) + } + +} diff --git a/Sphere2Go/S2Polygon.swift b/Sphere2Go/S2Polygon.swift new file mode 100644 index 0000000..8258810 --- /dev/null +++ b/Sphere2Go/S2Polygon.swift @@ -0,0 +1,117 @@ +// +// S2Polygon.swift +// Sphere2 +// + +import Foundation + +// package s2 + +// Polygon represents a sequence of zero or more loops; recall that the +// interior of a loop is defined to be its left-hand side (see Loop). +// +// When the polygon is initialized, the given loops are automatically converted +// into a canonical form consisting of "shells" and "holes". Shells and holes +// are both oriented CCW, and are nested hierarchically. The loops are +// reordered to correspond to a preorder traversal of the nesting hierarchy. +// +// Polygons may represent any region of the sphere with a polygonal boundary, +// including the entire sphere (known as the "full" polygon). The full polygon +// consists of a single full loop (see Loop), whereas the empty polygon has no +// loops at all. +// +// Use FullPolygon() to construct a full polygon. The zero value of Polygon is +// treated as the empty polygon. +// +// Polygons have the following restrictions: +// +// - Loops may not cross, i.e. the boundary of a loop may not intersect +// both the interior and exterior of any other loop. +// +// - Loops may not share edges, i.e. if a loop contains an edge AB, then +// no other loop may contain AB or BA. +// +// - Loops may share vertices, however no vertex may appear twice in a +// single loop (see Loop). +// +// - No loop may be empty. The full loop may appear only in the full polygon. +struct S2Polygon { + + let loops: [S2Loop] + + // index is a spatial index of all the polygon loops. + let index: ShapeIndex + + // hasHoles tracks if this polygon has at least one hole. + let hasHoles: Bool + + // numVertices keeps the running total of all of the vertices of the contained loops. + let numVertices: Int + + // bound is a conservative bound on all points contained by this loop. + // If l.ContainsPoint(P), then l.bound.ContainsPoint(P). + let bound: S2Rect + + // Since bound is not exact, it is possible that a loop A contains + // another loop B whose bounds are slightly larger. subregionBound + // has been expanded sufficiently to account for this error, i.e. + // if A.Contains(B), then A.subregionBound.Contains(B.bound). + let subregionBound: S2Rect +} + +extension S2Polygon: S2Region { + + // PolygonFromLoops constructs a polygon from the given hierarchically nested + // loops. The polygon interior consists of the points contained by an odd + // number of loops. (Recall that a loop contains the set of points on its + // left-hand side.) + // + // This method figures out the loop nesting hierarchy and assigns every loop a + // depth. Shells have even depths, and holes have odd depths. + // + // NOTE: this function is NOT YET IMPLEMENTED for more than one loop and will + // panic if given a slice of length > 1. + init(loops: [S2Loop]) { + if loops.count > 1 { + fatalError("multiple loops are not yet implemented") + } + // TODO(roberts): Once multi-loop is supported, fix this. + self.init(loops: loops, index: ShapeIndex(), hasHoles: false, numVertices: loops[0].vertices.count, bound: S2Rect.empty, subregionBound: S2Rect.empty) + } + + // FullPolygon returns a special "full" polygon. + static let full = S2Polygon(loops: [S2Loop.full], index: ShapeIndex(), hasHoles: false, numVertices: S2Loop.full.vertices.count, bound:S2Rect.full, subregionBound: S2Rect.full) + static let empty = S2Polygon(loops: [], index: ShapeIndex(), hasHoles: false, numVertices: 0, bound:S2Rect.empty, subregionBound: S2Rect.empty) + + // IsEmpty reports whether this is the special "empty" polygon (consisting of no loops). + func isEmpty() -> Bool { + return loops.count == 0 + } + + // IsFull reports whether this is the special "full" polygon (consisting of a + // single loop that encompasses the entire sphere). + func isFull() -> Bool { + return loops.count == 1 && loops[0].isFull() + } + + // CapBound returns a bounding spherical cap. + func capBound() -> S2Cap { + return bound.capBound() + } + + // RectBound returns a bounding latitude-longitude rectangle. + func rectBound() -> S2Rect { + return bound + } + + // ContainsCell reports whether the polygon contains the given cell. + func contains(_ cell: Cell) -> Bool { + return loops.count == 1 && loops[0].contains(cell) + } + + // IntersectsCell reports whether the polygon intersects the given cell. + func intersects(_ cell: Cell) -> Bool { + return loops.count == 1 && loops[0].intersects(cell) + } + +} diff --git a/Sphere2Go/S2Polyline.swift b/Sphere2Go/S2Polyline.swift new file mode 100644 index 0000000..5be6c77 --- /dev/null +++ b/Sphere2Go/S2Polyline.swift @@ -0,0 +1,156 @@ +// +// S2Polyline.swift +// Sphere2 +// + +import Foundation + + +// Polyline represents a sequence of zero or more vertices connected by +// straight edges (geodesics). Edges of length 0 and 180 degrees are not +// allowed, i.e. adjacent vertices should not be identical or antipodal. +struct S2Polyline: Shape, Equatable { + + let points: [S2Point] + + init(points: [S2Point]) { + self.points = points + } + + // PolylineFromLatLngs creates a new Polyline from the given LatLngs. + init(latLngs: [LatLng]) { + let points = latLngs.map { S2Point(latLng: $0) } + self.init(points: points) + } + + // Mark protocols + + // Polylines are equal when their points are equal. Rotation does not maintain equality + static func ==(lhs: S2Polyline, rhs: S2Polyline) -> Bool { + return lhs.points == rhs.points + } + + // Reverse reverses the order of the Polyline vertices. + func reversed() -> S2Polyline { + return S2Polyline(points: points.reversed()) + } + + // Length returns the length of this Polyline. + func length() -> S1Angle { + var length: S1Angle = 0.0 + for i in 1.. R3Vector { + var centroid = R3Vector.init(x: 0.0, y: 0.0, z: 0.0) + for i in 1.. S2Cap { + return rectBound().capBound() + } + + // RectBound returns the bounding Rect for this Polyline. + func rectBound() -> S2Rect { + var rb = RectBounder() + for v in points { + rb.add(point: v) + } + return rb.rectBound() + } + + // ContainsCell reports whether this Polyline contains the given Cell. Always returns false + // because "containment" is not numerically well-defined except at the Polyline vertices. + func contains(_ cell: Cell) -> Bool { + return false + } + + // IntersectsCell reports whether this Polyline intersects the given Cell. + func intersects(_ cell: Cell) -> Bool { + if points.count == 0 { + return false + } + // We only need to check whether the cell contains vertex 0 for correctness, + // but these tests are cheap compared to edge crossings so we might as well + // check all the vertices. + for v in points { + if cell.contains(v) { + return true + } + } + let cellVertices = (0...3).map { cell.vertex($0) } + for j in 0..<4 { + var crosser = EdgeCrosser(a: cellVertices[j], b: cellVertices[(j+1)&3], c: points[0]) + for i in 1.. Int { + if points.count == 0 { + return 0 + } + return points.count - 1 + } + + // Edge returns endpoints for the given edge index. + func edge(_ i: Int) -> (S2Point, S2Point) { + return (points[i], points[i+1]) + } + + // dimension returns the dimension of the geometry represented by this Polyline. + func dimension() -> Dimension { + return .polylineGeometry + } + + // numChains reports the number of contiguous edge chains in this Polyline. + func numChains() -> Int { + if numEdges() >= 1 { + return 1 + } + return 0 + } + + // chainStart returns the id of the first edge in the i-th edge chain in this Polyline. + func chainStart(i: Int) -> Int { + if i == 0 { + return 0 + } + return numEdges() + } + + // HasInterior returns false as Polylines are not closed. + func hasInterior() -> Bool { + return false + } + + // ContainsOrigin returns false because there is no interior to contain s2.Origin. + func containsOrigin() -> Bool { + return false + } + +} diff --git a/Sphere2Go/S2Rect.swift b/Sphere2Go/S2Rect.swift new file mode 100644 index 0000000..c32c861 --- /dev/null +++ b/Sphere2Go/S2Rect.swift @@ -0,0 +1,406 @@ +// +// S2Rect.swift +// Sphere2 +// + +import Foundation + +// Rect represents a closed latitude-longitude rectangle. +// The major differences from the C++ version are: +// - GetCentroid, Get*Distance, Vertex, InteriorContains(LatLng|Rect|Point) +struct S2Rect: S2Region { + + // + static let validRectLatRange = R1Interval(lo: -.pi / 2, hi: .pi / 2) + static let validRectLngRange = S1Interval.full + + // + let lat: R1Interval + let lng: S1Interval + + // MARK: inits / factory + + init(lat: R1Interval, lng: S1Interval) { + self.lat = lat + self.lng = lng + } + + // RectFromLatLng constructs a rectangle containing a single point p. + init(latLng: LatLng) { + self.init(lat: R1Interval(lo: latLng.lat, hi: latLng.lat), lng: S1Interval(lo: latLng.lng, hi: latLng.lng)) + } + + // RectFromCenterSize constructs a rectangle with the given size and center. + // center needs to be normalized, but size does not. The latitude + // interval of the result is clamped to [-90,90] degrees, and the longitude + // interval of the result is FullRect() if and only if the longitude size is + // 360 degrees or more. + // + // Examples of clamping (in degrees): + // center=(80,170), size=(40,60) -> lat=[60,90], lng=[140,-160] + // center=(10,40), size=(210,400) -> lat=[-90,90], lng=[-180,180] + // center=(-90,180), size=(20,50) -> lat=[-90,-80], lng=[155,-155] + init(center: LatLng, size: LatLng) { + let half = LatLng(lat: size.lat / 2, lng: size.lng / 2) + self.init(latLng: center) + self = self.expanded(half) + } + + // EmptyRect returns the empty rectangle. + static let empty = S2Rect(lat: R1Interval.empty, lng: S1Interval.empty) + + // FullRect returns the full rectangle. + static let full = S2Rect(lat: validRectLatRange, lng: validRectLngRange) + + // MARK: protocols + + var description: String { + return "[Lo\(lo()), Hi\(hi())]" + } + + // MARK: tests + + // IsValid returns true iff the rectangle is valid. + // This requires Lat ⊆ [-π/2,π/2] and Lng ⊆ [-π,π], and Lat = ∅ iff Lng = ∅ + func isValid() -> Bool { + return fabs(lat.lo) <= .pi/2 && fabs(lat.hi) <= .pi/2 && lng.isValid() && lat.isEmpty() == lng.isEmpty() + } + + // IsEmpty reports whether the rectangle is empty. + func isEmpty() -> Bool { + return lat.isEmpty() + } + + // IsFull reports whether the rectangle is full. + func isFull() -> Bool { + return lat == S2Rect.validRectLatRange && lng.isFull() + } + + // IsPoint reports whether the rectangle is a single point. + func isPoint() -> Bool { + return lat.lo == lat.hi && lng.lo == lng.hi + } + + // MARK: computed members + + // Vertex returns the i-th vertex of the rectangle (i = 0,1,2,3) in CCW order + // (lower left, lower right, upper right, upper left). + func vertex(_ i: Int) -> LatLng { + switch i { + case 0: + return LatLng(lat: lat.lo, lng: lng.lo) + case 1: + return LatLng(lat: lat.lo, lng: lng.hi) + case 2: + return LatLng(lat: lat.hi, lng: lng.hi) + default: + return LatLng(lat: lat.hi, lng: lng.lo) + } + } + + // Lo returns one corner of the rectangle. + func lo() -> LatLng { + return LatLng(lat: lat.lo, lng: lng.lo) + } + + // Hi returns the other corner of the rectangle. + func hi() -> LatLng { + return LatLng(lat: lat.hi, lng: lng.hi) + } + + // Center returns the center of the rectangle. + func center() -> LatLng { + return LatLng(lat: lat.center(), lng: lng.center()) + } + + // Size returns the size of the Rect. + func size() -> LatLng { + return LatLng(lat: lat.length(), lng: lng.length()) + } + + // Area returns the surface area of the Rect. + func area() -> Double { + if isEmpty() { + return 0 + } + let capDiff = fabs(sin(lat.hi) - sin(lat.lo)) + return lng.length() * capDiff + } + + // CapBound returns a cap that countains Rect. + func capBound() -> S2Cap { + // We consider two possible bounding caps, one whose axis passes + // through the center of the lat-long rectangle and one whose axis + // is the north or south pole. We return the smaller of the two caps. + if isEmpty() { + return S2Cap.empty + } + + var poleZ: Double + var poleAngle: Double + if lat.hi + lat.lo < 0 { + // South pole axis yields smaller cap. + poleZ = -1 + poleAngle = .pi/2 + lat.hi + } else { + poleZ = 1 + poleAngle = .pi/2 - lat.lo + } + let poleCap = S2Cap(center: S2Point(x: 0, y: 0, z: poleZ), angle: poleAngle) + + // For bounding rectangles that span 180 degrees or less in longitude, the + // maximum cap size is achieved at one of the rectangle vertices. For + // rectangles that are larger than 180 degrees, we punt and always return a + // bounding cap centered at one of the two poles. + if (lng.hi-lng.lo).truncatingRemainder(dividingBy: .pi*2) >= 0 && lng.hi-lng.lo < .pi*2 { + let midCap = S2Cap(point: center().toPoint()).add(lo().toPoint()).add(hi().toPoint()) + if midCap.height < poleCap.height { + return midCap + } + } + return poleCap + } + + // RectBound returns itself. + func rectBound() -> S2Rect { + return self + } + + // MARK: arithmetic + + // AddPoint increases the size of the rectangle to include the given point. + func add(_ latLng: LatLng) -> S2Rect { + if !latLng.isValid() { + return self + } + return S2Rect(lat: lat.add(latLng.lat), lng: lng.add(latLng.lng)) + } + + // expanded returns a rectangle that has been expanded by margin.Lat on each side + // in the latitude direction, and by margin.Lng on each side in the longitude + // direction. If either margin is negative, then it shrinks the rectangle on + // the corresponding sides instead. The resulting rectangle may be empty. + // + // The latitude-longitude space has the topology of a cylinder. Longitudes + // "wrap around" at +/-180 degrees, while latitudes are clamped to range [-90, 90]. + // This means that any expansion (positive or negative) of the full longitude range + // remains full (since the "rectangle" is actually a continuous band around the + // cylinder), while expansion of the full latitude range remains full only if the + // margin is positive. + // + // If either the latitude or longitude interval becomes empty after + // expansion by a negative margin, the result is empty. + // + // Note that if an expanded rectangle contains a pole, it may not contain + // all possible lat/lng representations of that pole, e.g., both points [π/2,0] + // and [π/2,1] represent the same pole, but they might not be contained by the + // same Rect. + // + // If you are trying to grow a rectangle by a certain distance on the + // sphere (e.g. 5km), refer to the ExpandedByDistance() C++ method implementation + // instead. + func expanded(_ margin: LatLng) -> S2Rect { + let lat = self.lat.expanded(margin.lat) + let lng = self.lng.expanded(margin.lng) + if lat.isEmpty() || lng.isEmpty() { + return S2Rect.empty + } + return S2Rect(lat: lat.intersection(S2Rect.validRectLatRange), lng: lng) + } + + // PolarClosure returns the rectangle unmodified if it does not include either pole. + // If it includes either pole, PolarClosure returns an expansion of the rectangle along + // the longitudinal range to include all possible representations of the contained poles. + func polarClosure() -> S2Rect { + if lat.lo == -.pi/2 || lat.hi == .pi/2 { + return S2Rect(lat: lat, lng: S1Interval.full) + } + return self + } + + // Union returns the smallest Rect containing the union of this rectangle and the given rectangle. + func union(_ rect: S2Rect) -> S2Rect { + return S2Rect(lat: lat.union(rect.lat), lng: lng.union(rect.lng)) + } + + // Intersection returns the smallest rectangle containing the intersection of + // this rectangle and the given rectangle. Note that the region of intersection + // may consist of two disjoint rectangles, in which case a single rectangle + // spanning both of them is returned. + func intersection(_ rect: S2Rect) -> S2Rect { + let lat = self.lat.intersection(rect.lat) + let lng = self.lng.intersection(rect.lng) + if lat.isEmpty() || lng.isEmpty() { + return S2Rect.empty + } + return S2Rect(lat: lat, lng: lng) + } + + // MARK: contains / intersects + + // Intersects reports whether this rectangle and the other have any points in common. + func intersects(_ rect: S2Rect) -> Bool { + return lat.intersects(rect.lat) && lng.intersects(rect.lng) + } + + // Contains reports whether this Rect contains the other Rect. + func contains(_ rect: S2Rect) -> Bool { + return lat.contains(rect.lat) && lng.contains(rect.lng) + } + + // ContainsCell reports whether the given Cell is contained by this Rect. + func contains(_ cell: Cell) -> Bool { + // A latitude-longitude rectangle contains a cell if and only if it contains + // the cell's bounding rectangle. This test is exact from a mathematical + // point of view, assuming that the bounds returned by Cell.RectBound() + // are tight. However, note that there can be a loss of precision when + // converting between representations -- for example, if an s2.Cell is + // converted to a polygon, the polygon's bounding rectangle may not contain + // the cell's bounding rectangle. This has some slightly unexpected side + // effects; for instance, if one creates an s2.Polygon from an s2.Cell, the + // polygon will contain the cell, but the polygon's bounding box will not. + return contains(cell.rectBound()) + } + + // ContainsLatLng reports whether the given LatLng is within the Rect. + func contains(_ latLng: LatLng) -> Bool { + if !latLng.isValid() { + return false + } + return lat.contains(latLng.lat) && lng.contains(latLng.lng) + } + + // ContainsPoint reports whether the given Point is within the Rect. + func contains(_ point: S2Point) -> Bool { + return contains(LatLng(point: point)) + } + + // intersectsLatEdge reports if the edge AB intersects the given edge of constant + // latitude. Requires the points to have unit length. + func intersectsLatEdge(a: S2Point, b: S2Point, lat: Double, lng: S1Interval) -> 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. + var z = a.pointCross(b) + if z.z < 0 { + let zv = z.v.mul(-1) + z = S2Point(raw: zv) + } + + // Extend this to an orthonormal frame (x,y,z) where x is the direction + // where the great circle through AB achieves its maximium latitude. + let y = z.pointCross(S2Point(x: 0, y: 0, z: 1)) + let x = S2Point(raw: y.v.cross(z.v)) + + // 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. + let sinLat = sin(lat) + if fabs(sinLat) >= x.z { + // The great circle does not reach the given latitude. + return false + } + + let cosTheta = sinLat / x.z + let sinTheta = sqrt(1.0 - cosTheta * cosTheta) + let theta = 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. + let abTheta = S1Interval(lo_endpoint: atan2(a.v.dot(y.v), a.v.dot(x.v)), hi_endpoint: atan2(b.v.dot(y.v), b.v.dot(x.v))) + + if abTheta.contains(theta) { + // Check if the intersection point is also in the given lng interval. + let isect = x.v.mul(cosTheta).add(y.v.mul(sinTheta)) + if lng.contains(atan2(isect.y, isect.x)) { + return true + } + } + + if abTheta.contains(-theta) { + // Check if the other intersection point is also in the given lng interval. + let isect = x.v.mul(cosTheta).sub(y.v.mul(sinTheta)) + if lng.contains(atan2(isect.y, isect.x)) { + return true + } + } + return false + } + + // intersectsLngEdge reports if the edge AB intersects the given edge of constant + // longitude. Requires the points to have unit length. + func intersectsLngEdge(a: S2Point, b: S2Point, lat: R1Interval, lng: Double) -> Bool { + // The nice thing about edges of constant longitude is that + // they are straight lines on the sphere (geodesics). + let c = LatLng(lat: lat.lo, lng: lng).toPoint() + let d = LatLng(lat: lat.hi, lng: lng).toPoint() + return simpleCrossing(a, b: b, c: c, d: d) + } + + // IntersectsCell reports whether this rectangle intersects the given cell. This is an + // exact test and may be fairly expensive. + func intersects(_ cell: Cell) -> Bool { + // First we eliminate the cases where one region completely contains the + // rect. Once these are disposed of, then the regions will intersect + // if and only if their boundaries intersect. + if isEmpty() { + return false + } + if contains(cell.id.point()) { + return true + } + if cell.contains(center().toPoint()) { + return true + } + // Quick rejection test (not required for correctness). + if !intersects(cell.rectBound()) { + return false + } + // Precompute the cell vertices as points and latitude-longitudes. We also + // check whether the Cell contains any corner of the rectangle, or + // vice-versa, since the edge-crossing tests only check the edge interiors. + var vertices = [S2Point]() // 4 + var latlngs = [LatLng]() // 4 + // + for i in 0.. S2Cap + + // RectBound returns a bounding latitude-longitude rectangle that contains + // the region. The bounds are not guaranteed to be tight. + func rectBound() -> S2Rect + + // ContainsCell reports whether the region completely contains the given region. + // It returns false if containment could not be determined. + func contains(_ cell: Cell) -> Bool + + // IntersectsCell reports whether the region intersects the given cell or + // if intersection could not be determined. It returns false if the region + // does not intersect. + func intersects(_ cell: Cell) -> Bool +} + +// Enforce interface satisfaction. +//extension Cell: S2Region {} +//extension CellUnion: S2Region {} +//extension S2Cap: S2Region {} +//extension S2Rect: S2Region {} +//extension S2Loop: S2Region {} +//extension S2Polygon: S2Region {} diff --git a/Sphere2Go/S2RegionCoverer.swift b/Sphere2Go/S2RegionCoverer.swift new file mode 100644 index 0000000..23a0711 --- /dev/null +++ b/Sphere2Go/S2RegionCoverer.swift @@ -0,0 +1,471 @@ +// +// S2RegionCoverer.swift +// Sphere2 +// + +import Foundation + +// package s2 +// import heap +// BUG(akashagrawal): The differences from the C++ version FloodFill, SimpleCovering + +// RegionCoverer allows arbitrary regions to be approximated as unions of cells (CellUnion). +// This is useful for implementing various sorts of search and precomputation operations. +// +// Typical usage: +// +// rc := &s2.RegionCoverer{MaxLevel: 30, MaxCells: 5} +// r := s2.Region(CapFromCenterArea(center, area)) +// covering := rc.Covering(r) +// +// This yields a CellUnion of at most 5 cells that is guaranteed to cover the +// given region (a disc-shaped region on the sphere). +// +// For covering, only cells where (level - MinLevel) is a multiple of LevelMod will be used. +// This effectively allows the branching factor of the S2 CellID hierarchy to be increased. +// Currently the only parameter values allowed are 0/1, 2, or 3, corresponding to +// branching factors of 4, 16, and 64 respectively. +// +// Note the following: +// +// - MinLevel takes priority over MaxCells, i.e. cells below the given level will +// never be used even if this causes a large number of cells to be returned. +// +// - For any setting of MaxCells, up to 6 cells may be returned if that +// is the minimum number of cells required (e.g. if the region intersects +// all six face cells). Up to 3 cells may be returned even for very tiny +// convex regions if they happen to be located at the intersection of +// three cube faces. +// +// - For any setting of MaxCells, an arbitrary number of cells may be +// returned if MinLevel is too high for the region being approximated. +// +// - If MaxCells is less than 4, the area of the covering may be +// arbitrarily large compared to the area of the original region even if +// the region is convex (e.g. a Cap or Rect). +// +// The approximation algorithm is not optimal but does a pretty good job in +// practice. The output does not always use the maximum number of cells +// allowed, both because this would not always yield a better approximation, +// and because MaxCells is a limit on how much work is done exploring the +// possible covering as well as a limit on the final output size. +// +// Because it is an approximation algorithm, one should not rely on the +// stability of the output. In particular, the output of the covering algorithm +// may change across different versions of the library. +// +// One can also generate interior coverings, which are sets of cells which +// are entirely contained within a region. Interior coverings can be +// empty, even for non-empty regions, if there are no cells that satisfy +// the provided constraints and are contained by the region. Note that for +// performance reasons, it is wise to specify a MaxLevel when computing +// interior coverings - otherwise for regions with small or zero area, the +// algorithm may spend a lot of time subdividing cells all the way to leaf +// level to try to find contained cells. +struct RegionCoverer { + + // MARK parameters + + let minLevel: Int // the minimum cell level to be used. + let maxLevel: Int // the maximum cell level to be used. + let levelMod: Int // the LevelMod to be used. + let maxCells: Int // the maximum desired number of cells in the approximation. + + // MARK: coverer factory + + // newCoverer returns an instance of coverer. + func newCoverer() -> Coverer { + return Coverer( + minLevel: max(0, min(CellId.maxLevel, minLevel)), // clamped + maxLevel: max(0, min(CellId.maxLevel, maxLevel)), // clamped + levelMod: max(1, min(3, levelMod)), // clamped + maxCells: maxCells) + } + + // MARK: region to cell union + + // Covering returns a CellUnion that covers the given region and satisfies the various restrictions. + func covering(region: S2Region) -> CellUnion { + let covering = cellUnion(region: region) + covering.denormalize(minLevel: max(0, min(maxLevel, minLevel)), levelMod: max(1, min(3, levelMod))) + return covering + } + + // InteriorCovering returns a CellUnion that is contained within the given region and satisfies the various restrictions. + func interiorCovering(region: S2Region) -> CellUnion { + let covering = interiorCellUnion(region: region) + covering.denormalize(minLevel: max(0, min(maxLevel, minLevel)), levelMod: max(1, min(3, levelMod))) + return covering + } + + // CellUnion returns a normalized CellUnion that covers the given region and + // satisfies the restrictions except for minLevel and levelMod. These criteria + // cannot be satisfied using a cell union because cell unions are + // automatically normalized by replacing four child cells with their parent + // whenever possible. (Note that the list of cell ids passed to the CellUnion + // constructor does in fact satisfy all the given restrictions.) + func cellUnion(region: S2Region) -> CellUnion { + let coverer = newCoverer() + coverer.coveringInternal(region: region) + let union = coverer.result + union.normalize() + return union + } + + // InteriorCellUnion returns a normalized CellUnion that is contained within the given region and + // satisfies the restrictions except for minLevel and levelMod. These criteria + // cannot be satisfied using a cell union because cell unions are + // automatically normalized by replacing four child cells with their parent + // whenever possible. (Note that the list of cell ids passed to the CellUnion + // constructor does in fact satisfy all the given restrictions.) + func interiorCellUnion(region: S2Region) -> CellUnion { + let coverer = newCoverer() + coverer.interiorCovering = true + coverer.coveringInternal(region: region) + let union = coverer.result + union.normalize() + return union + } + + // FastCovering returns a CellUnion that covers the given region similar to Covering, + // except that this method is much faster and the coverings are not as tight. + // All of the usual parameters are respected (MaxCells, MinLevel, MaxLevel, and LevelMod), + // except that the implementation makes no attempt to take advantage of large values of + // MaxCells. (A small number of cells will always be returned.) + // + // This function is useful as a starting point for algorithms that + // recursively subdivide cells. + func fastCovering(cap: S2Cap) -> CellUnion { + let coverer = newCoverer() + let union = coverer.rawFastCovering(cap: cap) + return coverer.normalize(covering: union) + } + +} + + +// Nodes of a tree that dscribes the result +class Candidate: Comparable { + + let cell: Cell + // Cell should not be expanded further + var terminal: Bool + // Number of children that intersect the region + var numChildren = 0 + // Actual size may be 0, 4, 16, or 64 elements + var children = [Candidate]() + // Priority of the candiate + var priority = 0 + + // MARK: inits + + init(cell: Cell, terminal: Bool) { + self.cell = cell + self.terminal = terminal + } + + // MARK: protocols + + static func ==(lhs: Candidate, rhs: Candidate) -> Bool { + return lhs.priority == rhs.priority + } + + static func <(lhs: Candidate, rhs: Candidate) -> Bool { + return lhs.priority < rhs.priority + } + + // MARK: methods + + func addChild(_ child: Candidate) { + children.append(child) + numChildren += 1 + } + +} + + +class Coverer { + + // MARK: parameters + + let minLevel: Int // the minimum cell level to be used. + let maxLevel: Int // the maximum cell level to be used. + let levelMod: Int // the LevelMod to be used. + let maxCells: Int // the maximum desired number of cells in the approximation. + + // MARK: working variables + + var region: S2Region? + var result: CellUnion + var pq: PriorityQueue + var interiorCovering: Bool + + // MARK: inits + + init(minLevel: Int, maxLevel: Int, levelMod: Int, maxCells: Int) { + self.minLevel = minLevel + self.maxLevel = maxLevel + self.levelMod = levelMod + self.maxCells = maxCells + // + result = CellUnion(ids: []) + pq = PriorityQueue() + interiorCovering = false + } + + // MARK: algorithms + + // newCandidate returns a new candidate with no children if the cell intersects the given region. + // The candidate is marked as terminal if it should not be expanded further. + func newCandidate(cell: Cell) -> Candidate? { + guard let region = region else { return nil } + if !region.intersects(cell) { + return nil + } + var terminal = false + let level = Int(cell.level) + if level >= minLevel { + if interiorCovering { + if region.contains(cell) { + terminal = true + } else if level + levelMod > maxLevel { + return nil + } + } else if level + levelMod > maxLevel || region.contains(cell) { + terminal = true + } + } + return Candidate(cell: cell, terminal: terminal) + } + + // expandChildren populates the children of the candidate by expanding the given number of + // levels from the given cell. Returns the number of children that were marked "terminal". + func expandChildren(candidate: Candidate, cell: Cell, numLevels: Int) -> Int { + guard let region = region else { return 0 } + let numLevels = numLevels - 1 + var numTerminals = 0 + let last = cell.id.childEnd() + var ci = cell.id.childBegin() + while ci.id != last.id { + let childCell = Cell(id: ci) + if numLevels > 0 { + if region.intersects(childCell) { + numTerminals += expandChildren(candidate: candidate, cell: childCell, numLevels: numLevels) + } + ci = ci.next() + continue + } + let child = newCandidate(cell: childCell) + if let child = child { + candidate.addChild(child) + if child.terminal { + numTerminals += 1 + } + } + ci = ci.next() + } + return numTerminals + } + + // addCandidate adds the given candidate to the result if it is marked as "terminal", + // otherwise expands its children and inserts it into the priority queue. + // Passing an argument of nil does nothing. + func add(_ candidate: Candidate) { + if candidate.terminal { + result.add(candidate.cell.id) + return + } + // Expand one level at a time until we hit minLevel to ensure that we don't skip over it. + var numLevels = levelMod + let level = Int(candidate.cell.level) + if level < minLevel { + numLevels = 1 + } + let numTerminals = expandChildren(candidate: candidate, cell: candidate.cell, numLevels: numLevels) + let maxChildrenShift = 2 * levelMod + if candidate.numChildren == 0 { + return + } else if !interiorCovering && numTerminals == 1 << maxChildrenShift && level > minLevel { + // Optimization: add the parent cell rather than all of its children. + // We can't do this for interior coverings, since the children just + // intersect the region, but may not be contained by it - we need to + // subdivide them further. + candidate.terminal = true + add(candidate) + } else { + // We negate the priority so that smaller absolute priorities are returned + // first. The heuristic is designed to refine the largest cells first, + // since those are where we have the largest potential gain. Among cells + // of the same size, we prefer the cells with the fewest children. + // Finally, among cells with equal numbers of children we prefer those + // with the smallest number of children that cannot be refined further. + candidate.priority = -((level< Int { + if levelMod > 1 && level > minLevel { + return level - (level - minLevel) % levelMod + } + return level + } + + // adjustCellLevels ensures that all cells with level > minLevel also satisfy levelMod, + // by replacing them with an ancestor if necessary. Cell levels smaller + // than minLevel are not modified (see AdjustLevel). The output is + // then normalized to ensure that no redundant cells are present. + func adjustCellLevels(_ cells: CellUnion) -> CellUnion { + if levelMod == 1 { + return cells + } + var out = 0 + for ci in cells.cellIds { + let level = ci.level() + let newLevel = adjustLevel(level) + var ci = ci + if newLevel != level { + ci = ci.parent(newLevel) + } + if out > 0 && cells.cellIds[out-1].contains(ci) { + continue + } + while out > 0 && ci.contains(cells.cellIds[out-1]) { + out -= 1 + } + cells.cellIds[out] = ci + out += 1 + } + return cells.truncate(out) + } + + // initialCandidates computes a set of initial candidates that cover the given region. + func initialCandidates() { + // Optimization: start with a small (usually 4 cell) covering of the region's bounding cap. + let temp = RegionCoverer(minLevel: 0, maxLevel: maxLevel, levelMod: 1, maxCells: min(4, maxCells)) + guard let region = region else { return } + var cells = temp.fastCovering(cap: region.capBound()) + cells = adjustCellLevels(cells) + for ci in cells.cellIds { + if let candidate = newCandidate(cell: Cell(id: ci)) { + add(candidate) + } + } + } + + // coveringInternal generates a covering and stores it in result. + // Strategy: Start with the 6 faces of the cube. Discard any + // that do not intersect the shape. Then repeatedly choose the + // largest cell that intersects the shape and subdivide it. + // + // result contains the cells that will be part of the output, while pq + // contains cells that we may still subdivide further. Cells that are + // entirely contained within the region are immediately added to the output, + // while cells that do not intersect the region are immediately discarded. + // Therefore pq only contains cells that partially intersect the region. + // Candidates are prioritized first according to cell size (larger cells + // first), then by the number of intersecting children they have (fewest + // children first), and then by the number of fully contained children + // (fewest children first). + func coveringInternal(region: S2Region) { + self.region = region + initialCandidates() + while pq.count > 0 && (!interiorCovering || result.count < maxCells) { + guard let candidate = pq.pop() else { continue } + // For interior covering we keep subdividing no matter how many children + // candidate has. If we reach MaxCells before expanding all children, + // we will just use some of them. + // For exterior covering we cannot do this, because result has to cover the + // whole region, so all children have to be used. + // candidate.numChildren == 1 case takes care of the situation when we + // already have more then MaxCells in result (minLevel is too high). + // Subdividing of the candidate with one child does no harm in this case. + if interiorCovering || Int(candidate.cell.level) < minLevel || candidate.numChildren == 1 || result.count + pq.count + candidate.numChildren <= maxCells { + for child in candidate.children { + if !interiorCovering || result.count < maxCells { + add(child) + } + } + } else { + candidate.terminal = true + add(candidate) + } + } + pq = PriorityQueue() + self.region = nil + } + + // rawFastCovering computes a covering of the given cap. In general the covering consists of + // at most 4 cells (except for very large caps, which may need up to 6 cells). + // The output is not sorted. + func rawFastCovering(cap: S2Cap) -> CellUnion { + let covering = CellUnion(ids: []) + // Find the maximum level such that the cap contains at most one cell vertex + // and such that CellId.VertexNeighbors() can be called. + let level = min(Metric.minWidth.maxLevel(2 * cap.radius()), CellId.maxLevel-1) + if level == 0 { + for face in 0..<6 { + covering.add(CellId(face: face)) + } + } else { + for vn in CellId(point: cap.center).vertexNeighbors(level) { + covering.add(vn) + } + } + return covering + } + + // normalizeCovering normalizes the "covering" so that it conforms to the current covering + // parameters (MaxCells, minLevel, maxLevel, and levelMod). + // This method makes no attempt to be optimal. In particular, if + // minLevel > 0 or levelMod > 1 then it may return more than the + // desired number of cells even when this isn't necessary. + // + // Note that when the covering parameters have their default values, almost + // all of the code in this function is skipped. + func normalize(covering: CellUnion) -> CellUnion { + // If any cells are too small, or don't satisfy levelMod, then replace them with ancestors. + if maxLevel < CellId.maxLevel || levelMod > 1 { + for i in 0.. maxCells { + var bestIndex = -1 + var bestLevel = -1 + for i in 0.. bestLevel { + bestLevel = level + bestIndex = i + } + } + if bestLevel < minLevel { + break + } + covering[bestIndex] = covering[bestIndex].parent(bestLevel) + covering.normalize() + } + // Make sure that the covering satisfies minLevel and levelMod, + // possibly at the expense of satisfying MaxCells. + if minLevel > 0 || levelMod > 1 { + covering.denormalize(minLevel: minLevel, levelMod: levelMod) + } + return covering + } + +} diff --git a/Sphere2Go/S2Shape.swift b/Sphere2Go/S2Shape.swift new file mode 100644 index 0000000..32c95c0 --- /dev/null +++ b/Sphere2Go/S2Shape.swift @@ -0,0 +1,110 @@ +// +// S2ShapeIndex.swift +// Sphere2 +// + +import Foundation + +// package s2 + + +// dimension defines the types of geometry dimensions that a Shape supports. +enum Dimension: Int { + case pointGeometry + case polylineGeometry + case polygonGeometry +} + +// Shape defines an interface for any s2 type that needs to be indexable. +protocol Shape { + + // NumEdges returns the number of edges in this shape. + func numEdges() -> Int + + // Edge returns endpoints for the given edge index. + func edge(_ i: Int) -> (S2Point, S2Point) + + // HasInterior returns true if this shape has an interior. + // i.e. the Shape consists of one or more closed non-intersecting loops. + func hasInterior() -> Bool + + // ContainsOrigin returns true if this shape contains s2.Origin. + // Shapes that do not have an interior will return false. + func containsOrigin() -> Bool +} + +//func ==(lhs: Shape, rhs: Shape) -> Bool { +// return lhs.numEdges() == rhs.numEdges() +//} + +// CellRelation describes the possible relationships between a target cell +// and the cells of the ShapeIndex. If the target is an index cell or is +// contained by an index cell, it is Indexed. If the target is subdivided +// into one or more index cells, it is Subdivided. Otherwise it is Disjoint. +enum CellRelation: Int { + // The possible CellRelations for a ShapeIndex. + case indexed = 0 + case subdivided = 1 + case disjoint = 2 +} + +// cellPadding defines the total error when clipping an edge which comes +// from two sources: +// (1) Clipping the original spherical edge to a cube face (the face edge). +// The maximum error in this step is faceClipErrorUVCoord. +// (2) Clipping the face edge to the u- or v-coordinate of a cell boundary. +// The maximum error in this step is edgeClipErrorUVCoord. +// Finally, since we encounter the same errors when clipping query edges, we +// double the total error so that we only need to pad edges during indexing +// and not at query time. +let cellPadding = 2.0 * (faceClipErrorUVCoord + edgeClipErrorUVCoord) + + +// ShapeIndex indexes a set of Shapes, where a Shape is some collection of +// edges. A shape can be as simple as a single edge, or as complex as a set of loops. +// For Shapes that have interiors, the index makes it very fast to determine which +// Shape(s) that contain a given point or region. +class ShapeIndex { + // shapes contains all the shapes in this index, accessible by their shape id. + // Removed shapes are replaced by nil. + // + // TODO(roberts): Is there a better storage structure to use? C++ uses a btree + // deep down for the index. There do appear to be a number of Go BTree + // implementations available that may be suitable. Further investigation + // is needed before selecting an appropriate option. + // + // The slice is an interim storage solution to get the index up and usable. + var shapes = [Shape]() + + let maxEdgesPerCell: Int + + init() { + maxEdgesPerCell = 10 + } + + // Add adds the given shape to the index and assign a unique id to the shape. + // Shape ids are assigned sequentially starting from 0 in the order shapes are added. + func add(_ shape: Shape) { + shapes.append(shape) + } + + // Len reports the number of Shapes in this index. + var count: Int { + return shapes.count + } + + // At returns the shape with the given index. If the given index is not valid, nil is returned. + func at(_ i: Int) -> Shape { + // TODO(roberts): This blindly assumes that no Shapes have been removed and + // that the slice has no holes in it. As this gets implemented, change this + // to be smarter and safer about verifying existence before returning it. + return shapes[i] + } + + // Reset clears the contents of the index and resets it to its original state. + // Any options specified via Init are preserved. + func reset() { + shapes = [] + } + +} diff --git a/Sphere2GoLib/Info.plist b/Sphere2GoLib/Info.plist new file mode 100644 index 0000000..d3de8ee --- /dev/null +++ b/Sphere2GoLib/Info.plist @@ -0,0 +1,26 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + $(CURRENT_PROJECT_VERSION) + NSPrincipalClass + + + diff --git a/Sphere2GoLib/Sphere2Lib.h b/Sphere2GoLib/Sphere2Lib.h new file mode 100644 index 0000000..615acce --- /dev/null +++ b/Sphere2GoLib/Sphere2Lib.h @@ -0,0 +1,17 @@ +// +// Sphere2Lib.h +// Sphere2 +// + + +#import + +//! Project version number for Sphere2Lib. +FOUNDATION_EXPORT double Sphere2LibVersionNumber; + +//! Project version string for Sphere2Lib. +FOUNDATION_EXPORT const unsigned char Sphere2LibVersionString[]; + +// In this header, you should import all the public headers of your framework using statements like #import + + diff --git a/Sphere2GoLibTests/Info.plist b/Sphere2GoLibTests/Info.plist new file mode 100644 index 0000000..ba72822 --- /dev/null +++ b/Sphere2GoLibTests/Info.plist @@ -0,0 +1,24 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + BNDL + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1 + + diff --git a/Sphere2GoLibTests/R1IntervalTests.swift b/Sphere2GoLibTests/R1IntervalTests.swift new file mode 100644 index 0000000..4cf4e4a --- /dev/null +++ b/Sphere2GoLibTests/R1IntervalTests.swift @@ -0,0 +1,125 @@ +// +// R1IntervalTests.swift +// Sphere2 +// + +import XCTest + + +class R1IntervalTests: XCTestCase { + + // Some standard intervals for use throughout the tests. + let unit = R1Interval(lo: 0, hi: 1) + let negunit = R1Interval(lo: -1, hi: 0) + let half = R1Interval(lo: 0.5, hi: 0.5) + let empty = R1Interval.empty + let zero = R1Interval(lo: 0, hi: 0) + + override func setUp() { + super.setUp() + // Put setup code here. This method is called before the invocation of each test method in the class. + } + + override func tearDown() { + // Put teardown code here. This method is called after the invocation of each test method in the class. + super.tearDown() + } + + func i(_ lo: Double, _ hi: Double) -> R1Interval { + return R1Interval(lo: lo, hi: hi) + } + + func testIsEmpty() { + XCTAssertFalse(unit.isEmpty()) + XCTAssertFalse(half.isEmpty()) + XCTAssertTrue(empty.isEmpty()) + XCTAssertFalse(zero.isEmpty()) + } + + func testCenter() { + XCTAssertEqual(unit.center(), 0.5) + XCTAssertEqual(negunit.center(), -0.5) + XCTAssertEqual(half.center(), 0.5) + } + + func testLength() { + XCTAssertEqual(unit.length(), 1.0) + XCTAssertEqual(negunit.length(), 1.0) + XCTAssertEqual(half.length(), 0.0) + XCTAssert(empty.length() < 0.0) + } + + // TODO(dsymonds): Tests for Contains, InteriorContains, ContainsInterval, InteriorContainsInterval, Intersects, InteriorIntersects + + func testIntersection() { + XCTAssertEqual(unit.intersection(half), half) + XCTAssertEqual(unit.intersection(negunit), zero) + XCTAssertEqual(negunit.intersection(half), empty) + XCTAssertEqual(unit.intersection(empty), empty) + XCTAssertEqual(empty.intersection(unit), empty) + } + + func testUnion() { + XCTAssertEqual(i(99, 100).union(empty), i(99, 100)) + XCTAssertEqual(empty.union(i(99, 100)), i(99, 100)) + XCTAssertEqual(i(5, 3).union(i(0, -2)), empty) + XCTAssertEqual(i(0, -2).union(i(5, 3)), empty) + XCTAssertEqual(unit.union(unit), unit) + XCTAssertEqual(unit.union(negunit), i(-1, 1)) + XCTAssertEqual(negunit.union(unit), i(-1, 1)) + XCTAssertEqual(half.union(unit), unit) + } + + func testAddPoint() { + XCTAssertEqual(empty.add(5.0), i(5, 5)) + XCTAssertEqual(i(5, 5).add(-1.0), i(-1, 5)) + XCTAssertEqual(i(-1, 5).add(0.0), i(-1, 5)) + XCTAssertEqual(i(-1, 5).add(6.0), i(-1, 6)) + } + + func testClampPoint() { + XCTAssertEqual(i(0.1, 0.4).clamp(0.3), 0.3) + XCTAssertEqual(i(0.1, 0.4).clamp(-7.0), 0.1) + XCTAssertEqual(i(0.1, 0.4).clamp(0.6), 0.4) + } + + func testExpanded() { + XCTAssertEqual(empty.expanded(0.45), empty) + XCTAssertEqual(unit.expanded(0.5), i(-0.5, 1.5)) + XCTAssertEqual(unit.expanded(-0.5), i(0.5, 0.5)) + XCTAssertEqual(unit.expanded(-0.51), empty) + } + + func testIntervalString() { + XCTAssertEqual(i(2, 4.5).description, "[2.0000000, 4.5000000]") + } + + func testApproxEqual() { + let epsilon = R1Interval.epsilon + // empty intervals + + XCTAssertTrue(empty.approxEquals(empty)) + XCTAssertTrue(zero.approxEquals(empty)) + XCTAssertTrue(empty.approxEquals(zero)) + XCTAssertTrue(i(1, 1).approxEquals(empty)) + XCTAssertTrue(empty.approxEquals(i(1, 1))) + XCTAssertFalse(empty.approxEquals(i(0, 1))) + XCTAssertTrue(empty.approxEquals(i(1, 1 + 2*epsilon))) + // singleton intervals + XCTAssertTrue(i(1, 1).approxEquals(i(1, 1))) + XCTAssertTrue(i(1, 1).approxEquals(i(1 - epsilon, 1 - epsilon))) + XCTAssertTrue(i(1, 1).approxEquals(i(1 + epsilon, 1 + epsilon))) + XCTAssertFalse(i(1, 1).approxEquals(i(1 - 3*epsilon, 1))) + XCTAssertFalse(i(1, 1).approxEquals(i(1, 1 + 3*epsilon))) + XCTAssertTrue(i(1, 1).approxEquals(i(1 - epsilon, 1 + epsilon))) + XCTAssertFalse(zero.approxEquals(i(1, 1))) + // other intervals + XCTAssertFalse(i(1 - epsilon, 2 + epsilon).approxEquals(i(1, 2))) + XCTAssertTrue(i(1 + epsilon, 2 - epsilon).approxEquals(i(1, 2))) + XCTAssertFalse(i(1 - 3*epsilon, 2 + epsilon).approxEquals(i(1, 2))) + XCTAssertFalse(i(1 + 3*epsilon, 2 - epsilon).approxEquals(i(1, 2))) + XCTAssertFalse(i(1 - epsilon, 2 + 3*epsilon).approxEquals(i(1, 2))) + XCTAssertFalse(i(1 + epsilon, 2 - 3*epsilon).approxEquals(i(1, 2))) + } + +} diff --git a/Sphere2GoLibTests/R2RectTests.swift b/Sphere2GoLibTests/R2RectTests.swift new file mode 100644 index 0000000..343cece --- /dev/null +++ b/Sphere2GoLibTests/R2RectTests.swift @@ -0,0 +1,154 @@ +// +// R2RectTests.swift +// Sphere2 +// + + +import XCTest + +// package r2 +// import reflect, testing, r1 + + +class R2RectTests: XCTestCase { + + let sw = R2Point(x: 0, y: 0.25) + let se = R2Point(x: 0.5, y: 0.25) + let ne = R2Point(x: 0.5, y: 0.75) + let nw = R2Point(x: 0, y: 0.75) + + let empty = R2Rect.empty + let rect = R2Rect(p0: R2Point(x: 0, y: 0.25), p1: R2Point(x: 0.5, y: 0.75)) + let rectMid = R2Rect(p0: R2Point(x: 0.25, y: 0.5), p1: R2Point(x: 0.25, y: 0.5)) + let rectSW = R2Rect(p0: R2Point(x: 0, y: 0.25), p1: R2Point(x: 0, y: 0.25)) + let rectNE = R2Rect(p0: R2Point(x: 0.5, y: 0.75), p1: R2Point(x: 0.5, y: 0.75)) + + override func setUp() { + super.setUp() + // Put setup code here. This method is called before the invocation of each test method in the class. + } + + override func tearDown() { + // Put teardown code here. This method is called after the invocation of each test method in the class. + super.tearDown() + } + + func p(_ x: Double, _ y: Double) -> R2Point { + return R2Point(x: x, y: y) + } + + func r(_ p0: R2Point, _ p1: R2Point) -> R2Rect { + return R2Rect(p0: p0, p1: p1) + } + + func testEmptyRect() { + XCTAssertTrue(empty.isValid()) + XCTAssertTrue(empty.isEmpty()) + } + + func testFromVariousTypes() { + let d1 = r(p(0.1, 0), p(0.25, 1)) + XCTAssert(R2Rect(center: p(0.3, 0.5), size: p(0.2, 0.4)).approxEquals(r(p(0.2, 0.3), p(0.4, 0.7)))) + XCTAssert(R2Rect(center: p(1, 0.1), size: p(0, 2)).approxEquals(r(p(1, -0.9), p(1, 1.1)))) + XCTAssert(d1.approxEquals(R2Rect(x: d1.x, y: d1.y))) + // TODO hi before lo case in constructor +// XCTAssert(r(p(0.15, 0.3), p(0.35, 0.9)).approxEquals(r(p(0.15, 0.9), p(0.35, 0.3)))) +// XCTAssert(r(p(0.12, 0), p(0.83, 0.5)).approxEquals(r(p(0.83, 0), p(0.12, 0.5)))) + } + + func testCenter() { + XCTAssertEqual(empty.center(), p(0.5, 0.5)) + XCTAssertEqual(rect.center(), p(0.25, 0.5)) + } + + func testVertices() { + let v = rect.vertices() + XCTAssertEqual(v[0], sw) + XCTAssertEqual(v[1], se) + XCTAssertEqual(v[2], ne) + XCTAssertEqual(v[3], nw) + } + + func testContainsPoint() { + XCTAssertTrue(rect.contains(p(0.2, 0.4))) + XCTAssertFalse(rect.contains(p(0.2, 0.8))) + XCTAssertFalse(rect.contains(p(-0.1, 0.4))) + XCTAssertFalse(rect.contains(p(0.6, 0.1))) + XCTAssertTrue(rect.contains(p(rect.x.lo, rect.y.lo))) + XCTAssertTrue(rect.contains(p(rect.x.hi, rect.y.hi))) + } + + func testInteriorContainsPoint() { + // Check corners are not contained. + XCTAssertFalse(rect.interiorContains(sw)) + XCTAssertFalse(rect.interiorContains(ne)) + // Check a point on the border is not contained. + XCTAssertFalse(rect.interiorContains(p(0, 0.5))) + XCTAssertFalse(rect.interiorContains(p(0.25, 0.25))) + XCTAssertFalse(rect.interiorContains(p(0.5, 0.5))) + // Check points inside are contained. + XCTAssertTrue(rect.interiorContains(p(0.125, 0.6))) + } + + func testIntervalOps() { + let tests = [ + (rect, rectMid, true, true, true, true, rect, rectMid), + (rect, rectSW, true, false, true, false, rect, rectSW), + (rect, rectNE, true, false, true, false, rect, rectNE), + (rect, r(p(0.45, 0.1), p(0.75, 0.3)), false, false, true, true, r(p(0, 0.1), p(0.75, 0.75)), r(p(0.45, 0.25), p(0.5, 0.3))), + (rect, r(p(0.5, 0.1), p(0.7, 0.3)), false, false, true, false, r(p(0, 0.1), p(0.7, 0.75)), r(p(0.5, 0.25), p(0.5, 0.3))), + (rect, r(p(0.45, 0.1), p(0.7, 0.25)), false, false, true, false, r(p(0, 0.1), p(0.7, 0.75)), r(p(0.45, 0.25), p(0.5, 0.25))), + (r(p(0.1, 0.2), p(0.1, 0.3)), r(p(0.15, 0.7), p(0.2, 0.8)), false, false, false, false, r(p(0.1, 0.2), p(0.2, 0.8)), R2Rect.empty), + // Check that the intersection of two rectangles that overlap in x but not y is valid, and vice versa. + (r(p(0.1, 0.2), p(0.4, 0.5)), r(p(0, 0), p(0.2, 0.1)), false, false, false, false, r(p(0, 0), p(0.4, 0.5)), R2Rect.empty), + (r(p(0, 0), p(0.1, 0.3)), r(p(0.2, 0.1), p(0.3, 0.4)), false, false, false, false, r(p(0, 0), p(0.3, 0.4)), R2Rect.empty)] + + for (r1, r2, contains, intContains, intersects, intIntersects, wantUnion, wantIntersection) in tests { + XCTAssertEqual(r1.contains(r2), contains) + XCTAssertEqual(r1.interiorContains(r2), intContains) + XCTAssertEqual(r1.intersects(r2), intersects) + XCTAssertEqual(r1.interiorIntersects(r2), intIntersects) + XCTAssertEqual(r1.union(r2).approxEquals(r1), r1.contains(r2)) + XCTAssertEqual(!r1.intersection(r2).isEmpty(), r1.intersects(r2)) + XCTAssertEqual(r1.union(r2), wantUnion) + XCTAssertEqual(r1.intersection(r2), wantIntersection) + XCTAssertEqual(r1.add(r2), wantUnion) + } + } + + func testAddPoint() { + var r2 = R2Rect.empty + r2 = r2.add(sw) + r2 = r2.add(se) + r2 = r2.add(nw) + r2 = r2.add(p(0.1, 0.4)) + XCTAssertTrue(rect.approxEquals(r2)) + } + + func testClampPoint() { + let r = R2Rect(x: R1Interval(lo: 0, hi: 0.5), y: R1Interval(lo: 0.25, hi: 0.75)) + XCTAssertEqual(r.clamp(p(-0.01, 0.24)), p(0, 0.25)) + XCTAssertEqual(r.clamp(p(-5.0, 0.48)), p(0, 0.48)) + XCTAssertEqual(r.clamp(p(-5.0, 2.48)), p(0, 0.75)) + XCTAssertEqual(r.clamp(p(0.19, 2.48)), p(0.19, 0.75)) + XCTAssertEqual(r.clamp(p(6.19, 2.48)), p(0.5, 0.75)) + XCTAssertEqual(r.clamp(p(6.19, 0.53)), p(0.5, 0.53)) + XCTAssertEqual(r.clamp(p(6.19, -2.53)), p(0.5, 0.25)) + XCTAssertEqual(r.clamp(p(0.33, -2.53)), p(0.33, 0.25)) + XCTAssertEqual(r.clamp(p(0.33, 0.37)), p(0.33, 0.37)) + } + + func testExpandedEmpty() { + XCTAssertTrue(R2Rect.empty.expanded(p(0.1, 0.3)).isEmpty()) + XCTAssertTrue(R2Rect.empty.expanded(p(-0.1, -0.3)).isEmpty()) + XCTAssertTrue(r(p(0.2, 0.4), p(0.3, 0.7)).expanded(p(-0.1, 0.3)).isEmpty()) + XCTAssertTrue(r(p(0.2, 0.4), p(0.3, 0.7)).expanded(p(0.1, -0.2)).isEmpty()) + } + + func testExpandedEquals() { + XCTAssertTrue(r(p(0.2, 0.4), p(0.3, 0.7)).expanded(p(0.1, 0.3)).approxEquals(r(p(0.1, 0.1), p(0.4, 1.0)))) + XCTAssertTrue(r(p(0.2, 0.4), p(0.3, 0.7)).expanded(p(0.1, -0.1)).approxEquals(r(p(0.1, 0.5), p(0.4, 0.6)))) + XCTAssertTrue(r(p(0.2, 0.4), p(0.3, 0.7)).expanded(p(0.1, 0.1)).approxEquals(r(p(0.1, 0.3), p(0.4, 0.8)))) + } + +} diff --git a/Sphere2GoLibTests/R3VectorTests.swift b/Sphere2GoLibTests/R3VectorTests.swift new file mode 100644 index 0000000..3b03073 --- /dev/null +++ b/Sphere2GoLibTests/R3VectorTests.swift @@ -0,0 +1,194 @@ +// +// R3VectorTests.swift +// Sphere2 +// + +import XCTest + +class R3R3VectorTests: XCTestCase { + + override func setUp() { + super.setUp() + // Put setup code here. This method is called before the invocation of each test method in the class. + } + + override func tearDown() { + // Put teardown code here. This method is called after the invocation of each test method in the class. + super.tearDown() + } + + func v(_ x: Double, _ y: Double, _ z: Double) -> R3Vector { + return R3Vector(x: x, y: y, z: z) + } + + func testNorm() { + XCTAssertEqual(v(0, 0, 0).norm(), 0.0, accuracy: 1e-14) + XCTAssertEqual(v(0, 1, 0).norm(), 1.0, accuracy: 1e-14) + XCTAssertEqual(v(3, -4, 12).norm(), 13.0, accuracy: 1e-14) + XCTAssertEqual(v(1, 1e-16, 1e-32).norm(), 1.0, accuracy: 1e-14) + } + + func testNorm2() { + XCTAssertEqual(v(0, 0, 0).norm2(), 0.0, accuracy: 1e-14) + XCTAssertEqual(v(0, 1, 0).norm2(), 1.0, accuracy: 1e-14) + XCTAssertEqual(v(1, 1, 1).norm2(), 3.0, accuracy: 1e-14) + XCTAssertEqual(v(1, 2, 3).norm2(), 14.0, accuracy: 1e-14) + XCTAssertEqual(v(3, -4, 12).norm2(), 169.0, accuracy: 1e-14) + XCTAssertEqual(v(1, 1e-16, 1e-32).norm2(), 1.0, accuracy: 1e-14) + } + + func testNormalize() { + let vectors = [(1, 0, 0), (0, 1, 0), (0, 0, 1), (1, 1, 1), (1, 1e-16, 1e-32), (12.34, 56.78, 91.01)] + for v1 in vectors { + let v = R3Vector(x: v1.0, y: v1.1, z: v1.2) + let nv = v.normalize() + XCTAssertEqual(v.x*nv.y, v.y*nv.x, accuracy: 1e-14) + XCTAssertEqual(v.x*nv.z, v.z*nv.x, accuracy: 1e-14) + XCTAssertEqual(nv.norm(), 1.0, accuracy: 1e-14) + } + } + + func testIsUnit() { + let tests = [ + (v(0, 0, 0), false), + (v(0, 1, 0), true), + (v(1 + 2.0 * 1e-15, 0, 0), true), + (v(1 * (1.0 + 1e-15), 0, 0), true), + (v(1, 1, 1), false), + (v(1, 1e-16, 1e-32), true)] + for (v, want) in tests { + XCTAssertEqual(v.isUnit(), want) + } + } + + func testDot() { + let tests = [ + (v(1, 0, 0), v(1, 0, 0), 1), + (v(1, 0, 0), v(0, 1, 0), 0), + (v(1, 0, 0), v(0, 1, 1), 0), + (v(1, 1, 1), v(-1, -1, -1), -3), + (v(1, 2, 2), v(-0.3, 0.4, -1.2), -1.9)] + for (v1, v2, want) in tests { + let v1 = v(v1.x, v1.y, v1.z) + let v2 = v(v2.x, v2.y, v2.z) + XCTAssertEqual(v1.dot(v2), want, accuracy: 1e-15) + XCTAssertEqual(v2.dot(v1), want, accuracy: 1e-15) + } + } + + func testCross() { + let tests = [ + (v(1, 0, 0), v(1, 0, 0), v(0, 0, 0)), + (v(1, 0, 0), v(0, 1, 0), v(0, 0, 1)), + (v(0, 1, 0), v(1, 0, 0), v(0, 0, -1)), + (v(1, 2, 3), v(-4, 5, -6), v(-27, -6, 13))] + for (v1, v2, want) in tests { + XCTAssert(v1.cross(v2).approxEquals(want)) + } + } + + func testAdd() { + let tests = [ + (v(0, 0, 0), v(0, 0, 0), v(0, 0, 0)), + (v(1, 0, 0), v(0, 0, 0), v(1, 0, 0)), + (v(1, 2, 3), v(4, 5, 7), v(5, 7, 10)), + (v(1, -3, 5), v(1, -6, -6), v(2, -9, -1))] + for (v1, v2, want) in tests { + XCTAssert(v1.add(v2).approxEquals(want)) + } + } + + func testSub() { + let tests = [ + (v(0, 0, 0), v(0, 0, 0), v(0, 0, 0)), + (v(1, 0, 0), v(0, 0, 0), v(1, 0, 0)), + (v(1, 2, 3), v(4, 5, 7), v(-3, -3, -4)), + (v(1, -3, 5), v(1, -6, -6), v(0, 3, 11))] + for (v1, v2, want) in tests { + XCTAssert(v1.sub(v2).approxEquals(want)) + } + } + + func testDistance() { + let tests = [ + (v(1, 0, 0), v(1, 0, 0), 0), + (v(1, 0, 0), v(0, 1, 0), 1.41421356237310), + (v(1, 0, 0), v(0, 1, 1), 1.73205080756888), + (v(1, 1, 1), v(-1, -1, -1), 3.46410161513775), + (v(1, 2, 2), v(-0.3, 0.4, -1.2), 3.80657326213486)] + for (v1, v2, want) in tests { + let v1 = v(v1.x, v1.y, v1.z) + let v2 = v(v2.x, v2.y, v2.z) + XCTAssertEqual(v1.distance(v2), want, accuracy: 1e-13) + XCTAssertEqual(v1.distance(v2), want, accuracy: 1e-13) + } + } + + func testMul() { + let tests = [ + (v(0, 0, 0), 3.0, v(0, 0, 0)), + (v(1, 0, 0), 1, v(1, 0, 0)), + (v(1, 0, 0), 0, v(0, 0, 0)), + (v(1, 0, 0), 3, v(3, 0, 0)), + (v(1, -3, 5), -1, v(-1, 3, -5)), + (v(1, -3, 5), 2, v(2, -6, 10))] + for (v, m, want) in tests { + XCTAssert(v.mul(m).approxEquals(want)) + } + } + + func testAngle() { + let tests = [ + (v(1, 0, 0), v(1, 0, 0), 0), + (v(1, 0, 0), v(0, 1, 0), .pi / 2), + (v(1, 0, 0), v(0, 1, 1), .pi / 2), + (v(1, 0, 0), v(-1, 0, 0), .pi), + (v(1, 2, 3), v(2, 3, -1), 1.2055891055045298)] + for (v1, v2, want) in tests { + XCTAssertEqual(v1.angle(v2), want, accuracy: 1e-15) + XCTAssertEqual(v2.angle(v1), want, accuracy: 1e-15) + } + } + + func testOrtho() { + let vectors = [ + v(1, 0, 0), + v(1, 1, 0), + v(1, 2, 3), + v(1, -2, -5), + v(0.012, 0.0053, 0.00457), + v(-0.012, -1, -0.00457)] + for v in vectors { + XCTAssertEqual(v.dot(v.ortho()), 0, accuracy: 1e-15) + XCTAssertEqual(v.ortho().norm(), 1, accuracy: 1e-15) + } + } + + func testIdentities() { + let tests = [ + (v(0, 0, 0), v(0, 0, 0)), + (v(0, 0, 0), v(0, 1, 2)), + (v(1, 0, 0), v(0, 1, 0)), + (v(1, 0, 0), v(0, 1, 1)), + (v(1, 1, 1), v(-1, -1, -1)), + (v(1, 2, 2), v(-0.3, 0.4, -1.2))] + for (v1, v2) in tests { + let a1 = v1.angle(v2) + let a2 = v2.angle(v1) + let c1 = v1.cross(v2) + let c2 = v2.cross(v1) + let d1 = v1.dot(v2) + let d2 = v2.dot(v1) + // Angle commutes + XCTAssertEqual(a1, a2, accuracy: 1e-15) + // Dot commutes + XCTAssertEqual(d1, d2, accuracy: 1e-15) + // Cross anti-commutes + XCTAssert(c1.approxEquals(c2.mul(-1.0))) + // Cross is orthogonal to original vectors + XCTAssertEqual(v1.dot(c1), 0.0, accuracy: 1e-15) + XCTAssertEqual(v2.dot(c1), 0.0, accuracy: 1e-15) + } + } + +} diff --git a/Sphere2GoLibTests/S1IntervalTests.swift b/Sphere2GoLibTests/S1IntervalTests.swift new file mode 100644 index 0000000..f870ffb --- /dev/null +++ b/Sphere2GoLibTests/S1IntervalTests.swift @@ -0,0 +1,373 @@ +// +// S1IntervalTests.swift +// Sphere2 +// + +import XCTest + +class S1IntervalTests: XCTestCase { + + let empty = S1Interval.empty + let full = S1Interval.full + // Single-point intervals: + let zero = S1Interval(lo_endpoint: 0, hi_endpoint: 0) + let pi2 = S1Interval(lo_endpoint: .pi/2, hi_endpoint: .pi/2) + let pi = S1Interval(lo_endpoint: .pi, hi_endpoint: .pi) + let mipi = S1Interval(lo_endpoint: -.pi, hi_endpoint: -.pi) // same as pi after normalization + let mipi2 = S1Interval(lo_endpoint: -.pi/2, hi_endpoint: -.pi/2) + // Single quadrants: + let quad1 = S1Interval(lo_endpoint: 0, hi_endpoint: .pi/2) + let quad2 = S1Interval(lo_endpoint: .pi/2, hi_endpoint: -.pi) // equivalent to (pi/2, pi) + let quad3 = S1Interval(lo_endpoint: .pi, hi_endpoint: -.pi/2) + let quad4 = S1Interval(lo_endpoint: -.pi/2, hi_endpoint: 0) + // Quadrant pairs: + let quad12 = S1Interval(lo_endpoint: 0, hi_endpoint: -.pi) + let quad23 = S1Interval(lo_endpoint: .pi/2, hi_endpoint: -.pi/2) + let quad34 = S1Interval(lo_endpoint: -.pi, hi_endpoint: 0) + let quad41 = S1Interval(lo_endpoint: -.pi/2, hi_endpoint: .pi/2) + // Quadrant triples: + let quad123 = S1Interval(lo_endpoint: 0, hi_endpoint: -.pi/2) + let quad234 = S1Interval(lo_endpoint: .pi/2, hi_endpoint: 0) + let quad341 = S1Interval(lo_endpoint: .pi, hi_endpoint: .pi/2) + let quad412 = S1Interval(lo_endpoint: -.pi/2, hi_endpoint: -.pi) + // Small intervals around the midpoints between quadrants, + // such that the center of each interval is offset slightly CCW from the midpoint. + let mid12 = S1Interval(lo_endpoint: .pi/2-0.01, hi_endpoint: .pi/2+0.02) + let mid23 = S1Interval(lo_endpoint: .pi-0.01, hi_endpoint: -.pi+0.02) + let mid34 = S1Interval(lo_endpoint: -.pi/2-0.01, hi_endpoint: -.pi/2+0.02) + let mid41 = S1Interval(lo_endpoint: -0.01, hi_endpoint: 0.02) + + override func setUp() { + super.setUp() + // Put setup code here. This method is called before the invocation of each test method in the class. + } + + override func tearDown() { + // Put teardown code here. This method is called after the invocation of each test method in the class. + super.tearDown() + } + + func testConstructors() { + // Check that [-π,-π] is normalized to [π,π]. + XCTAssertEqual(mipi.lo, .pi) + XCTAssertEqual(mipi.hi, .pi) +// XCTAssertTrue(S1Interval().isValid()) + } + + func testSimplePredicates() { + XCTAssertFalse(!zero.isValid() || zero.isEmpty() || zero.isFull()) + XCTAssertFalse(!empty.isValid() || !empty.isEmpty() || empty.isFull()) + XCTAssertFalse(!empty.isInverted()) + XCTAssertFalse(!full.isValid() || full.isEmpty() || !full.isFull()) + XCTAssertFalse(!pi.isValid() || pi.isEmpty() || pi.isInverted()) + XCTAssertFalse(!mipi.isValid() || mipi.isEmpty() || mipi.isInverted()) + } + + func testCenter() { + XCTAssertEqual(quad12.center(), .pi / 2, accuracy: 1e-15) + XCTAssertEqual(S1Interval(lo_endpoint: 3.1, hi_endpoint: 2.9).center(), 3 - .pi, accuracy: 1e-15) + XCTAssertEqual(S1Interval(lo_endpoint: -2.9, hi_endpoint: -3.1).center(), .pi - 3, accuracy: 1e-15) + XCTAssertEqual(S1Interval(lo_endpoint: 2.1, hi_endpoint: -2.1).center(), .pi, accuracy: 1e-15) + XCTAssertEqual(pi.center(), .pi, accuracy: 1e-15) + XCTAssertEqual(mipi.center(), .pi, accuracy: 1e-15) + XCTAssertEqual(quad23.center(), .pi, accuracy: 1e-15) + XCTAssertEqual(quad123.center(), 0.75 * .pi, accuracy: 1e-15) + } + +} + +//func testLength() { +// tests := []struct { +// interval Interval +// want float64 +// }{ +// {quad12, .pi}, +// {pi, 0}, +// {mipi, 0}, +// // TODO(dsymonds): The C++ test for quad123 uses DOUBLE_EQ. Why? +// {quad123, 1.5 * .pi}, +// // TODO(dsymonds): The C++ test for quad23 uses fabs. Why? +// {quad23, .pi}, +// {full, 2 * .pi}, +// } +// for _, test := range tests { +// if l := test.interval.Length(); l != test.want { +// t.Errorf("%v.Length() got %v, want %v", test.interval, l, test.want) +// } +// } +// if l := empty.Length(); l >= 0 { +// t.Errorf("empty interval has non-negative length %v", l) +// } +//} +// +//func testContains() { +// tests := []struct { +// interval Interval +// in, out []float64 // points that should be inside/outside the interval +// iIn, iOut []float64 // points that should be inside/outside the interior +// }{ +// {empty, nil, []float64{0, .pi, -.pi}, nil, []float64{.pi, -.pi}}, +// {full, []float64{0, .pi, -.pi}, nil, []float64{.pi, -.pi}, nil}, +// {quad12, []float64{0, .pi, -.pi}, nil, +// []float64{.pi / 2}, []float64{0, .pi, -.pi}}, +// {quad23, []float64{.pi / 2, -.pi / 2, .pi, -.pi}, []float64{0}, +// []float64{.pi, -.pi}, []float64{.pi / 2, -.pi / 2, 0}}, +// {pi, []float64{.pi, -.pi}, []float64{0}, nil, []float64{.pi, -.pi}}, +// {mipi, []float64{.pi, -.pi}, []float64{0}, nil, []float64{.pi, -.pi}}, +// {zero, []float64{0}, nil, nil, []float64{0}}, +// } +// for _, test := range tests { +// for _, p := range test.in { +// if !test.interval.Contains(p) { +// t.Errorf("%v should contain %v", test.interval, p) +// } +// } +// for _, p := range test.out { +// if test.interval.Contains(p) { +// t.Errorf("%v should not contain %v", test.interval, p) +// } +// } +// for _, p := range test.iIn { +// if !test.interval.InteriorContains(p) { +// t.Errorf("interior of %v should contain %v", test.interval, p) +// } +// } +// for _, p := range test.iOut { +// if test.interval.InteriorContains(p) { +// t.Errorf("interior %v should not contain %v", test.interval, p) +// } +// } +// } +//} +// +//func testIntervalOperations() { +// quad12eps := IntervalFromEndpoints(quad12.Lo, mid23.Hi) +// quad2hi := IntervalFromEndpoints(mid23.Lo, quad12.Hi) +// quad412eps := IntervalFromEndpoints(mid34.Lo, quad12.Hi) +// quadeps12 := IntervalFromEndpoints(mid41.Lo, quad12.Hi) +// quad1lo := IntervalFromEndpoints(quad12.Lo, mid41.Hi) +// quad2lo := IntervalFromEndpoints(quad23.Lo, mid12.Hi) +// quad3hi := IntervalFromEndpoints(mid34.Lo, quad23.Hi) +// quadeps23 := IntervalFromEndpoints(mid12.Lo, quad23.Hi) +// quad23eps := IntervalFromEndpoints(quad23.Lo, mid34.Hi) +// quadeps123 := IntervalFromEndpoints(mid41.Lo, quad23.Hi) +// +// // This massive list of test cases is ported directly from the C++ test case. +// tests := []struct { +// x, y Interval +// xContainsY, xInteriorContainsY bool +// xIntersectsY, xInteriorIntersectsY bool +// wantUnion, wantIntersection Interval +// }{ +// // 0 +// {empty, empty, true, true, false, false, empty, empty}, +// {empty, full, false, false, false, false, full, empty}, +// {empty, zero, false, false, false, false, zero, empty}, +// {empty, pi, false, false, false, false, pi, empty}, +// {empty, mipi, false, false, false, false, mipi, empty}, +// +// // 5 +// {full, empty, true, true, false, false, full, empty}, +// {full, full, true, true, true, true, full, full}, +// {full, zero, true, true, true, true, full, zero}, +// {full, pi, true, true, true, true, full, pi}, +// {full, mipi, true, true, true, true, full, mipi}, +// {full, quad12, true, true, true, true, full, quad12}, +// {full, quad23, true, true, true, true, full, quad23}, +// +// // 12 +// {zero, empty, true, true, false, false, zero, empty}, +// {zero, full, false, false, true, false, full, zero}, +// {zero, zero, true, false, true, false, zero, zero}, +// {zero, pi, false, false, false, false, IntervalFromEndpoints(0, .pi), empty}, +// {zero, pi2, false, false, false, false, quad1, empty}, +// {zero, mipi, false, false, false, false, quad12, empty}, +// {zero, mipi2, false, false, false, false, quad4, empty}, +// {zero, quad12, false, false, true, false, quad12, zero}, +// {zero, quad23, false, false, false, false, quad123, empty}, +// +// // 21 +// {pi2, empty, true, true, false, false, pi2, empty}, +// {pi2, full, false, false, true, false, full, pi2}, +// {pi2, zero, false, false, false, false, quad1, empty}, +// {pi2, pi, false, false, false, false, IntervalFromEndpoints(.pi/2, .pi), empty}, +// {pi2, pi2, true, false, true, false, pi2, pi2}, +// {pi2, mipi, false, false, false, false, quad2, empty}, +// {pi2, mipi2, false, false, false, false, quad23, empty}, +// {pi2, quad12, false, false, true, false, quad12, pi2}, +// {pi2, quad23, false, false, true, false, quad23, pi2}, +// +// // 30 +// {pi, empty, true, true, false, false, pi, empty}, +// {pi, full, false, false, true, false, full, pi}, +// {pi, zero, false, false, false, false, IntervalFromEndpoints(.pi, 0), empty}, +// {pi, pi, true, false, true, false, pi, pi}, +// {pi, pi2, false, false, false, false, IntervalFromEndpoints(.pi/2, .pi), empty}, +// {pi, mipi, true, false, true, false, pi, pi}, +// {pi, mipi2, false, false, false, false, quad3, empty}, +// {pi, quad12, false, false, true, false, IntervalFromEndpoints(0, .pi), pi}, +// {pi, quad23, false, false, true, false, quad23, pi}, +// +// // 39 +// {mipi, empty, true, true, false, false, mipi, empty}, +// {mipi, full, false, false, true, false, full, mipi}, +// {mipi, zero, false, false, false, false, quad34, empty}, +// {mipi, pi, true, false, true, false, mipi, mipi}, +// {mipi, pi2, false, false, false, false, quad2, empty}, +// {mipi, mipi, true, false, true, false, mipi, mipi}, +// {mipi, mipi2, false, false, false, false, IntervalFromEndpoints(-.pi, -.pi/2), empty}, +// {mipi, quad12, false, false, true, false, quad12, mipi}, +// {mipi, quad23, false, false, true, false, quad23, mipi}, +// +// // 48 +// {quad12, empty, true, true, false, false, quad12, empty}, +// {quad12, full, false, false, true, true, full, quad12}, +// {quad12, zero, true, false, true, false, quad12, zero}, +// {quad12, pi, true, false, true, false, quad12, pi}, +// {quad12, mipi, true, false, true, false, quad12, mipi}, +// {quad12, quad12, true, false, true, true, quad12, quad12}, +// {quad12, quad23, false, false, true, true, quad123, quad2}, +// {quad12, quad34, false, false, true, false, full, quad12}, +// +// // 56 +// {quad23, empty, true, true, false, false, quad23, empty}, +// {quad23, full, false, false, true, true, full, quad23}, +// {quad23, zero, false, false, false, false, quad234, empty}, +// {quad23, pi, true, true, true, true, quad23, pi}, +// {quad23, mipi, true, true, true, true, quad23, mipi}, +// {quad23, quad12, false, false, true, true, quad123, quad2}, +// {quad23, quad23, true, false, true, true, quad23, quad23}, +// {quad23, quad34, false, false, true, true, quad234, IntervalFromEndpoints(-.pi, -.pi/2)}, +// +// // 64 +// {quad1, quad23, false, false, true, false, quad123, IntervalFromEndpoints(.pi/2, .pi/2)}, +// {quad2, quad3, false, false, true, false, quad23, mipi}, +// {quad3, quad2, false, false, true, false, quad23, pi}, +// {quad2, pi, true, false, true, false, quad2, pi}, +// {quad2, mipi, true, false, true, false, quad2, mipi}, +// {quad3, pi, true, false, true, false, quad3, pi}, +// {quad3, mipi, true, false, true, false, quad3, mipi}, +// +// // 71 +// {quad12, mid12, true, true, true, true, quad12, mid12}, +// {mid12, quad12, false, false, true, true, quad12, mid12}, +// +// // 73 +// {quad12, mid23, false, false, true, true, quad12eps, quad2hi}, +// {mid23, quad12, false, false, true, true, quad12eps, quad2hi}, +// +// // This test checks that the union of two disjoint intervals is the smallest +// // interval that contains both of them. Note that the center of "mid34" +// // slightly CCW of -Pi/2 so that there is no ambiguity about the result. +// // 75 +// {quad12, mid34, false, false, false, false, quad412eps, empty}, +// {mid34, quad12, false, false, false, false, quad412eps, empty}, +// +// // 77 +// {quad12, mid41, false, false, true, true, quadeps12, quad1lo}, +// {mid41, quad12, false, false, true, true, quadeps12, quad1lo}, +// +// // 79 +// {quad23, mid12, false, false, true, true, quadeps23, quad2lo}, +// {mid12, quad23, false, false, true, true, quadeps23, quad2lo}, +// {quad23, mid23, true, true, true, true, quad23, mid23}, +// {mid23, quad23, false, false, true, true, quad23, mid23}, +// {quad23, mid34, false, false, true, true, quad23eps, quad3hi}, +// {mid34, quad23, false, false, true, true, quad23eps, quad3hi}, +// {quad23, mid41, false, false, false, false, quadeps123, empty}, +// {mid41, quad23, false, false, false, false, quadeps123, empty}, +// } +// should := func(b bool) string { +// if b { +// return "should" +// } +// return "should not" +// } +// for _, test := range tests { +// if test.x.ContainsInterval(test.y) != test.xContainsY { +// t.Errorf("%v %s contain %v", test.x, should(test.xContainsY), test.y) +// } +// if test.x.InteriorContainsInterval(test.y) != test.xInteriorContainsY { +// t.Errorf("interior of %v %s contain %v", test.x, should(test.xInteriorContainsY), test.y) +// } +// if test.x.Intersects(test.y) != test.xIntersectsY { +// t.Errorf("%v %s intersect %v", test.x, should(test.xIntersectsY), test.y) +// } +// if test.x.InteriorIntersects(test.y) != test.xInteriorIntersectsY { +// t.Errorf("interior of %v %s intersect %v", test.x, should(test.xInteriorIntersectsY), test.y) +// } +// if u := test.x.Union(test.y); u != test.wantUnion { +// t.Errorf("%v ∪ %v was %v, want %v", test.x, test.y, u, test.wantUnion) +// } +// if u := test.x.Intersection(test.y); u != test.wantIntersection { +// t.Errorf("%v ∩ %v was %v, want %v", test.x, test.y, u, test.wantIntersection) +// } +// } +//} +// +//func testAddPoint() { +// tests := []struct { +// interval Interval +// points []float64 +// want Interval +// }{ +// {empty, []float64{0}, zero}, +// {empty, []float64{.pi}, pi}, +// {empty, []float64{-.pi}, mipi}, +// {empty, []float64{.pi, -.pi}, pi}, +// {empty, []float64{-.pi, .pi}, mipi}, +// {empty, []float64{mid12.Lo, mid12.Hi}, mid12}, +// {empty, []float64{mid23.Lo, mid23.Hi}, mid23}, +// +// {quad1, []float64{-0.9 * .pi, -.pi / 2}, quad123}, +// {full, []float64{0}, full}, +// {full, []float64{.pi}, full}, +// {full, []float64{-.pi}, full}, +// } +// for _, test := range tests { +// got := test.interval +// for _, point := range test.points { +// got = got.AddPoint(point) +// } +// want := test.want +// if math.Abs(got.Lo-want.Lo) > 1e-15 || math.Abs(got.Hi-want.Hi) > 1e-15 { +// t.Errorf("%v.AddPoint(%v) = %v, want %v", test.interval, test.points, got, want) +// } +// } +//} +// +//func testExpanded() { +// tests := []struct { +// interval Interval +// margin float64 +// want Interval +// }{ +// {empty, 1, empty}, +// {full, 1, full}, +// {zero, 1, Interval{-1, 1}}, +// {mipi, 0.01, Interval{.pi - 0.01, -.pi + 0.01}}, +// {pi, 27, full}, +// {pi, .pi / 2, quad23}, +// {pi2, .pi / 2, quad12}, +// {mipi2, .pi / 2, quad34}, +// +// {empty, -1, empty}, +// {full, -1, full}, +// {quad123, -27, empty}, +// {quad234, -27, empty}, +// {quad123, -.pi / 2, quad2}, +// {quad341, -.pi / 2, quad4}, +// {quad412, -.pi / 2, quad1}, +// } +// for _, test := range tests { +// if got, want := test.interval.Expanded(test.margin), test.want; math.Abs(got.Lo-want.Lo) > 1e-15 || math.Abs(got.Hi-want.Hi) > 1e-15 { +// t.Errorf("%v.Expanded(%v) = %v, want %v", test.interval, test.margin, got, want) +// } +// } +//} +// +//func testIntervalString() { +// if s, exp := pi.String(), "[3.1415927, 3.1415927]"; s != exp { +// t.Errorf("pi.String() = %q, want %q", s, exp) +// } +//} diff --git a/Sphere2GoLibTests/S2CapTests.swift b/Sphere2GoLibTests/S2CapTests.swift new file mode 100644 index 0000000..5fa9917 --- /dev/null +++ b/Sphere2GoLibTests/S2CapTests.swift @@ -0,0 +1,342 @@ +// +// S2CapTests.swift +// Sphere2 +// + +import XCTest + +class S2CapTests: XCTestCase { + + let tinyRad = 1e-10 + + let empty = S2Cap.empty + let full = S2Cap.full + let defaultCap = S2Cap.empty + + let xAxisPt = S2Point(x: 1, y: 0, z: 0) + let yAxisPt = S2Point(x: 0, y: 1, z: 0) + + let xAxis = S2Cap(point: S2Point(x: 1, y: 0, z: 0)) + let yAxis = S2Cap(point: S2Point(x: 0, y: 1, z: 0)) + let xComp = S2Cap(point: S2Point(x: 1, y: 0, z: 0)).complement() + + let hemi = S2Cap(center: S2Point(x: 1, y: 0, z: 1), height: 1.0) + let concave = S2Cap(center: LatLng(lat: 80.0 * toRadians, lng: 10.0 * toRadians).toPoint(), angle: 150.0 * toRadians) + let tiny = S2Cap(center: S2Point(x: 1, y: 2, z: 3), angle: 1e-10) + + override func setUp() { + super.setUp() + // Put setup code here. This method is called before the invocation of each test method in the class. + } + + override func tearDown() { + // Put teardown code here. This method is called after the invocation of each test method in the class. + super.tearDown() + } + + func testCapBasicEmptyFullValid() { + let tests = [ +// (S2Cap(), false, false, false), + (empty, true, false, true), + (empty.complement(), false, true, true), + (full, false, true, true), + (full.complement(), true, false, true), + (defaultCap, true, false, true), + (xComp, false, true, true), + (xComp.complement(), true, false, true), + (tiny, false, false, true), + (concave, false, false, true), + (hemi, false, false, true), + (tiny, false, false, true)] + for (got, empty, full, valid) in tests { + XCTAssertEqual(got.isEmpty(), empty, "\(got), \(empty)") + XCTAssertEqual(got.isFull(), full) + XCTAssertEqual(got.isValid(), valid, "\(got), \(valid)") + } + } + + func testCapCenterHeightRadius() { + XCTAssertTrue(xAxis.approxEquals(xAxis.complement().complement())) + XCTAssertEqual(full.height, S2Cap.fullHeight) + XCTAssertEqual(full.radius(), 180.0 * toRadians) + XCTAssertEqual(empty.center, defaultCap.center) + XCTAssertEqual(empty.height, defaultCap.height) + XCTAssertEqual(yAxis.height, S2Cap.zeroHeight) + XCTAssertEqual(xAxis.height, S2Cap.zeroHeight) + XCTAssertEqual(xAxis.radius(), S2Cap.zeroHeight) + XCTAssertEqual(hemi.center.inverse(), hemi.complement().center) + XCTAssertEqual(hemi.height, 1.0) + } + + func testCapContains() { + let epsilon = 1e-14 + XCTAssertTrue(empty.contains(empty)) + XCTAssertTrue(full.contains(empty)) + XCTAssertTrue(full.contains(full)) + XCTAssertFalse(empty.contains(xAxis)) + XCTAssertTrue(full.contains(xAxis)) + XCTAssertFalse(xAxis.contains(full)) + XCTAssertTrue(xAxis.contains(xAxis)) + XCTAssertTrue(xAxis.contains(empty)) + XCTAssertTrue(hemi.contains(tiny)) + XCTAssertTrue(hemi.contains(S2Cap(center: xAxisPt, angle: .pi / 4 - epsilon))) + XCTAssertFalse(hemi.contains(S2Cap(center: xAxisPt, angle: .pi / 4 + epsilon))) + XCTAssertTrue(concave.contains(hemi)) + XCTAssertFalse(concave.contains(S2Cap(center: concave.center.inverse(), height: 0.1))) + } + + func testCapContainsPoint() { + // We don't use the standard epsilon in this test due different compiler + // math optimizations that are permissible (FMA vs no FMA) that yield + // slightly different floating point results between gccgo and gc. + let tangent = tiny.center.v.cross(S2Point(x: 3, y: 2, z: 1).v) + let epsilon = 1e-14 + XCTAssertTrue(xAxis.contains(xAxisPt)) + XCTAssertFalse(xAxis.contains(S2Point(x: 1, y: 1e-20, z: 0))) + XCTAssertFalse(yAxis.contains(xAxis.center)) + XCTAssertTrue(xComp.contains(xAxis.center)) + XCTAssertFalse(xComp.complement().contains(xAxis.center)) + XCTAssertTrue(tiny.contains(tiny.center.v.add(tangent.mul(tinyRad * 0.99)).s2)) + XCTAssertFalse(tiny.contains(tiny.center.v.add(tangent.mul(tinyRad * 1.81)).s2)) + XCTAssertTrue(hemi.contains(S2Point(x: 1, y: 0, z: -(1 - epsilon)))) + XCTAssertTrue(hemi.contains(xAxisPt)) + XCTAssertFalse(hemi.complement().contains(xAxisPt)) + XCTAssertTrue(concave.contains(LatLng(latDegrees: -70*(1-epsilon), lngDegrees: 10).toPoint())) + XCTAssertFalse(concave.contains(LatLng(latDegrees: -70*(1+epsilon), lngDegrees: 10).toPoint())) + // This test case is the one where the floating point values end up + // different in the 15th place and beyond. + XCTAssertTrue(concave.contains(LatLng(latDegrees: -50*(1-epsilon), lngDegrees: -170).toPoint())) + XCTAssertFalse(concave.contains(LatLng(latDegrees: -50*(1+epsilon), lngDegrees: -170).toPoint())) + } + + func testCapInteriorIntersects() { + let tests = [ + (empty, empty, false), + (empty, xAxis, false), + (full, empty, false), + (full, full, true), + (full, xAxis, true), + (xAxis, full, false), + (xAxis, xAxis, false), + (xAxis, empty, false), + (concave, hemi.complement(), true)] + for (c1, c2, want) in tests { + XCTAssertEqual(c1.interiorIntersects(c2), want) + } + } + + func testCapInteriorContains() { + let epsilon = 1e-14 + XCTAssertFalse(hemi.interiorContains(S2Point(x: 1, y: 0, z: -(1 + epsilon).normalize()))) + } + + func testCapExpanded() { + let cap50 = S2Cap(center: xAxisPt, angle: 50.0 * toRadians) + let cap51 = S2Cap(center: xAxisPt, angle: 51.0 * toRadians) + XCTAssertTrue(empty.expanded(S2Cap.fullHeight).isEmpty()) + XCTAssertTrue(full.expanded(S2Cap.fullHeight).isFull()) + XCTAssertTrue(cap50.expanded(0).approxEquals(cap50)) + XCTAssertTrue(cap50.expanded(1 * toRadians).approxEquals(cap51)) + XCTAssertFalse(cap50.expanded(129.99 * toRadians).isFull()) + XCTAssertTrue(cap50.expanded(130.01 * toRadians).isFull()) + } + + func testRadiusToHeight() { + let tests = [ + // Above/below boundary checks. + (-0.5, S2Cap.emptyHeight), + (0, 0), + (.pi, S2Cap.fullHeight), + (2 * .pi, S2Cap.fullHeight), + // Degree tests. + (-7.0 * toRadians, S2Cap.emptyHeight), + (-0.0 * toRadians, 0), + (0.0 * toRadians, 0), + (12.0 * toRadians, 0.0218523992661943), + (30.0 * toRadians, 0.1339745962155613), + (45.0 * toRadians, 0.2928932188134525), + (90.0 * toRadians, 1.0), + (179.99 * toRadians, 1.9999999847691292), + (180.0 * toRadians, S2Cap.fullHeight), + (270.0 * toRadians, S2Cap.fullHeight), + // Radians tests. + (-1.0, S2Cap.emptyHeight), + (-0.0, 0), + (0.0, 0), + (1.0, 0.45969769413186), + (.pi / 2.0, 1.0), + (2.0, 1.4161468365471424), + (3.0, 1.9899924966004454), + (.pi, S2Cap.fullHeight), + (4.0, S2Cap.fullHeight)] + for (got, want) in tests { + XCTAssertEqual(S2Cap.radiusToHeight(got), want, accuracy: 1e-14) + } + } + + func testCapGetRectBounds() { + let epsilon = 1e-13 + let tests = [ + ("Cap that includes South Pole.", S2Cap(center: LatLng(latDegrees: -45, lngDegrees: 57).toPoint(), angle: 50 * toRadians), -90.0, 5.0, -180.0, 180.0, true), + ("Cap that is tangent to the North Pole.", S2Cap(center: S2Point(x: 1, y: 0, z: 1), angle: .pi/4.0+1e-16), 0, 90, -180, 180, true), + ("Cap that at 45 degree center that goes from equator to the pole.", S2Cap(center: S2Point(x: 1, y: 0, z: 1), angle: (45+5e-15) * toRadians), 0, 90, -180, 180, true), + ("The eastern hemisphere.", S2Cap(center: S2Point(x: 0, y: 1, z: 0), angle: .pi/2+2e-16), -90, 90, -180, 180, true), + ("A cap centered on the equator.", S2Cap(center: LatLng(latDegrees: 0, lngDegrees: 50).toPoint(), angle: 20 * toRadians), -20, 20, 30, 70, false), + ("A cap centered on the North Pole.", S2Cap(center: LatLng(latDegrees: 90, lngDegrees: 123).toPoint(), angle: 10 * toRadians), 80, 90, -180, 180, true)] + + for (_, have, latLoDeg, latHiDeg, lngLoDeg, lngHiDeg, isFull) in tests { + let r = have.rectBound() + XCTAssertEqual(r.lat.lo, latLoDeg * toRadians, accuracy: epsilon) + XCTAssertEqual(r.lat.hi, latHiDeg * toRadians, accuracy: epsilon) + XCTAssertEqual(r.lng.lo, lngLoDeg * toRadians, accuracy: epsilon) + XCTAssertEqual(r.lng.hi, lngHiDeg * toRadians, accuracy: epsilon) + XCTAssertEqual(r.lng.isFull(), isFull) + } + // Empty and full caps. + XCTAssertTrue(S2Cap.empty.rectBound().isEmpty()) + + XCTAssertTrue(S2Cap.full.rectBound().isFull()) + } + + func testCapAddPoint() { + let tests = [ + // S2Cap plus its center equals itself. + (xAxis, xAxisPt, xAxis), + (yAxis, yAxisPt, yAxis), + // Cap plus opposite point equals full. + (xAxis, S2Point(x: -1, y: 0, z: 0), full), + (yAxis, S2Point(x: 0, y: -1, z: 0), full), + // Cap plus orthogonal axis equals half cap. + (xAxis, S2Point(x: 0, y: 0, z: 1), S2Cap(center: xAxisPt, angle: .pi / 2.0)), + (xAxis, S2Point(x: 0, y: 0, z: -1), S2Cap(center: xAxisPt, angle: .pi / 2.0)), + // The 45 degree angled hemisphere plus some points. + (hemi, S2Point(x: 0, y: 1, z: -1), S2Cap(center: S2Point(x: 1, y: 0, z: 1), angle: 120.0 * toRadians)), + (hemi, S2Point(x: 0, y: -1, z: -1), S2Cap(center: S2Point(x: 1, y: 0, z: 1), angle: 120.0 * toRadians)), + // This angle between this point and the center is acos(-sqrt(2/3)) + (hemi, S2Point(x: -1, y: -1, z: -1), S2Cap(center: S2Point(x: 1, y: 0, z: 1), angle: 2.5261129449194)), + (hemi, S2Point(x: 0, y: 1, z: 1), hemi), + (hemi, S2Point(x: 1, y: 0, z: 0), hemi)] + for (have, p, want) in tests { + let got = have.add(p) + NSLog("\n\(have) \(p)\n\(got)\n\(want)") + XCTAssertTrue(got.approxEquals(want)) + XCTAssertTrue(got.contains(p)) + } + } + + func testCapAddCap() { + let tests = [ + // Identity cases. + (empty, empty, empty), + (full, full, full), + // Anything plus empty equals itself. + (full, empty, full), + (empty, full, full), + (xAxis, empty, xAxis), + (empty, xAxis, xAxis), + (yAxis, empty, yAxis), + (empty, yAxis, yAxis), + // Two halves make a whole. + (xAxis, xComp, full), + // Two zero-height orthogonal axis caps make a half-cap. + (xAxis, yAxis, S2Cap(center: xAxisPt, angle: .pi/2.0))] + for (have, other, want) in tests { + let got = have.add(other) + XCTAssertTrue(got.approxEquals(want)) + } + } + + func testCapContainsCell() { + // call this to initialize the lookup tables + CellId.setup() + // + let eps = 1e-15 + let faceRadius = atan(sqrt(2.0)) + for face in 0..<6 { + // The cell consisting of the entire face. + let rootCell = Cell(id: CellId(face: face)) + // A leaf cell at the midpoint of the v=1 edge. + let edgeCell = Cell(point: S2Cube(face: face, u: 0, v: 1-eps).vector().s2) + // A leaf cell at the u=1, v=1 corner + let cornerCell = Cell(point: S2Cube(face: face, u: 1-eps, v: 1-eps).vector().s2) + // Quick check for full and empty caps. + XCTAssertTrue(full.contains(rootCell), "face \(face)") + // Check intersections with the bounding caps of the leaf cells that are adjacent to + // cornerCell along the Hilbert curve. Because this corner is at (u=1,v=1), the curve + // stays locally within the same cube face. + let first = cornerCell.id.advance(-3) + let last = cornerCell.id.advance(4) + var id = first + while id.id < last.id { + let c = Cell(id: id).capBound() + XCTAssertEqual(c.contains(cornerCell), id == cornerCell.id) + id = id.next() + } + // + for capFace in 0..<6 { + // A cap that barely contains all of capFace. + let center = S2Cube.unitNorm(face: capFace) + let covering = S2Cap(center: center, angle: faceRadius+eps) + XCTAssertEqual(covering.contains(rootCell), capFace == face) + XCTAssertEqual(covering.contains(edgeCell), center.v.dot(edgeCell.id.point().v) > 0.1) + XCTAssertEqual(covering.contains(edgeCell), covering.intersects(edgeCell)) + XCTAssertEqual(covering.contains(cornerCell), capFace == face) + // A cap that barely intersects the edges of capFace. + let bulging = S2Cap(center: center, angle: .pi/4+eps) + XCTAssertFalse(bulging.contains(rootCell)) + XCTAssertEqual(bulging.contains(edgeCell), capFace == face) + XCTAssertFalse(bulging.contains(cornerCell)) + } + } + } + + func testCapIntersectsCell() { + // call this to initialize the lookup tables + CellId.setup() + // + let eps = 1e-15 + let faceRadius = atan(sqrt(2.0)) + for face in 0..<6 { + // The cell consisting of the entire face. + let rootCell = Cell(id: CellId(face: face)) + // A leaf cell at the midpoint of the v=1 edge. + let edgeCell = Cell(point: S2Cube(face: face, u: 0, v: 1-eps).vector().s2) + // A leaf cell at the u=1, v=1 corner + let cornerCell = Cell(point: S2Cube(face: face, u: 1-eps, v: 1-eps).vector().s2) + // Quick check for full and empty caps. + XCTAssertFalse(empty.intersects(rootCell)) + // Check intersections with the bounding caps of the leaf cells that are adjacent to + // cornerCell along the Hilbert curve. Because this corner is at (u=1,v=1), the curve + // stays locally within the same cube face. + let first = cornerCell.id.advance(-3) + let last = cornerCell.id.advance(4) + var id = first + while id.id < last.id { + let c = Cell(id: id).capBound() + XCTAssertEqual(c.intersects(cornerCell), id.immediateParent().contains(cornerCell.id)) + id = id.next() + } + let antiFace = (face + 3) % 6 + for capFace in 0..<6 { + // A cap that barely contains all of capFace. + let center = S2Cube.unitNorm(face: capFace) + let covering = S2Cap(center: center, angle: faceRadius+eps) + XCTAssertEqual(covering.intersects(rootCell), capFace != antiFace) + XCTAssertEqual(covering.intersects(edgeCell), covering.contains(edgeCell)) + XCTAssertEqual(covering.intersects(cornerCell), center.v.dot(cornerCell.id.point().v) > 0) + // A cap that barely intersects the edges of capFace. + let bulging = S2Cap(center: center, angle: .pi/4+eps) + XCTAssertEqual(bulging.intersects(rootCell), capFace != antiFace) + XCTAssertEqual(bulging.intersects(edgeCell), center.v.dot(edgeCell.id.point().v) > 0.1) + XCTAssertFalse(bulging.intersects(cornerCell)) + // A singleton cap. + let singleton = S2Cap(center: center, angle: 0) + XCTAssertEqual(singleton.intersects(rootCell), capFace == face) + XCTAssertFalse(singleton.intersects(edgeCell)) + XCTAssertFalse(singleton.intersects(cornerCell)) + } + } + } + +} + diff --git a/Sphere2GoLibTests/S2CellIdTests.swift b/Sphere2GoLibTests/S2CellIdTests.swift new file mode 100644 index 0000000..2372e9e --- /dev/null +++ b/Sphere2GoLibTests/S2CellIdTests.swift @@ -0,0 +1,331 @@ +// +// S2CellIdTests.swift +// Sphere2 +// + +import XCTest + +class S2CellIdTests: XCTestCase { + + override func setUp() { + super.setUp() + // Put setup code here. This method is called before the invocation of each test method in the class. + } + + override func tearDown() { + // Put teardown code here. This method is called after the invocation of each test method in the class. + super.tearDown() + } + + func testCellIdFromFace() { + for face in 0..<6 { + XCTAssertEqual(CellId(face: face, pos: 0, level: 0), CellId(face: face)) + } + } + + func testParentChildRelationships() { + let ci = CellId(face: 3, pos: 0x12345678, level: CellId.maxLevel-4) + XCTAssertTrue(ci.isValid()) + XCTAssertEqual(ci.face(), 3) + XCTAssertEqual(ci.pos(), 0x12345700) + XCTAssertEqual(ci.level(), 26) + XCTAssertFalse(ci.isLeaf()) + XCTAssertEqual(ci.childBegin(ci.level() + 2).pos(), 0x12345610) + XCTAssertEqual(ci.childBegin().pos(), 0x12345640) + XCTAssertEqual(ci.children()[0].pos(), 0x12345640) + XCTAssertEqual(ci.immediateParent().pos(), 0x12345400) + XCTAssertEqual(ci.parent(ci.level() - 2).pos(), 0x12345000) + XCTAssert(ci.childBegin().id < ci.id) + XCTAssert(ci.childEnd().id > ci.id) + XCTAssertEqual(ci.childEnd().id, ci.childBegin().next().next().next().next().id) + XCTAssertEqual(ci.rangeMin().id, ci.childBegin(CellId.maxLevel).id) + XCTAssertEqual(ci.rangeMax().next().id, ci.childEnd(CellId.maxLevel).id) + } + + func testContainment() { + let a = CellId(id: 0x80855c0000000000) // Pittsburg + let b = CellId(id: 0x80855d0000000000) // child of a + let c = CellId(id: 0x80855dc000000000) // child of b + let d = CellId(id: 0x8085630000000000) // part of Pittsburg disjoint from a + let tests = [ + (a, a, true, true, true), + (a, b, true, false, true), + (a, c, true, false, true), + (a, d, false, false, false), + (b, b, true, true, true), + (b, c, true, false, true), + (b, d, false, false, false), + (c, c, true, true, true), + (c, d, false, false, false), + (d, d, true, true, true)] + for (x, y, xContainsY, yContainsX, xIntersectsY) in tests { + XCTAssertEqual(x.contains(y), xContainsY) + XCTAssertEqual(x.intersects(y), xIntersectsY) + XCTAssertEqual(y.contains(x), yContainsX) + } + // TODO(dsymonds): Test Contains, Intersects better, such as with adjacent cells. + } + + func testCellIDString() { + let ci = CellId(id: 0xbb04000000000000) + XCTAssertEqual(ci.description, "5/31200") + } + + func testLatLng() { + // call this to initialize the lookup tables + CellId.setup() + // You can generate these with the s2cellid2latlngtestcase C++ program in this directory. + let tests = [ + (UInt64(0x47a1cbd595522b39), 49.703498679, 11.770681595), + (0x46525318b63be0f9, 55.685376759, 12.588490937), + (0x52b30b71698e729d, 45.486546517, -93.449700022), + (0x46ed8886cfadda85, 58.299984854, 23.049300056), + (0x3663f18a24cbe857, 34.364439040, 108.330699969), + (0x10a06c0a948cf5d, -30.694551352, -30.048758753), + (0x2b2bfd076787c5df, -25.285264027, 133.823116966), + (0xb09dff882a7809e1, -75.000000031, 0.000000133), + (0x94daa3d000000001, -24.694439215, -47.537363213), + (0x87a1000000000001, 38.899730392, -99.901813021), + (0x4fc76d5000000001, 81.647200334, -55.631712940), + (0x3b00955555555555, 10.050986518, 78.293170610), + (0x1dcc469991555555, -34.055420593, 18.551140038), + (0xb112966aaaaaaaab, -69.219262171, 49.670072392)] + for (id, lat, lng) in tests { + let cellId = CellId(id: id) + let ll = LatLng(latDegrees: lat, lngDegrees: lng) + let l2 = cellId.latLng() + let s1 = String(format: "%lX", CellId(latLng: ll).id) + let s2 = String(format: "%lX", id) + NSLog("\(ll) \(s1)") + NSLog("\(l2) \(s2)") + XCTAssert(ll.distance(l2) <= 1e-9 * toRadians, "\(ll.distance(l2))") // ~0.1mm on earth. + XCTAssertEqual(id, CellId(latLng: ll).id) + } + } + + func testEdgeNeighbors() { + // Check the edge neighbors of face 1. + let faces = [5, 3, 2, 0] + let p0 = CellId(face: 1, i: 0, j: 0) + let p1 = CellId(face: 1, i: 0, j: 0).parent(0) + for (i, nbr) in p1.edgeNeighbors().enumerated() { + XCTAssertTrue(nbr.isFace()) + XCTAssertEqual(nbr.face(), faces[i]) + } + // Check the edge neighbors of the corner cells at all levels. This case is + // trickier because it requires projecting onto adjacent faces. + let maxIJ = CellId.maxSize - 1 + for level in 1...CellId.maxLevel { + let id = p0.parent(level) + // These neighbors were determined manually using the face and axis + // relationships. + let levelSizeIJ = CellId.sizeIJ(level) + let want = [ + CellId(face: 5, i: maxIJ, j: maxIJ).parent(level), + CellId(face: 1, i: levelSizeIJ, j: 0).parent(level), + CellId(face: 1, i: 0, j: levelSizeIJ).parent(level), + CellId(face: 0, i: maxIJ, j: 0).parent(level)] + for (i, nbr) in id.edgeNeighbors().enumerated() { + XCTAssertEqual(nbr, want[i]) + } + } + } + + func testVertexNeighbors() { + // Check the vertex neighbors of the center of face 2 at level 5. + let id = CellId(point: S2Point(x: 0, y: 0, z: 1)) + var neighbors = id.vertexNeighbors(5) + neighbors.sort { $0.id < $1.id } + + for (n, nbr) in neighbors.enumerated() { + var i = 1<<29 + var j = 1<<29 + if n < 2 { + i -= 1 + } + if n == 0 || n == 3 { + j -= 1 + } + let want = CellId(face: 2, i: i, j: j).parent(5) + + XCTAssertEqual(nbr, want) + } + let i = 1<<29 + let j = 1<<29 + XCTAssertEqual(neighbors[0], CellId(face: 2, i: i-1, j: j-1).parent(5), String(format: "%X", neighbors[0].id)) + XCTAssertEqual(neighbors[1], CellId(face: 2, i: i-1, j: j).parent(5)) + XCTAssertEqual(neighbors[2], CellId(face: 2, i: i, j: j).parent(5)) +// XCTAssertEqual(neighbors[3], CellId(face: 2, i: i, j: j-1).parent(5)) + + // Check the vertex neighbors of the corner of faces 0, 4, and 5. + let id2 = CellId(face: 0, pos: 0, level: CellId.maxLevel) + var neighbors2 = id2.vertexNeighbors(0) + neighbors2.sort { $0.id < $1.id } + XCTAssertEqual(neighbors2.count, 3) + XCTAssertEqual(neighbors2[0], CellId(face: 0)) + XCTAssertEqual(neighbors2[1], CellId(face: 4)) + } + + func testCellIDTokensNominal() { + let tests = [ + ("1", UInt64(0x1000000000000000)), + ("3", 0x3000000000000000), + ("14", 0x1400000000000000), + ("41", 0x4100000000000000), + ("094", 0x0940000000000000), + ("537", 0x5370000000000000), + ("3fec", 0x3fec000000000000), + ("72f3", 0x72f3000000000000), + ("52b8c", 0x52b8c00000000000), + ("990ed", 0x990ed00000000000), + ("4476dc", 0x4476dc0000000000), + ("2a724f", 0x2a724f0000000000), + ("7d4afc4", 0x7d4afc4000000000), + ("b675785", 0xb675785000000000), + ("40cd6124", 0x40cd612400000000), + ("3ba32f81", 0x3ba32f8100000000), + ("08f569b5c", 0x08f569b5c0000000), + ("385327157", 0x3853271570000000), + ("166c4d1954", 0x166c4d1954000000), + ("96f48d8c39", 0x96f48d8c39000000), + ("0bca3c7f74c", 0x0bca3c7f74c00000), + ("1ae3619d12f", 0x1ae3619d12f00000), + ("07a77802a3fc", 0x07a77802a3fc0000), + ("4e7887ec1801", 0x4e7887ec18010000), + ("4adad7ae74124", 0x4adad7ae74124000), + ("90aba04afe0c5", 0x90aba04afe0c5000), + ("8ffc3f02af305c", 0x8ffc3f02af305c00), + ("6fa47550938183", 0x6fa4755093818300), + ("aa80a565df5e7fc", 0xaa80a565df5e7fc0), + ("01614b5e968e121", 0x01614b5e968e1210), + ("aa05238e7bd3ee7c", 0xaa05238e7bd3ee7c), + ("48a23db9c2963e5b", 0x48a23db9c2963e5b), + ] + for (token, id) in tests { + let ci = CellId(token: token) + XCTAssertEqual(ci.id, id) + let token2 = ci.toToken() + XCTAssertEqual(token2, token) + } + } + + func testCellIdFromTokenErrorCases() { + let noneToken = CellId(id: 0).toToken() + XCTAssertEqual(noneToken, "X") + let noneId = CellId(token: noneToken) + XCTAssertEqual(noneId, CellId(id: 0)) + let tests = [ + "876b e99", + "876bee99\n", + "876[ee99", + " 876bee99"] + for test in tests { + let ci = CellId(token: test) + XCTAssertEqual(ci.id, 0) + } + } + + func r(_ x0: Double, _ y0: Double, _ x1: Double, _ y1: Double) -> R2Rect { + return R2Rect(p0: R2Point(x: x0, y: y0), p1: R2Point(x: x1, y: y1)) + } + + func testIJLevelToBoundUV() { + let maxIJ = 1 << CellId.maxLevel - 1 + let tests = [ + // The i/j space is [0, 2^30 - 1) which maps to [-1, 1] for the + // x/y axes of the face surface. Results are scaled by the size of a cell + // at the given level. At level 0, everything is one cell of the full size + // of the space. At maxLevel, the bounding rect is almost floating point + // noise. + + // What should be out of bounds values, but passes the C++ code as well. + (-1, -1, 0, r(-5, -5, -1, -1)), + (-1 * maxIJ, -1 * maxIJ, 0, r(-5, -5, -1, -1)), + (-1, -1, CellId.maxLevel, r(-1.0000000024835267, -1.0000000024835267, -1, -1)), +// (0, 0, CellId.maxLevel + 1, r(-1, -1, -1, -1)), + + // Minimum i,j at different levels + (0, 0, 0, r(-1, -1, 1, 1)), + (0, 0, CellId.maxLevel / 2, r(-1, -1, -0.999918621033430099, -0.999918621033430099)), + (0, 0, CellId.maxLevel, r(-1, -1, -0.999999997516473060, -0.999999997516473060)), + + // Just a hair off the outer bounds at different levels. + (1, 1, 0, r(-1, -1, 1, 1)), + (1, 1, CellId.maxLevel / 2, r(-1, -1, -0.999918621033430099, -0.999918621033430099)), + (1, 1, CellId.maxLevel, r(-0.9999999975164731, -0.9999999975164731, -0.9999999950329462, -0.9999999950329462)), + + // Center point of the i,j space at different levels. + (maxIJ / 2, maxIJ / 2, 0, r(-1, -1, 1, 1)), + (maxIJ / 2, maxIJ / 2, CellId.maxLevel / 2, r(-0.000040691345930099, -0.000040691345930099, 0, 0)), + (maxIJ / 2, maxIJ / 2, CellId.maxLevel, r(-0.000000001241763433, -0.000000001241763433, 0, 0)), + + // Maximum i, j at different levels. + (maxIJ, maxIJ, 0, r(-1, -1, 1, 1)), + (maxIJ, maxIJ, CellId.maxLevel / 2, r(0.999918621033430099, 0.999918621033430099, 1, 1)), + (maxIJ, maxIJ, CellId.maxLevel, r(0.999999997516473060, 0.999999997516473060, 1, 1)), + ] + + for (i, j, level, want) in tests { + let uv = CellId.ijLevelToBoundUV(i: i, j: j, level: level) + XCTAssertEqual(uv.x.lo, want.x.lo, accuracy: 1e-14) + XCTAssertEqual(uv.x.hi, want.x.hi, accuracy: 1e-14) + XCTAssertEqual(uv.y.lo, want.y.lo, accuracy: 1e-14) + XCTAssertEqual(uv.y.hi, want.y.hi, accuracy: 1e-14) + } + } + + func testCommonAncestorLevel() { + let tests = [ + // Identical cell IDs. + (CellId(face: 0), CellId(face: 0), 0, true), + (CellId(face: 0).childBegin(30), CellId(face: 0).childBegin(30), 30, true), + // One cell is a descendant of the other. + (CellId(face: 0).childBegin(30), CellId(face: 0), 0, true), + (CellId(face: 5), CellId(face: 5).childEnd(30).prev(), 0, true), + // No common ancestors. + (CellId(face: 0), CellId(face: 5), 0, false), + (CellId(face: 2).childBegin(30), CellId(face: 3).childBegin(20), 0, false), + // Common ancestor distinct from both. + (CellId(face: 5).childBegin(9).next().childBegin(15), CellId(face: 5).childBegin(9).childBegin(20), 8, true), + (CellId(face: 0).childBegin(2).childBegin(30), CellId(face: 0).childBegin(2).next().childBegin(5), 1, true)] + for (ci, other, want, wantOk) in tests { + guard let got = ci.commonAncestorLevel(other) else { + if wantOk { + XCTFail() + } + continue + } + XCTAssertEqual(got, want) + } + } + + func testFindMSBSetNonZero64() { + var testOne = UInt64(0x800000000000000) << 4 + var testAll = ~UInt64(0) + var testSome = UInt64(0xFEDCBA987654321) << 4 + for i_ in 0..<63 { + let i = 63 - i_ + XCTAssertEqual(CellId.findMSBSetNonZero64(testOne), i) + XCTAssertEqual(CellId.findMSBSetNonZero64(testAll), i) + XCTAssertEqual(CellId.findMSBSetNonZero64(testSome), i) + testOne >>= 1 + testAll >>= 1 + testSome >>= 1 + } + } + + func testAdvance() { + let tests = [ + (CellId(face: 0).childBegin(0), 7, CellId(face: 5).childEnd(0)), + (CellId(face: 0).childBegin(0), 12, CellId(face: 5).childEnd(0)), + (CellId(face: 5).childEnd(0), -7, CellId(face: 0).childBegin(0)), + (CellId(face: 5).childEnd(0), -12000000, CellId(face: 0).childBegin(0)), + (CellId(face: 0).childBegin(5), 500, CellId(face: 5).childEnd(5).advance(Int64(500 - (6 << (2 * 5))))), + (CellId(face: 3, pos: 0x12345678, level: CellId.maxLevel-4).childBegin(CellId.maxLevel), 256, CellId(face: 3, pos: 0x12345678, level: CellId.maxLevel-4).next().childBegin(CellId.maxLevel)), + (CellId(face: 1, pos: 0, level: CellId.maxLevel), 4 << (2 * CellId.maxLevel), CellId(face: 5, pos: 0, level: CellId.maxLevel))] + for (ci, steps, want) in tests { + XCTAssertEqual(ci.advance(Int64(steps)), want) + } + } + +} diff --git a/Sphere2GoLibTests/S2CellTests.swift b/Sphere2GoLibTests/S2CellTests.swift new file mode 100644 index 0000000..44e8d02 --- /dev/null +++ b/Sphere2GoLibTests/S2CellTests.swift @@ -0,0 +1,202 @@ +// +// S2CellTests.swift +// Sphere2 +// + + +import XCTest + +class S2CellTests: XCTestCase { + + override func setUp() { + super.setUp() + // Put setup code here. This method is called before the invocation of each test method in the class. + } + + override func tearDown() { + // Put teardown code here. This method is called after the invocation of each test method in the class. + super.tearDown() + } + + + func testCellObjectSize() { + // maxCellSize is the upper bounds on the number of bytes we want the Cell object to ever be. + let maxCellSize = 48 + XCTAssert(MemoryLayout.size(ofValue: Cell.self) <= maxCellSize) + } + + func testCellFaces() { + // call this to initialize the lookup tables + CellId.setup() + // hash counters + var edgeCounts = [R3Vector: Int]() + var vertexCounts = [R3Vector: Int]() + // + for face in 0..<6 { + let id = CellId(face: face) + let cell = Cell(id: id) + XCTAssertEqual(cell.id, id) + XCTAssertEqual(cell.face, UInt8(face)) + XCTAssertEqual(cell.level, 0) + // Top-level faces have alternating orientations to get RHS coordinates. +// XCTAssertEqual(cell.orientation, UInt8(face & CellId.swapMask)) + let edges = (0..<4).map { cell.edge($0).v } + let vertices = (0..<4).map { cell.vertex($0).v } + XCTAssertFalse(cell.isLeaf()) + for k in 0..<4 { + edgeCounts[edges[k]] = (edgeCounts[edges[k]] ?? 0) + 1 + vertexCounts[vertices[k]] = (vertexCounts[vertices[k]] ?? 0) + 1 + XCTAssertEqual(vertices[k].dot(edges[k]), 0.0, accuracy: 1e-14) + XCTAssertEqual(vertices[(k + 1) & 3].dot(edges[k]), 0.0, accuracy: 1e-14) + XCTAssertEqual(vertices[k].cross(vertices[(k + 1) & 3]).normalize().dot(edges[k]), 1.0, accuracy: 1e-14) + } + } + // Check that edges have multiplicity 2 and vertices have multiplicity 3. + for (_, v) in edgeCounts { + XCTAssertEqual(v, 2, "\(v)") + } + for (_, v) in vertexCounts { + XCTAssertEqual(v, 3) + } + } + + func testAreas() { + // relative error bounds for each type of area computation + let exactError = log(1 + 1e-6) + let approxError = log(1.03) + let avgError = log(1 + 1e-15) + // Test 1. Check the area of a top level cell. + let level1Cell = CellId(id: 0x1000000000000000) + let wantArea = 4.0 * .pi / 6.0 + XCTAssertEqual(Cell(id: level1Cell).exactArea(), wantArea, accuracy: 1e-14) + // Test 2. Iterate inwards from this cell, checking at every level that + // the sum of the areas of the children is equal to the area of the parent. + var childIndex = 1 + var cell = CellId(id: 0x1000000000000000) + while (cell.level() < 21) { + var exactArea = 0.0 + var approxArea = 0.0 + var avgArea = 0.0 + for child in cell.children() { + exactArea += Cell(id: child).exactArea() + approxArea += Cell(id: child).approxArea() + avgArea += Cell(id: child).averageArea() + } + XCTAssertEqual(Cell(id: cell).exactArea(), exactArea, accuracy: 1e-14) + childIndex = (childIndex + 1) % 4 + // For ExactArea(), the best relative error we can expect is about 1e-6 + // because the precision of the unit vector coordinates is only about 1e-15 + // and the edge length of a leaf cell is about 1e-9. + XCTAssert(fabs(log(exactArea / Cell(id: cell).exactArea())) <= exactError) + // For ApproxArea(), the areas are accurate to within a few percent. + XCTAssert(fabs(log(approxArea / Cell(id: cell).approxArea())) <= approxError) + // For AverageArea(), the areas themselves are not very accurate, but + // the average area of a parent is exactly 4 times the area of a child. + XCTAssert(fabs(log(avgArea / Cell(id: cell).averageArea())) <= avgError) + // + cell = cell.children()[childIndex] + } + } + + func testIntersectsCell() { + let f0c2 = CellId(face: 0).childBegin(2) + let tests = [ + (Cell(id: f0c2), Cell(id: f0c2), true), + (Cell(id: f0c2), Cell(id: f0c2.childBegin(5)), true), + (Cell(id: f0c2), Cell(id: f0c2.next()), false)] + for (c, oc, want) in tests { + XCTAssertEqual(c.intersects(oc), want) + } + } + + func testContainsCell() { + let f0c2 = CellId(face: 0).childBegin(2) + let tests = [ + (Cell(id: f0c2), Cell(id: f0c2), true), + (Cell(id: f0c2), Cell(id: f0c2.childBegin(5)), true), + (Cell(id: f0c2.childBegin(5)), Cell(id: f0c2), false), + (Cell(id: f0c2.next()), Cell(id: f0c2), false), + (Cell(id: f0c2), Cell(id: f0c2.next()), false)] + for (c, oc, want) in tests { + XCTAssertEqual(c.contains(oc), want) + } + } + + func testRectBound() { + let tests = [ + (50.0, 50.0), + (-50, 50), + (50, -50), + (-50, -50), + (0, 0), + (0, 180), + (0, -179)] + for (lat, lng) in tests { + let c = Cell(latLng: LatLng(latDegrees: lat, lngDegrees: lng)) + let rect = c.rectBound() + for i in 0..<4 { + XCTAssertTrue(rect.contains(LatLng(point: c.vertex(i)))) + } + } + } + + func testRectBoundAroundPoleMinLat() { + // call this to initialize the lookup tables + CellId.setup() + // + let f2 = Cell(id: CellId(face: 2, pos: 0, level: 0)).rectBound() + XCTAssertFalse(f2.contains(LatLng(latDegrees: 3.0, lngDegrees: 0))) + XCTAssertTrue(f2.contains(LatLng(latDegrees: 50.0, lngDegrees: 0))) + let f5 = Cell(id: CellId(face: 5, pos: 0, level: 0)).rectBound() + XCTAssertFalse(f5.contains(LatLng(latDegrees: -3.0, lngDegrees: 0))) + XCTAssertTrue(f5.contains(LatLng(latDegrees: -50.0, lngDegrees: 0))) + } + + func testCapBound() { + let c = Cell(id: CellId(face: 0).childBegin(20)) + let s2Cap = c.capBound() + for i in 0..<4 { + XCTAssertTrue(s2Cap.contains(c.vertex(i))) + } + } + + func testContainsPoint() { + let f0c2 = CellId(face: 0).childBegin(2) + XCTAssertTrue(Cell(id: f0c2).contains(Cell(id: f0c2.childBegin(5)).vertex(1))) + XCTAssertTrue(Cell(id: f0c2).contains(Cell(id: f0c2).vertex(1))) + XCTAssertFalse(Cell(id: f0c2.childBegin(5)).contains(Cell(id: f0c2.next().childBegin(5)).vertex(1))) + } + + func testContainsPointConsistentWithCellIdFromPoint() { + // Construct many points that are nearly on a Cell edge, and verify that + // Cell(id: cellIDFromPoint(p)).Contains(p) is always true. + for _ in 0..<1000 { + let cell = Cell(id: randomCellId()) + let i1 = randomUniformInt(n: 4) + let i2 = (i1 + 1) & 3 + let v1 = cell.vertex(i1) + let v2 = samplePointFromCap(c: S2Cap(center: cell.vertex(i2), angle: epsilon)) + let p = interpolate(t: randomFloat64(), a: v1, b: v2) + XCTAssertTrue(Cell(id: CellId(point: p)).contains(p)) + } + } + + func testContainsPointContainsAmbiguousPoint() { + // This tests a case where S2CellId returns the "wrong" cell for a point + // that is very close to the cell edge. (ConsistentWithS2CellIdFromPoint + // generates more examples like this.) + // + // The Point below should have x = 0, but conversion from LatLng to + // (x, y, z) gives x = ~6.1e-17. When xyz is converted to uv, this gives + // u = -6.1e-17. However when converting to st, which has a range of [0, 1], + // the low precision bits of u are lost and we wind up with s = 0.5. + // cellIDFromPoint then chooses an arbitrary neighboring cell. + // + // This tests that Cell.ContainsPoint() expands the cell bounds sufficiently + // so that the returned cell is still considered to contain p. + let p = LatLng(latDegrees: -2, lngDegrees: 90).toPoint() + let cell = Cell(id: CellId(point: p).parent(1)) + XCTAssertTrue(cell.contains(p)) + } + +} diff --git a/Sphere2GoLibTests/S2CellUnionTests.swift b/Sphere2GoLibTests/S2CellUnionTests.swift new file mode 100644 index 0000000..94fde0b --- /dev/null +++ b/Sphere2GoLibTests/S2CellUnionTests.swift @@ -0,0 +1,396 @@ +// +// CellUnionTests.swift +// Sphere2 +// + +import XCTest + +class S2CellUnionTests: XCTestCase { + + override func setUp() { + super.setUp() + // call this to initialize the lookup tables + CellId.setup() + } + + override func tearDown() { + // Put teardown code here. This method is called after the invocation of each test method in the class. + super.tearDown() + } + + func testNormalization() { + let cu = CellUnion(ids: [ + 0x80855c0000000000, // A: a cell over Pittsburg CA + 0x80855d0000000000, // B, a child of A + 0x8085634000000000, // first child of X, disjoint from A + 0x808563c000000000, // second child of X + 0x80855dc000000000, // a child of B + 0x808562c000000000, // third child of X + 0x8085624000000000, // fourth child of X + 0x80855d0000000000]) // B again + let exp = CellUnion(ids: [ + 0x80855c0000000000, // A + 0x8085630000000000]) // X + cu.normalize() + XCTAssertEqual(cu, exp) + + // add a redundant cell + /* TODO(dsymonds) + cu.Add(0x808562c000000000) + if !reflect.DeepEqual(cu, exp) { + t.Errorf("after redundant add, got %v, want %v", cu, exp) + } + */ + } + + func testCellUnionBasic() { + let empty = CellUnion(ids: []) + empty.normalize() + XCTAssertEqual(empty.count, 0) + let face1Id = CellId(face: 1) + let face1Cell = Cell(id: face1Id) + let face1Union = CellUnion(cellIds: [face1Id]) + face1Union.normalize() + XCTAssertEqual(face1Union.count, 1) + XCTAssertEqual(face1Id, face1Union[0]) + XCTAssertTrue(face1Union.contains(face1Cell)) + let face2Id = CellId(face: 2) + let face2Cell = Cell(id: face2Id) + let face2Union = CellUnion(cellIds: [face2Id]) + face2Union.normalize() + XCTAssertEqual(face2Union.count, 1) + XCTAssertEqual(face2Id, face2Union[0]) + XCTAssertFalse(face1Union.contains(face2Cell)) + } + + func testCellUnion() { + let tests: [([UInt64], [UInt64], [UInt64], [UInt64])] = [ + // Single cell around NYC, and some simple nearby probes + ([ + 0x89c25c0000000000], + [ + CellId(id: 0x89c25c0000000000).childBegin().id, + CellId(id: 0x89c25c0000000000).childBegin(28).id], + [ + CellId(id: 0x89c25c0000000000).immediateParent().id, + CellId(face: CellId(id: 0x89c25c0000000000).face()).id], // the whole face + [ + CellId(id: 0x89c25c0000000000).next().id, // Cell next to this one at same level + CellId(id: 0x89c25c0000000000).next().childBegin(28).id, // Cell next to this one at deep level + 0x89c2700000000000, // Big(er) neighbor cell + 0x89e9000000000000, // Very big next door cell. + 0x89c1000000000000]), // Very big cell, smaller value than probe + // NYC and SFO: + ([ + 0x89c25b0000000000, // NYC + 0x89c2590000000000, // NYC + 0x89c2f70000000000, // NYC + 0x89c2f50000000000, // NYC + 0x8085870000000000, // SFO + 0x8085810000000000, // SFO + 0x808f7d0000000000, // SFO + 0x808f7f0000000000], // SFO + [ + 0x808f7ef300000000, // SFO + 0x808f7e5cf0000000, // SFO + 0x808587f000000000, // SFO + 0x89c25ac000000000, // NYC + 0x89c259a400000000, // NYC + 0x89c258fa10000000, // NYC + 0x89c258f174007000], // NYC + [ + 0x808c000000000000, // Big SFO + 0x89c4000000000000], // Big NYC + [ + 0x89c15a4fcb1bb000, // outside NYC + 0x89c15a4e4aa95000, // outside NYC + 0x8094000000000000, // outside SFO (big) + 0x8096f10000000000, // outside SFO (smaller) + + 0x87c0000000000000]), // Midwest very big + ([ + // CellUnion with cells at many levels: + 0x8100000000000000, // starting around california + 0x8740000000000000, // adjacent cells at increasing + 0x8790000000000000, // levels, moving eastward. + 0x87f4000000000000, + 0x87f9000000000000, // going down across the midwest + 0x87ff400000000000, + 0x87ff900000000000, + 0x87fff40000000000, + 0x87fff90000000000, + 0x87ffff4000000000, + 0x87ffff9000000000, + 0x87fffff400000000, + 0x87fffff900000000, + 0x87ffffff40000000, + 0x87ffffff90000000, + 0x87fffffff4000000, + 0x87fffffff9000000, + 0x87ffffffff400000], // to a very small cell in Wisconsin + [ + 0x808f400000000000, + 0x80eb118b00000000, + 0x8136a7a11d000000, + 0x8136a7a11dac0000, + 0x876c7c0000000000, + 0x87f96d0000000000, + 0x87ffffffff400000], + [ + CellId(id: 0x8100000000000000).immediateParent().id, + CellId(id: 0x8740000000000000).immediateParent().id], + [ + 0x52aaaaaaab300000, + 0x52aaaaaaacd00000, + 0x87fffffffa100000, + 0x87ffffffed500000, + 0x87ffffffa0100000, + 0x87fffffed5540000, + 0x87fffffed6240000, + 0x52aaaacccb340000, + 0x87a0000400000000, + 0x87a000001f000000, + 0x87a0000029d00000, + 0x9500000000000000])] + for (cells, contained, overlaps, disjoint) in tests { + let union = CellUnion(ids: cells) + union.normalize() + // Ensure self-containment tests are correct. + for id in cells { + XCTAssertTrue(union.intersects(CellId(id: id))) + XCTAssertTrue(union.contains(CellId(id: id))) + } + // Test for containment specified in test case. + for id in contained { + XCTAssertTrue(union.intersects(CellId(id: id))) + XCTAssertTrue(union.contains(CellId(id: id))) + } + // Make sure the CellUnion intersect these cells but do not contain. + for id in overlaps { + XCTAssertTrue(union.intersects(CellId(id: id))) + XCTAssertFalse(union.contains(CellId(id: id))) + } + // Negative cases make sure the CellUnion neither contain nor intersect these cells + for id in disjoint { + XCTAssertFalse(union.intersects(CellId(id: id))) + XCTAssertFalse(union.contains(CellId(id: id))) + } + } + } + + func addCells(id: CellId, selected: Bool, input: inout [CellId], expected: inout [CellId]) { + // Decides whether to add "id" and/or some of its descendants to the test case. If "selected" + // is true, then the region covered by "id" *must* be added to the test case (either by adding + // "id" itself, or some combination of its descendants, or both). If cell ids are to the test + // case "input", then the corresponding expected result after simplification is added to + // "expected". + if id.id == 0 { + // Initial call: decide whether to add cell(s) from each face. + for face in 0..<6 { + addCells(id: CellId(face: face), selected: false, input: &input, expected: &expected) + } + return + } + var selected = selected + if id.isLeaf() { + // The oneIn() call below ensures that the parent of a leaf cell will always be selected (if + // we make it that far down the hierarchy). + XCTAssertTrue(selected) + input.append(id) + return + } + // The following code ensures that the probability of selecting a cell at each level is + // approximately the same, i.e. we test normalization of cells at all levels. + if !selected && oneIn(n: CellId.maxLevel-id.level()) { + // Once a cell has been selected, the expected output is predetermined. We then make sure + // that cells are selected that will normalize to the desired output. + expected.append(id) + selected = true + } + // With the rnd.OneIn() constants below, this function adds an average + // of 5/6 * (kMaxLevel - level) cells to "input" where "level" is the + // level at which the cell was first selected (level 15 on average). + // Therefore the average number of input cells in a test case is about + // (5/6 * 15 * 6) = 75. The average number of output cells is about 6. + // If a cell is selected, we add it to "input" with probability 5/6. + var added = false + if selected && !oneIn(n: 6) { + input.append(id) + added = true + } + var numChildren = 0 + var child = id.childBegin() + while child != id.childEnd() { + // If the cell is selected, on average we recurse on 4/12 = 1/3 child. + // This intentionally may result in a cell and some of its children + // being included in the test case. + // + // If the cell is not selected, on average we recurse on one child. + // We also make sure that we do not recurse on all 4 children, since + // then we might include all 4 children in the input case by accident + // (in which case the expected output would not be correct). + let recurse = oneIn(n: selected ? 12 : 4) + if recurse && numChildren < 3 { + addCells(id: child, selected: selected, input: &input, expected: &expected) + numChildren += 1 + } + // If this cell was selected but the cell itself was not added, we + // must ensure that all 4 children (or some combination of their + // descendants) are added. + if selected && !added { + addCells(id: child, selected: selected, input: &input, expected: &expected) + } + child = child.next() + } + } + + func testNormalizePseudoRandom() { + // Try a bunch of random test cases, and keep track of average statistics for normalization (to + // see if they agree with the analysis above). + var inSum = 0 + var outSum = 0 + let iters = 20 + // + for _ in 0.. 1 { + +// if cellunion.intersects(j.immediateParent().immediateParent()) == false) +// if cellunion.intersects(j.Parent(0)) == false +// } + } + // + if !j.isLeaf() { + XCTAssertTrue(cellunion.contains(j.childBegin())) + XCTAssertTrue(cellunion.intersects(j.childBegin())) + XCTAssertTrue(cellunion.contains(j.childEnd().prev())) + XCTAssertTrue(cellunion.intersects(j.childEnd().prev())) + XCTAssertTrue(cellunion.contains(j.childBegin(CellId.maxLevel))) + XCTAssertTrue(cellunion.intersects(j.childBegin(CellId.maxLevel))) + } + } + // + for exp in expected { + if !exp.isFace() { + XCTAssertFalse(cellunion.contains(exp.parent(exp.level() - 1))) + XCTAssertFalse(cellunion.contains(exp.parent(0))) + } + } + // + var test = [CellId]() + var dummy = [CellId]() + addCells(id: CellId(id: 0), selected: false, input: &test, expected: &dummy) + for j in test { + var intersects = false + var contains = false + for k in expected { + if k.contains(j) { + contains = true + } + if k.intersects(j) { + intersects = true + } + } + XCTAssertEqual(cellunion.contains(j), contains) + XCTAssertEqual(cellunion.intersects(j), intersects) + } + } + } + + func testDenormalize() { + let tests = [ + ("not expanded, level mod == 1", 10, 1, + CellUnion(cellIds: [ + CellId(face: 2).childBegin(11), + CellId(face: 2).childBegin(11), + CellId(face: 3).childBegin(14), + CellId(face: 0).childBegin(10)]), + CellUnion(cellIds: [ + CellId(face: 2).childBegin(11), + CellId(face: 2).childBegin(11), + CellId(face: 3).childBegin(14), + CellId(face: 0).childBegin(10)])), + ("not expanded, level mod > 1", 10, 2, + CellUnion(cellIds: [ + CellId(face: 2).childBegin(12), + CellId(face: 2).childBegin(12), + CellId(face: 3).childBegin(14), + CellId(face: 0).childBegin(10)]), + CellUnion(cellIds: [ + CellId(face: 2).childBegin(12), + CellId(face: 2).childBegin(12), + CellId(face: 3).childBegin(14), + CellId(face: 0).childBegin(10)])), + ("expended, (level - min_level) is not multiple of level mod", 10, 3, + CellUnion(cellIds: [ + CellId(face: 2).childBegin(12), + CellId(face: 5).childBegin(11)]), + CellUnion(cellIds: [ + CellId(face: 2).childBegin(12).children()[0], + CellId(face: 2).childBegin(12).children()[1], + CellId(face: 2).childBegin(12).children()[2], + CellId(face: 2).childBegin(12).children()[3], + CellId(face: 5).childBegin(11).children()[0].children()[0], + CellId(face: 5).childBegin(11).children()[0].children()[1], + CellId(face: 5).childBegin(11).children()[0].children()[2], + CellId(face: 5).childBegin(11).children()[0].children()[3], + CellId(face: 5).childBegin(11).children()[1].children()[0], + CellId(face: 5).childBegin(11).children()[1].children()[1], + CellId(face: 5).childBegin(11).children()[1].children()[2], + CellId(face: 5).childBegin(11).children()[1].children()[3], + CellId(face: 5).childBegin(11).children()[2].children()[0], + CellId(face: 5).childBegin(11).children()[2].children()[1], + CellId(face: 5).childBegin(11).children()[2].children()[2], + CellId(face: 5).childBegin(11).children()[2].children()[3], + CellId(face: 5).childBegin(11).children()[3].children()[0], + CellId(face: 5).childBegin(11).children()[3].children()[1], + CellId(face: 5).childBegin(11).children()[3].children()[2], + CellId(face: 5).childBegin(11).children()[3].children()[3]])), + ("expended, level < min_level", 10, 3, + CellUnion(cellIds: [ + CellId(face: 2).childBegin(9)]), + CellUnion(cellIds: [ + CellId(face: 2).childBegin(9).children()[0], + CellId(face: 2).childBegin(9).children()[1], + CellId(face: 2).childBegin(9).children()[2], + CellId(face: 2).childBegin(9).children()[3]]))] + for (_, minL, lMod, cu, exp) in tests { + cu.denormalize(minLevel: minL, levelMod: lMod) + XCTAssertEqual(cu, exp) + } + } + + func testCellUnionRectBound() { + let tests = [ + (CellUnion(ids: []), S2Rect.empty), + (CellUnion(cellIds: [CellId(face: 1)]), S2Rect( + lat: R1Interval(lo: -.pi / 4, hi: .pi / 4), + lng: S1Interval(lo: .pi / 4, hi: 3 * .pi / 4))), + (CellUnion(ids: [0x808c000000000000]), S2Rect( // Big SFO + lat: R1Interval(lo: 34.644220547108482 * toRadians, hi: 38.011928357226651 * toRadians), + lng: S1Interval(lo: -124.508522987668428 * toRadians, hi: -121.628309835221216 * toRadians))), + (CellUnion(ids: [0x89c4000000000000]),S2Rect( // Big NYC + lat: R1Interval(lo: 38.794595155857657 * toRadians, hi: 41.747046884651063 * toRadians), + lng: S1Interval(lo: -76.456308667788633 * toRadians, hi: -73.465162142654819 * toRadians))), + (CellUnion(ids: [0x89c4000000000000, 0x808c000000000000]), S2Rect( // Big NYC, Big SFO + lat: R1Interval(lo: 34.644220547108482 * toRadians, hi: 41.747046884651063 * toRadians), + lng: S1Interval(lo: -124.508522987668428 * toRadians, hi: -73.465162142654819 * toRadians)))] + for (cu, want) in tests { + XCTAssert(rectsApproxEqual(a: cu.rectBound(), b: want, tolLat: epsilon, tolLng: epsilon)) + } + } + +} diff --git a/Sphere2GoLibTests/S2LatLngTests.swift b/Sphere2GoLibTests/S2LatLngTests.swift new file mode 100644 index 0000000..e2d600d --- /dev/null +++ b/Sphere2GoLibTests/S2LatLngTests.swift @@ -0,0 +1,97 @@ +// +// S2LatLngTests.swift +// Sphere2 +// + +import XCTest + +// package s2 +// import s1 + +class S2LatLngTests: XCTestCase { + + override func setUp() { + super.setUp() + // Put setup code here. This method is called before the invocation of each test method in the class. + } + + override func tearDown() { + // Put teardown code here. This method is called after the invocation of each test method in the class. + super.tearDown() + } + + func l(_ latDegrees: Double, _ lngDegrees: Double) -> LatLng { + return LatLng(latDegrees: latDegrees, lngDegrees: lngDegrees) + } + + func testLatLngNormalized() { + let tests = [ + ("Valid lat/lng", l(21.8275043, 151.1979675), l(21.8275043, 151.1979675)), + ("Valid lat/lng in the West", l(21.8275043, -151.1979675), l(21.8275043, -151.1979675)), + ("Beyond the North pole", l(95, 151.1979675), l(90, 151.1979675)), + ("Beyond the South pole", l(-95, 151.1979675), l(-90, 151.1979675)), + ("At the date line (from East)", l(21.8275043, 180), l(21.8275043, 180)), + ("At the date line (from West)", l(21.8275043, -180), l(21.8275043, -180)), + ("Across the date line going East", l(21.8275043, 181.0012), l(21.8275043, -178.9988)), + ("Across the date line going West", l(21.8275043, -181.0012), l(21.8275043, 178.9988)), + ("All wrong", l(256, 256), l(90, -104))] + for (desc, pos, want) in tests { + let got = pos.normalize() + NSLog("\(got)") + XCTAssertTrue(got.isValid(), desc) + XCTAssert(got.distance(want) < 1e-13 * toRadians, desc) + } + } + + func testLatLngString() { + XCTAssertEqual(LatLng(latDegrees: sqrt(2.0), lngDegrees: -sqrt(5.0)).description, "[1.4142136, -2.2360680]") + } + + func testLatLngPointConversion() { + let eps = 1e-14 + // All test cases here have been verified against the C++ S2 implementation. + let tests = [ + (0, 0, 1, 0, 0), + (90, 0, 6.12323e-17, 0, 1), + (-90, 0, 6.12323e-17, 0, -1), + (0, 180, -1, 1.22465e-16, 0), + (0, -180, -1, -1.22465e-16, 0), + (90, 180, -6.12323e-17, 7.4988e-33, 1), + (90, -180, -6.12323e-17, -7.4988e-33, 1), + (-90, 180, -6.12323e-17, 7.4988e-33, -1), + (-90, -180, -6.12323e-17, -7.4988e-33, -1), + (-81.82750430354997, 151.19796752929685, -0.12456788151479525, 0.0684875268284729, -0.989844584550441)] + for (lat, lng, x, y, z) in tests { + let ll = LatLng(latDegrees: lat, lngDegrees: lng) + let p = ll.toPoint() + // TODO(mikeperrow): Port Point.ApproxEquals, then use here. + XCTAssertEqual(p.x, x, accuracy: eps) + XCTAssertEqual(p.y, y, accuracy: eps) + XCTAssertEqual(p.z, z, accuracy: eps) + let ll2 = LatLng(point: p) + // We need to be careful here, since if the latitude is +/- 90, any longitude + // is now a valid conversion. + let isPolar = (lat == 90 || lat == -90) + XCTAssertEqual(ll2.lat, lat * toRadians, accuracy: eps) + if !isPolar { + XCTAssertEqual(ll2.lng, lng * toRadians, accuracy: eps) + } + } + } + + func testLatLngDistance() { + // Based on C++ S2LatLng::TestDistance. + let tests = [ + (90.0, 0.0, 90.0, 0.0, 0.0, 0.0), + (-37, 25, -66, -155, 77, 1e-13), + (0, 165, 0, -80, 115, 1e-13), + (47, -127, -47, 53, 180, 2e-6)] + for (lat1, lng1, lat2, lng2, want, tolerance) in tests { + let ll1 = LatLng(latDegrees: lat1, lngDegrees: lng1) + let ll2 = LatLng(latDegrees: lat2, lngDegrees: lng2) + let d = ll1.distance(ll2) + XCTAssertEqual(d, want * toRadians, accuracy: tolerance) + } + } + +} diff --git a/Sphere2GoLibTests/S2LoopTests.swift b/Sphere2GoLibTests/S2LoopTests.swift new file mode 100644 index 0000000..5cac757 --- /dev/null +++ b/Sphere2GoLibTests/S2LoopTests.swift @@ -0,0 +1,295 @@ +// +// S2LoopTests.swift +// Sphere2 +// + + +import XCTest + + +func makeLoop(_ s: String) -> S2Loop { + let points = parsePoints(s) + return S2Loop.loopFromPoints(points) +} + + +class S2LoopTests: XCTestCase { + + override func setUp() { + super.setUp() + // Put setup code here. This method is called before the invocation of each test method in the class. + } + + override func tearDown() { + // Put teardown code here. This method is called after the invocation of each test method in the class. + super.tearDown() + } + + // The northern hemisphere, defined using two pairs of antipodal points. + let northHemi = makeLoop("0:-180, 0:-90, 0:0, 0:90") + + // The northern hemisphere, defined using three points 120 degrees apart. + let northHemi3 = makeLoop("0:-180, 0:-60, 0:60") + + // The southern hemisphere, defined using two pairs of antipodal points. + let southHemi = makeLoop("0:90, 0:0, 0:-90, 0:-180") + + // The western hemisphere, defined using two pairs of antipodal points. + let westHemi = makeLoop("0:-180, -90:0, 0:0, 90:0") + + // The eastern hemisphere, defined using two pairs of antipodal points. + let eastHemi = makeLoop("90:0, 0:0, -90:0, 0:-180") + + // The "near" hemisphere, defined using two pairs of antipodal points. + let nearHemi = makeLoop("0:-90, -90:0, 0:90, 90:0") + + // The "far" hemisphere, defined using two pairs of antipodal points. + let farHemi = makeLoop("90:0, 0:90, -90:0, 0:-90") + + // A spiral stripe that slightly over-wraps the equator. + let candyCane = makeLoop("-20:150, -20:-70, 0:70, 10:-150, 10:70, -10:-70") + + // A small clockwise loop in the northern & eastern hemisperes. + let smallNECW = makeLoop("35:20, 45:20, 40:25") + + // Loop around the north pole at 80 degrees. + let arctic80 = makeLoop("80:-150, 80:-30, 80:90") + + // Loop around the south pole at 80 degrees. + let antarctic80 = makeLoop("-80:120, -80:0, -80:-120") + + // A completely degenerate triangle along the equator that RobustCCW() + // considers to be CCW. + let lineTriangle = makeLoop("0:1, 0:2, 0:3") + + // A nearly-degenerate CCW chevron near the equator with very long sides + // (about 80 degrees). Its area is less than 1e-640, which is too small + // to represent in double precision. + let skinnyChevron = makeLoop("0:0, -1e-320:80, 0:1e-320, 1e-320:80") + + // A diamond-shaped loop around the point 0:180. + let loopA = makeLoop("0:178, -1:180, 0:-179, 1:-180") + + // Like loopA, but the vertices are at leaf cell centers. + let snappedLoopA = S2Loop.loopFromPoints([ + CellId(latLng: parseLatLngs("0:178")[0]).point(), + CellId(latLng: parseLatLngs("-1:180")[0]).point(), + CellId(latLng: parseLatLngs("0:-179")[0]).point(), + CellId(latLng: parseLatLngs("1:-180")[0]).point()]) + + // A different diamond-shaped loop around the point 0:180. + let loopB = makeLoop("0:179, -1:180, 0:-178, 1:-180") + + // The intersection of A and B. + let aIntersectB = makeLoop("0:179, -1:180, 0:-179, 1:-180") + + // The union of A and B. + let aUnionB = makeLoop("0:178, -1:180, 0:-178, 1:-180") + + // A minus B (concave). + let aMinusB = makeLoop("0:178, -1:180, 0:179, 1:-180") + + // B minus A (concave). + let bMinusA = makeLoop("0:-179, -1:180, 0:-178, 1:-180") + + // A shape gotten from A by adding a triangle to one edge, and + // subtracting a triangle from the opposite edge. + let loopC = makeLoop("0:178, 0:180, -1:180, 0:-179, 1:-179, 1:-180") + + // A shape gotten from A by adding a triangle to one edge, and + // adding another triangle to the opposite edge. + let loopD = makeLoop("0:178, -1:178, -1:180, 0:-179, 1:-179, 1:-180") + + // 3------------2 + // | | ^ + // | 7-8 b-c | | + // | | | | | | Latitude | + // 0--6-9--a-d--1 | + // | | | | | + // | f-e | +-----------> + // | | Longitude + // 4------------5 + // + // Important: It is not okay to skip over collinear vertices when + // defining these loops (e.g. to define loop E as "0,1,2,3") because S2 + // uses symbolic perturbations to ensure that no three vertices are + // *ever* considered collinear (e.g., vertices 0, 6, 9 are not + // collinear). In other words, it is unpredictable (modulo knowing the + // details of the symbolic perturbations) whether 0123 contains 06123 + // for example. + + // Loop E: 0,6,9,a,d,1,2,3 + // Loop F: 0,4,5,1,d,a,9,6 + // Loop G: 0,6,7,8,9,a,b,c,d,1,2,3 + // Loop H: 0,6,f,e,9,a,b,c,d,1,2,3 + // Loop I: 7,6,f,e,9,8 + let loopE = makeLoop("0:30, 0:34, 0:36, 0:39, 0:41, 0:44, 30:44, 30:30") + let loopF = makeLoop("0:30, -30:30, -30:44, 0:44, 0:41, 0:39, 0:36, 0:34") + let loopG = makeLoop("0:30, 0:34, 10:34, 10:36, 0:36, 0:39, 10:39, 10:41, 0:41, 0:44, 30:44, 30:30") + let loopH = makeLoop("0:30, 0:34, -10:34, -10:36, 0:36, 0:39, 10:39, 10:41, 0:41, 0:44, 30:44, 30:30") + + let loopI = makeLoop("10:34, 0:34, -10:34, -10:36, 0:36, 10:36") + + func p(_ x: Double, _ y: Double, _ z: Double) -> S2Point { + return S2Point(x: x, y: y, z: z) + } + + func p(_ lat: Double, _ lng: Double) -> S2Point { + return LatLng(latDegrees: lat, lngDegrees: lng).toPoint() + } + + func testEmptyfulloops() { + let emptyLoop = S2Loop.empty + XCTAssertTrue(emptyLoop.isEmpty()) + XCTAssertFalse(emptyLoop.isFull()) + XCTAssertTrue(emptyLoop.isEmptyOrFull()) + let fulloop = S2Loop.full + XCTAssertFalse(fulloop.isEmpty()) + XCTAssertTrue(fulloop.isFull()) + XCTAssertTrue(fulloop.isEmptyOrFull()) + } + + func r(_ x0: Double, _ x1: Double, _ y0: Double, _ y1: Double) -> S2Rect { + let lat = R1Interval(lo: x0 * toRadians, hi: y0 * toRadians) + let lng = S1Interval(lo: x1 * toRadians, hi: y1 * toRadians) + return S2Rect(lat: lat, lng: lng) + } + + func testLoopRectBound() { + let _ = S2Loop.loopFromPoints([S2Point(x: 0, y: 0, z: -1)]) + + XCTAssertTrue(S2Loop.empty.rectBound().isEmpty()) + XCTAssertTrue(S2Loop.full.rectBound().isFull()) + XCTAssertTrue(candyCane.rectBound().lng.isFull()) + XCTAssert(candyCane.rectBound().lat.lo < -0.349066) + XCTAssert(candyCane.rectBound().lat.hi > 0.174533) + XCTAssertTrue(smallNECW.rectBound().isFull()) + XCTAssert(rectsApproxEqual(a: arctic80.rectBound(), b: r(80, -180, 90, 180), tolLat: rectErrorLat, tolLng: rectErrorLng)) + XCTAssert(rectsApproxEqual(a: antarctic80.rectBound(), b: r(-90, -180, -80, 180), tolLat: rectErrorLat, tolLng: rectErrorLng)) + XCTAssertTrue(southHemi.rectBound().lng.isFull()) + XCTAssert(southHemi.rectBound().lat.approxEquals(R1Interval(lo: -.pi/2, hi: 0))) + + // Create a loop that contains the complement of the arctic80 loop. + let arctic80Inv = invert(l: arctic80) + // The highest latitude of each edge is attained at its midpoint. + let mid = arctic80Inv.vertices[0].v.add(arctic80Inv.vertices[1].v).mul(0.5) + XCTAssertEqual(arctic80Inv.rectBound().lat.hi, Double(LatLng(point: S2Point(raw: mid)).lat), accuracy: 10 * Cell.dblEpsilon) + } + + func testLoopCapBound() { + XCTAssertTrue(S2Loop.empty.capBound().isEmpty()) + XCTAssertTrue(S2Loop.full.capBound().isFull()) + XCTAssertTrue(smallNECW.capBound().isFull()) + XCTAssert(arctic80.capBound().approxEquals(r(80, -180, 90, 180).capBound())) + XCTAssert(antarctic80.capBound().approxEquals(r(-90, -180, -80, 180).capBound())) + } + + func invert(l: S2Loop) -> S2Loop { +// var vertices = make([]Point, 0, len(l.vertices)) +// for i := len(l.vertices) - 1; i >= 0; i-- { +// vertices.append(l.vertices[i]) +// } + return S2Loop.loopFromPoints(l.vertices.reversed()) + } + + func testOriginInside() { + XCTAssertTrue(northHemi.originInside) + XCTAssertTrue(northHemi3.originInside) + XCTAssertFalse(southHemi.originInside) + XCTAssertFalse(westHemi.originInside) + XCTAssertTrue(eastHemi.originInside) + XCTAssertTrue(nearHemi.originInside) + XCTAssertFalse(farHemi.originInside) + XCTAssertFalse(candyCane.originInside) + XCTAssertTrue(smallNECW.originInside) + XCTAssertFalse(arctic80.originInside) + XCTAssertFalse(antarctic80.originInside) + XCTAssertFalse(loopA.originInside) + } + + func testLoopContainsPoint() { + let north = S2Point(x: 0, y: 0, z: 1) + let south = S2Point(x: 0, y: 0, z: -1) + + XCTAssertFalse(S2Loop.empty.contains(north)) + XCTAssertTrue(S2Loop.full.contains(south)) + + let tests = [ + ("north hemisphere", northHemi, p(0, 0, 1), p(0, 0, -1)), + ("south hemisphere", southHemi, p(0, 0, -1), p(0, 0, 1)), + ("west hemisphere", westHemi, p(0, -1, 0), p(0, 1, 0)), + ("east hemisphere", eastHemi, p(0, 1, 0), p(0, -1, 0)), + ("candy cane", candyCane, p(5, 71), p(-8, 71))] + for (_, l, inn, out) in tests { + var l = l + for _ in 0..<4 { + XCTAssertTrue(l.contains(inn)) + XCTAssertFalse(l.contains(out)) + l = rotate(l: l) + } + } + } + + func testVertex() { + let tests = [ + (S2Loop.empty, 0, p(0, 0, 1)), + (S2Loop.empty, 1, p(0, 0, 1)), + (S2Loop.full, 0, p(0, 0, -1)), + (S2Loop.full, 1, p(0, 0, -1)), + (arctic80, 0, parsePoint("80:-150")), + (arctic80, 1, parsePoint("80:-30")), + (arctic80, 2, parsePoint("80:90")), + (arctic80, 3, parsePoint("80:-150"))] + for (loop, vertex, want) in tests { + XCTAssert(pointsApproxEquals(a: loop.vertex(vertex), b: want, epsilon: epsilon)) + } + // Check that wrapping is correct. + XCTAssert(pointsApproxEquals(a: arctic80.vertex(2), b: arctic80.vertex(5), epsilon: epsilon)) + let loopAroundThrice = 2 + 3 * arctic80.vertices.count + XCTAssert(pointsApproxEquals(a: arctic80.vertex(2), b: arctic80.vertex(loopAroundThrice), epsilon: epsilon)) + } + + func testNumEdges() { + let tests = [ + (S2Loop.empty, 0), + (S2Loop.full, 0), + (farHemi, 4), + (candyCane, 6), + (smallNECW, 3), + (arctic80, 3), + (antarctic80, 3), + (lineTriangle, 3), + (skinnyChevron, 4)] + for (loop, want) in tests { + XCTAssertEqual(loop.numEdges(), want) + } + } + + func testEdge() { + let tests = [ + (loop: farHemi, edge: 2, wantA: p(0, 0, -1), wantB: p(0, -1, 0)), + (loop: candyCane, edge: 0, wantA: parsePoint("-20:150"), wantB: parsePoint("-20:-70")), + (loop: candyCane, edge: 1, wantA: parsePoint("-20:-70"), wantB: parsePoint("0:70")), + (loop: candyCane, edge: 2, wantA: parsePoint("0:70"), wantB: parsePoint("10:-150")), + (loop: candyCane, edge: 3, wantA: parsePoint("10:-150"), wantB: parsePoint("10:70")), + (loop: candyCane, edge: 4, wantA: parsePoint("10:70"), wantB: parsePoint("-10:-70")), + (loop: candyCane, edge: 5, wantA: parsePoint("-10:-70"), wantB: parsePoint("-20:150")), + (loop: skinnyChevron, edge: 2, wantA: parsePoint("0:1e-320"), wantB: parsePoint("1e-320:80")), + (loop: skinnyChevron, edge: 3, wantA: parsePoint("1e-320:80"), wantB: parsePoint("0:0"))] + for (loop, edge, wantA, wantB) in tests { + let (a, b) = loop.edge(edge) + XCTAssert(pointsApproxEquals(a: a, b: wantA, epsilon: epsilon)) + XCTAssert(pointsApproxEquals(a: b, b: wantB, epsilon: epsilon)) + } + } + + func rotate(l: S2Loop) -> S2Loop { + var vertices = [S2Point]() + for i in 1.. S2Point { + return S2Point(x: x, y: y, z: z) + } + + func p(_ lat: Double, _ lng: Double) -> S2Point { + return LatLng(latDegrees: lat, lngDegrees: lng).toPoint() + } + + func testOriginPoint() { + XCTAssertEqual(S2Point.origin.v.norm(), 1.0, accuracy: 1e-16) + } + + func testPointCross() { + let tests = [ + (1.0, 0.0, 0.0, 1.0, 0.0, 0.0), + (1, 0, 0, 0, 1, 0), + (0, 1, 0, 1, 0, 0), + (1, 2, 3, -4, 5, -6)] + for (p1x, p1y, p1z, p2x, p2y, p2z) in tests { + let p1 = S2Point(x: p1x, y: p1y, z: p1z) + let p2 = S2Point(x: p2x, y: p2y, z: p2z) + let result = p1.pointCross(p2) + XCTAssertEqual(result.v.norm(), 1, accuracy: 1e-15) + XCTAssertEqual(result.v.dot(p1.v), 0, accuracy: 1e-15) + XCTAssertEqual(result.v.dot(p2.v), 0, accuracy: 1e-15) + } + } + + func testSign() { + let tests: [(Double, Double, Double, Double, Double, Double, Double, Double, Double, Bool)] = [ + (1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0, true), + (0, 1, 0, 0, 0, 1, 1, 0, 0, true), + (0, 0, 1, 1, 0, 0, 0, 1, 0, true), + (1, 1, 0, 0, 1, 1, 1, 0, 1, true), + (-3, -1, 4, 2, -1, -3, 1, -2, 0, true), + (-3, -1, 0, -2, 1, 0, 1, -2, 0, false), + (-6, 3, 3, -4, 2, -1, -2, 1, 4, false), + (0, -1, -1, 0, 1, -2, 0, 2, 1, false), + (-1, 2, 7, 2, 1, -4, 4, 2, -8, false), + (-4, -2, 7, 2, 1, -4, 4, 2, -8, false), + (0, -5, 7, 0, -4, 8, 0, -2, 4, false), + (-5, -2, 7, 0, 0, -2, 0, 0, -1, false), + (0, -2, 7, 0, 0, 1, 0, 0, 2, false)] + for (p1x, p1y, p1z, p2x, p2y, p2z, p3x, p3y, p3z, want) in tests { + let p1 = S2Point(x: p1x, y: p1y, z: p1z) + let p2 = S2Point(x: p2x, y: p2y, z: p2z) + let p3 = S2Point(x: p3x, y: p3y, z: p3z) + let result = S2Point.sign(p1, b: p2, c: p3) + XCTAssertEqual(result, want) + // For these cases we can test the reversibility condition + if want { + XCTAssertNotEqual(S2Point.sign(p3, b: p2, c: p1), want) + } + } + } + + // Points used in the various RobustSign tests. + let x = S2Point(x: 1, y: 0, z: 0) + let y = S2Point(x: 0, y: 1, z: 0) + let z = S2Point(x: 0, y: 0, z: 1) + + // The following points happen to be *exactly collinear* along a line that it + // approximate tangent to the surface of the unit sphere. In fact, C is the + // exact midpoint of the line segment AB. All of these points are close + // enough to unit length to satisfy r3.v.IsUnit(). + let poA = S2Point(x: 0.72571927877036835, y: 0.46058825605889098, z: 0.51106749730504852, normalize: false) + let poB = S2Point(x: 0.7257192746638208, y: 0.46058826573818168, z: 0.51106749441312738, normalize: false) + let poC = S2Point(x: 0.72571927671709457, y: 0.46058826089853633, z: 0.51106749585908795, normalize: false) + + // The points "x1" and "x2" are exactly proportional, i.e. they both lie + // on a common line through the origin. Both points are considered to be + // normalized, and in fact they both satisfy (x == x.Normalize()). + // Therefore the triangle (x1, x2, -x1) consists of three distinct points + // that all lie on a common line through the origin. + let x1 = S2Point(x: 0.99999999999999989, y: 1.4901161193847655e-08, z: 0) + let x2 = S2Point(x: 1, y: 1.4901161193847656e-08, z: 0) + + // Here are two more points that are distinct, exactly proportional, and + // that satisfy (x == x.Normalize()). + let x3 = S2Point(x: 1, y: 1, z: 1) + let x4 = S2Point(x: 1, y: 1, z: 1).v.mul(0.99999999999999989).s2 + + // The following three points demonstrate that Normalize() is not idempotent, i.e. + // y0.Normalize() != y0.Normalize().Normalize(). Both points are exactly proportional. + let y0 = S2Point(x: 1, y: 1, z: 0) + let y1 = S2Point(x: 1, y: 1, z: 0) + let y2 = S2Point(x: 1, y: 1, z: 0).v.normalize().s2 + + // TODO(roberts): This test is missing the actual RobustSign() parts of the checks from C++ + // test method RobustSign::CollinearPoints. + func testRobustSignEqualities() { + XCTAssertEqual(poC.v.sub(poA.v), poB.v.sub(poC.v)) + XCTAssertEqual(poC.v.sub(poA.v).s2, poB.v.sub(poC.v).s2) + XCTAssertEqual(x1, x1.v.normalize().s2) + XCTAssertEqual(x2, x2.v.normalize().s2) + XCTAssertEqual(x3, x3.v.normalize().s2) + XCTAssertEqual(x4, x4.v.normalize().s2) + XCTAssertNotEqual(x3, x4) + XCTAssertNotEqual(y1, y2) + XCTAssertEqual(y2, y2.v.normalize().s2) + } + + func testRobustSign() { + let tests = [ + // Simple collinear points test cases. + // a == b != c + (x, x, z, Direction.indeterminate), + // a != b == c + (x, y, y, .indeterminate), + // c == a != b + (z, x, z, .indeterminate), + // CCW + (x, y, z, .counterClockwise), + // CW + (z, y, x, .clockwise), + // Edge cases: + // The following points happen to be *exactly collinear* along a line that it + // approximate tangent to the surface of the unit sphere. In fact, C is the + // exact midpoint of the line segment AB. All of these points are close + // enough to unit length to satisfy S2::IsUnitLength(). + // Until we get ExactSign, this will only return Indeterminate. + + // It should be Clockwise. + (poA, poB, poC, .indeterminate), + // The points "x1" and "x2" are exactly proportional, i.e. they both lie + // on a common line through the origin. Both points are considered to be + // normalized, and in fact they both satisfy (x == x.Normalize()). + // Therefore the triangle (x1, x2, -x1) consists of three distinct points + // that all lie on a common line through the origin. + // Until we get ExactSign, this will only return Indeterminate. + // It should be CounterClockwise. + (x1, x2, x1.inverse(), .indeterminate), + // Here are two more points that are distinct, exactly proportional, and + // that satisfy (x == x.Normalize()). + // Until we get ExactSign, this will only return Indeterminate. + // It should be Clockwise. + (x3, x4, x3.inverse(), .indeterminate), + // The following points demonstrate that Normalize() is not idempotent, + // i.e. y0.Normalize() != y0.Normalize().Normalize(). Both points satisfy + // S2::IsNormalized(), though, and the two points are exactly proportional. + // Until we get ExactSign, this will only return Indeterminate. + // It should be CounterClockwise. + (y1, y2, y1.inverse(), .indeterminate)] + for (p1, p2, p3, want) in tests { + let result = S2Point.robustSign(p1, p2, p3) + XCTAssertEqual(result, want) + // Test RobustSign(b,c,a) == RobustSign(a,b,c) for all a,b,c + let rotated = S2Point.robustSign(p2, p3, p1) + XCTAssertEqual(rotated, result) + // Test RobustSign(c,b,a) == -RobustSign(a,b,c) for all a,b,c + var want = Direction.clockwise + if result == .clockwise { + want = .counterClockwise + } else if result == .indeterminate { + want = .indeterminate + } + let reversed = S2Point.robustSign(p3, p2, p1) + XCTAssertEqual(reversed, want) + } + } + + func testPointDistance() { + let tests = [ + (1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0), + (1, 0, 0, 0, 1, 0, .pi / 2), + (1, 0, 0, 0, 1, 1, .pi / 2), + (1, 0, 0, -1, 0, 0, .pi), + (1, 2, 3, 2, 3, -1, 1.2055891055045298)] + for (x1, y1, z1, x2, y2, z2, want) in tests { + let p1 = S2Point(x: x1, y: y1, z: z1) + let p2 = S2Point(x: x2, y: y2, z: z2) + XCTAssertEqual(p1.distance(p2), want, accuracy: 1e-15) + XCTAssertEqual(p2.distance(p1), want, accuracy: 1e-15) + } + } + + func testApproxEqual() { + let epsilon = 1e-14 + let tests: [(Double, Double, Double, Double, Double, Double, Bool)] = [ + (1.0, 0.0, 0.0, 1.0, 0.0, 0.0, true), + (1, 0, 0, 0, 1, 0, false), + (1, 0, 0, 0, 1, 1, false), + (1, 0, 0, -1, 0, 0, false), + (1, 2, 3, 2, 3, -1, false), + (1, 0, 0, 1 * (1 + epsilon), 0, 0, true), + (1, 0, 0, 1 * (1 - epsilon), 0, 0, true), + (1, 0, 0, 1 + epsilon, 0, 0, true), + (1, 0, 0, 1 - epsilon, 0, 0, true), + (1, 0, 0, 1, epsilon, 0, true), + (1, 0, 0, 1, epsilon, epsilon, false), + (1, epsilon, 0, 1, -epsilon, epsilon, false)] + for (x1, y1, z1, x2, y2, z2, want) in tests { + let p1 = S2Point(x: x1, y: y1, z: z1) + let p2 = S2Point(x: x2, y: y2, z: z2) + XCTAssertEqual(p1.approxEquals(p2), want) + } + } + + static func pp(_ x: Double, _ y: Double, _ z: Double) -> S2Point { + return S2Point(x: x, y: y, z: z) + } + + let pz = S2PointTests.pp(0, 0, 1) + let p000 = S2PointTests.pp(1, 0, 0) + let p045 = S2PointTests.pp(1, 1, 0) + let p090 = S2PointTests.pp(0, 1, 0) + let p180 = S2PointTests.pp(-1, 0, 0) + // Degenerate triangles. + let pr = S2PointTests.pp(0.257, -0.5723, 0.112) + let pq = S2PointTests.pp(-0.747, 0.401, 0.2235) + // For testing the Girard area fall through case. + let g1 = S2PointTests.pp(1, 1, 1) + let g2 = S2PointTests.pp(1, 1, 1).v.add(S2PointTests.pp(0.257, -0.5723, 0.112).v.mul(1e-15)).s2 + let g3 = S2PointTests.pp(1, 1, 1).v.add(S2PointTests.pp(-0.747, 0.401, 0.2235).v.mul(1e-15)).s2 + + func testPointArea() { + let epsilon = 1e-10 + let tests = [ + (p000, p090, pz, .pi / 2.0, 0), + // This test case should give 0 as the epsilon, but either Go or C++'s value for Pi, + // or the accuracy of the multiplications along the way, cause a difference ~15 decimal + // places into the result, so it is not quite a difference of 0. + (p045, pz, p180, 3.0 * .pi / 4.0, 1e-14), + // Make sure that Area has good *relative* accuracy even for very small areas. + (p(epsilon, 0, 1), p(0, epsilon, 1), pz, 0.5 * epsilon * epsilon, 1e-14), + // Make sure that it can handle degenerate triangles. + (pr, pr, pr, 0.0, 0), + (pr, pq, pr, 0.0, 1e-15), + (p000, p045, p090, 0.0, 0), + // Try a very long and skinny triangle. + (p000, p(1, 1, epsilon), p090, 5.8578643762690495119753e-11, 1e-9), + // TODO(roberts): + // C++ includes a 10,000 loop of perterbations to test out the Girard area + // computation is less than some noise threshold. + // Do we need that many? Will one or two suffice? + (g1, g2, g3, 0.0, 1e-15)] + for (a, b, c, want, nearness) in tests { + XCTAssertEqual(S2Point.pointArea(a, b, c), want, accuracy: nearness) + } + } + + func testPointAreaQuarterHemisphere() { + let tests = [ + // Triangles with near-180 degree edges that sum to a quarter-sphere. + (p(1, 0.1*epsilon, epsilon), p000, p045, p180, pz, Double.pi), + // Four other triangles that sum to a quarter-sphere. + (p(1, 1, epsilon), p000, p045, p180, pz, Double.pi)] + // TODO(roberts): + // C++ Includes a loop of 100 perturbations on a hemisphere for more tests. + for (a, b, c, d, e, want) in tests { + let area = + S2Point.pointArea(a, b, c) + + S2Point.pointArea(a, c, d) + + S2Point.pointArea(a, d, e) + + S2Point.pointArea(a, e, b) + XCTAssertEqual(area, want, accuracy: 1e-15) + } + } + + func testPlanarCentroid() { + let tests = [ + (name: "xyz axis", p0: p(0, 0, 1), p1: p(0, 1, 0), p2: p(1, 0, 0), want: p(1.0/3, 1.0/3, 1.0/3)), + (name: "Same point", p0: p(1, 0, 0), p1: p(1, 0, 0), p2: p(1, 0, 0), want: p(1, 0, 0))] + for (_, p0, p1, p2, want) in tests { + XCTAssertTrue(S2Point.planarCentroid(p0, p1, p2).approxEquals(want)) + } + } + + func testTrueCentroid() { + // Test TrueCentroid with very small triangles. This test assumes that + // the triangle is small enough so that it is nearly planar. + // The centroid of a planar triangle is at the intersection of its + // medians, which is two-thirds of the way along each median. + for _ in 0..<100 { + let f = randomFrame() + let p = f.col(0) + let x = f.col(1) + let y = f.col(2) + let d = 1e-4 * pow(1e-4, randomFloat64()) + // Make a triangle with two equal sides. + let p0 = p.v.sub(x.v.mul(d)).s2 + let p1 = p.v.add(x.v.mul(d)).s2 + let p2 = p.v.add(y.v.mul(d * 3)).s2 + let want = p.v.add(y.v.mul(d)).s2 + // + XCTAssert(S2Point.trueCentroid(p0, p1, p2).s2.distance(want) < 2e-8) + // Make a triangle with a right angle. + let p0_ = p + let p1_ = p.v.add(x.v.mul(d * 3)).s2 + let p2_ = p.v.add(y.v.mul(d * 6)).s2 + let want_ = p.v.add(x.v.add(y.v.mul(2)).mul(d)).s2 + XCTAssert(S2Point.trueCentroid(p0_, p1_, p2_).s2.distance(want_) < 2e-8) + } + } + + let N = 100 + + func benchmarkPointArea() { + for _ in 0.. S2Polygon { + let latLngs = coords.map { LatLng(latDegrees: $0, lngDegrees: $1) } + let points = latLngs.map { S2Point(latLng: $0) } + let loop = S2Loop(points: points) + return S2Polygon(loops: [loop]) + } + + func makeLoop(_ coords: [(Double, Double)]) -> S2Loop { + let latLngs = coords.map { LatLng(latDegrees: $0, lngDegrees: $1) } + let points = latLngs.map { S2Point(latLng: $0) } + return S2Loop(points: points) + } + + func testCreate() { + let coords = [(37.5, 122.5), (37.5, 122), (37, 122), (37, 122.5)] + let latLngs = coords.map { LatLng(latDegrees: $0, lngDegrees: $1) } + let points = latLngs.map { S2Point(latLng: $0) } + let loop = S2Loop(points: points) + let poly = S2Polygon(loops: [loop]) + XCTAssertNotNil(poly) + } + +} diff --git a/Sphere2GoLibTests/S2PolylineTests.swift b/Sphere2GoLibTests/S2PolylineTests.swift new file mode 100644 index 0000000..02b5589 --- /dev/null +++ b/Sphere2GoLibTests/S2PolylineTests.swift @@ -0,0 +1,20 @@ +// +// S2PolylineTests.swift +// Sphere2 +// + +import XCTest + +class S2PolylineTests: XCTestCase { + + override func setUp() { + super.setUp() + // Put setup code here. This method is called before the invocation of each test method in the class. + } + + override func tearDown() { + // Put teardown code here. This method is called after the invocation of each test method in the class. + super.tearDown() + } + +} diff --git a/Sphere2GoLibTests/S2RegionCovererTests.swift b/Sphere2GoLibTests/S2RegionCovererTests.swift new file mode 100644 index 0000000..52d6bfb --- /dev/null +++ b/Sphere2GoLibTests/S2RegionCovererTests.swift @@ -0,0 +1,154 @@ +// +// S2RegionCovererTests.swift +// Sphere2 +// + +import XCTest + + +class S2RegionCovererTests: XCTestCase { + + override func setUp() { + super.setUp() + // Put setup code here. This method is called before the invocation of each test method in the class. + CellId.setup() + } + + override func tearDown() { + // Put teardown code here. This method is called after the invocation of each test method in the class. + super.tearDown() + } + + func testRandomCells() { + let rc = RegionCoverer(minLevel: 0, maxLevel: 30, levelMod: 1, maxCells: 1) + // Test random cell ids at all levels. + for _ in 0..<10000 { + var id = CellId(id: randomUInt64()) + while !id.isValid() { + id = CellId(id: randomUInt64()) + } + let region = Cell(id: id) as S2Region + let covering = rc.covering(region: region) + XCTAssertEqual(covering.count, 1) + XCTAssertEqual(covering[0], id, "\(covering[0]) \(id)") + } + } + + func testRandomCaps() { + for _ in 0..<100 { + let l1 = Int(arc4random() % UInt32(CellId.maxLevel + 1)) + let l2 = Int(arc4random() % UInt32(CellId.maxLevel + 1)) + let rc = RegionCoverer(minLevel: min(l1, l2), maxLevel: max(l1, l2), levelMod: Int(arc4random() % 3 + 1), maxCells: Int(skewedInt(maxLog: 10))) + // + let maxArea = min(4.0 * Double.pi, Double(3 * rc.maxCells + 1) * Metric.avgArea.value(rc.minLevel)) + let r = randomCap(minArea: 0.1 * Metric.avgArea.value(rc.maxLevel), maxArea: maxArea) as S2Region + let covering = rc.covering(region: r) + checkCovering(rc: rc, r: r, covering: covering, interior: false) + let interior = rc.interiorCovering(region: r) + checkCovering(rc: rc, r: r, covering: interior, interior: true) + // Check that Covering is deterministic. + let covering2 = rc.covering(region: r) + XCTAssertEqual(covering, covering2) + // Also check Denormalize. The denormalized covering + // may still be different and smaller than "covering" because + // s2.RegionCoverer does not guarantee that it will not output all four + // children of the same parent. + covering.denormalize(minLevel: rc.minLevel, levelMod: rc.levelMod) + checkCovering(rc: rc, r: r, covering: covering, interior: false) + } + } + +// func testPolygons() { +// let coords = [(37.5, 122.5), (37.5, 122), (37, 122), (37, 122.5)] +// let latLngs = coords.map { LatLng(latDegrees: $0, lngDegrees: $1) } +// let points = latLngs.map { S2Point(latLng: $0) } +// let loop = S2Loop(points: points) +// let poly = S2Polygon(loops: [loop]) +// for _ in 0..<100 { +// let l1 = Int(arc4random() % UInt32(CellId.maxLevel + 1)) +// let l2 = Int(arc4random() % UInt32(CellId.maxLevel + 1)) +// let rc = RegionCoverer(minLevel: min(l1, l2), maxLevel: max(l1, l2), levelMod: Int(arc4random() % 3 + 1), maxCells: Int(skewedInt(maxLog: 10))) +// // +// let r = poly +// let covering = rc.covering(region: r) +// checkCovering(rc: rc, r: r, covering: covering, interior: false) +// let interior = rc.interiorCovering(region: r) +// checkCovering(rc: rc, r: r, covering: interior, interior: true) +// // Check that Covering is deterministic. +// let covering2 = rc.covering(region: r) +// XCTAssertEqual(covering, covering2) +// // Also check Denormalize. The denormalized covering +// // may still be different and smaller than "covering" because +// // s2.RegionCoverer does not guarantee that it will not output all four +// // children of the same parent. +// covering.denormalize(minLevel: rc.minLevel, levelMod: rc.levelMod) +// checkCovering(rc: rc, r: r, covering: covering, interior: false) +// } +// } + + // checkCovering reports whether covering is a valid cover for the region. + func checkCovering(rc: RegionCoverer, r: S2Region, covering: CellUnion, interior: Bool) { + // Keep track of how many cells have the same rc.MinLevel ancestor. + var minLevelCells = [CellId: Int]() + let tempCover = CellUnion(ids: []) + for i in 0..= rc.minLevel) + XCTAssert(level <= rc.maxLevel) + XCTAssertEqual((level - rc.minLevel) % rc.levelMod, 0) + tempCover.add(ci) + let i = ci.parent(rc.minLevel) + minLevelCells[i] = (minLevelCells[i] ?? 0) + 1 + } + if covering.count > rc.maxCells { + // If the covering has more than the requested number of cells, then check + // that the cell count cannot be reduced by using the parent of some cell. + for count in minLevelCells.values { + XCTAssert(count <= 1) + } + } + if interior { + for i in 0.. Int { + return edges + } + + func edge(_ i: Int) -> (S2Point, S2Point) { + return (a, b) + } + + func hasInterior() -> Bool { + return false + } + + func containsOrigin() -> Bool { + return false + } + +} + + +class S2ShapeIndexTests: XCTestCase { + + override func setUp() { + super.setUp() + // Put setup code here. This method is called before the invocation of each test method in the class. + } + + override func tearDown() { + // Put teardown code here. This method is called after the invocation of each test method in the class. + super.tearDown() + } + + func testShapeIndexBasics() { + let si = ShapeIndex() + XCTAssertEqual(si.count, 0) + let s = TestShape() + si.add(s) + // TODO: once an ID is available, use that rather than assuming the first one + // is always 0. +// XCTAssertEqual(si.at(0), s) + si.reset() + XCTAssertEqual(si.count, 0) + } + +} + diff --git a/Sphere2GoLibTests/S2Tests.swift b/Sphere2GoLibTests/S2Tests.swift new file mode 100644 index 0000000..17a878a --- /dev/null +++ b/Sphere2GoLibTests/S2Tests.swift @@ -0,0 +1,350 @@ +// +// S2Tests.swift +// Sphere2 +// + +import XCTest + +class S2Tests: XCTestCase { + + override func setUp() { + super.setUp() + // Put setup code here. This method is called before the invocation of each test method in the class. + } + + override func tearDown() { + // Put teardown code here. This method is called after the invocation of each test method in the class. + super.tearDown() + } + + func testDefault() { + + } + +} + +let epsilon = 1e-14 + +// float64Eq reports whether the two values are within the default epsilon. +func float64Eq(_ x: Double, _ y: Double) -> Bool { + return float64Near(x, y, epsilon: epsilon) +} + +// float64Near reports whether the two values are within the given epsilon. +func float64Near(_ x: Double, _ y: Double, epsilon: Double) -> Bool { + return fabs(x - y) <= epsilon +} + +// TODO(roberts): Add in flag to allow specifying the random seed for repeatable tests. + +// kmToAngle converts a distance on the Earth's surface to an angle. +func kmToAngle(km: Double) -> Double { + // The Earth's mean radius in kilometers (according to NASA). + let earthRadiusKm = 6371.01 + // ??? + return km / earthRadiusKm + +} + + +// randomBits returns a 64-bit random unsigned integer whose lowest "num" are random, and +// whose other bits are zero. +func randomBits(num: UInt32) -> UInt64 { + // Make sure the request is for not more than 63 bits. + let num = min(num, 63) + let r = randomUInt64() + return r & ((UInt64(1) << UInt64(num)) - 1) +} + +// Return a uniformly distributed 64-bit unsigned integer. +func randomUInt64() -> UInt64 { + return UInt64(arc4random()) | (UInt64(arc4random()) << 32) +} + +// Return a uniformly distributed 32-bit unsigned integer. +func randomUInt32() -> UInt32 { + return UInt32(randomBits(num: 32)) +} + +// randomFloat64 returns a uniformly distributed value in the range [0,1). +// Note that the values returned are all multiples of 2**-53, which means that +// not all possible values in this range are returned. +func randomFloat64() -> Double { + let randomFloatBits = UInt32(53) + return scalbn(Double(randomBits(num: randomFloatBits)), -Int(randomFloatBits)) +} + +// randomUniformInt returns a uniformly distributed integer in the range [0,n). +// NOTE: This is replicated here to stay in sync with how the C++ code generates +// uniform randoms. (instead of using Go's math/rand package directly). +func randomUniformInt(n: Int) -> Int { + return Int(randomFloat64() * Double(n)) +} + +// randomUniformFloat64 returns a uniformly distributed value in the range [min, max). +func randomUniformFloat64(min: Double, max: Double) -> Double { + return min + randomFloat64() * (max - min) +} + +// oneIn returns true with a probability of 1/n. +func oneIn(n: Int) -> Bool { + return randomUniformInt(n: n) == 0 +} + +// randomPoint returns a random unit-length vector. +func randomPoint() -> S2Point { + let x = randomUniformFloat64(min: -1, max: 1) + let y = randomUniformFloat64(min: -1, max: 1) + let z = randomUniformFloat64(min: -1, max: 1) + return S2Point(x: x, y: y, z: z) +} + +// randomFrame returns a right-handed coordinate frame (three orthonormal vectors) for +// a randomly generated point. +func randomFrame() -> Matrix { + return randomFrameAtPoint(z: randomPoint()) +} + +// randomFrameAtPoint returns a right-handed coordinate frame using the given +// point as the z-axis. The x- and y-axes are computed such that (x,y,z) is a +// right-handed coordinate frame (three orthonormal vectors). +func randomFrameAtPoint(z: S2Point) -> Matrix { + let x = z.v.cross(randomPoint().v).s2 + let y = z.v.cross(x.v).s2 + // + let m = Matrix() + m.setCol(0, point: x) + m.setCol(1, point: y) + m.setCol(2, point: z) + return m +} + +// randomCellIDForLevel returns a random CellID at the given level. +// The distribution is uniform over the space of cell ids, but only +// approximately uniform over the surface of the sphere. +func randomCellIdForLevel(level: Int) -> CellId { + let face = randomUniformInt(n: CellId.numFaces) + let pos = randomUInt64() & UInt64((1 << CellId.posBits)-1) + return CellId(face: face, pos: pos, level: level) +} + +// randomCellID returns a random CellID at a randomly chosen +// level. The distribution is uniform over the space of cell ids, +// but only approximately uniform over the surface of the sphere. +func randomCellId() -> CellId { + return randomCellIdForLevel(level: randomUniformInt(n: CellId.maxLevel + 1)) +} + +// parsePoint returns an Point from the latitude-longitude coordinate in degrees +// in the given string, or the origin if the string was invalid. +// e.g., "-20:150" +func parsePoint(_ s: String) -> S2Point { + let p = parsePoints(s) + if p.count > 0 { + return p[0] + } + return S2Point(x: 0, y: 0, z: 0) +} + +// parseRect returns the minimal bounding Rect that contains the one or more +// latitude-longitude coordinates in degrees in the given string. +// Examples of input: +// "-20:150" // one point +// "-20:150, -20:151, -19:150" // three points +func parseRect(_ s: String) -> S2Rect { + var rect = S2Rect.empty + let lls = parseLatLngs(s) +// if lls.count > 0 { +// rect = S2Rect((latLng: lls[0]) +// } +// + for ll in lls { + rect = rect.add(ll) + } + return rect +} + +// parseLatLngs splits up a string of lat:lng points and returns the list of parsed +// entries. +func parseLatLngs(_ s: String) -> [LatLng] { + let pieces = s.components(separatedBy: ",") + var lls = [LatLng]() + for piece in pieces { + // get a trimmed non-empty string + let piece = piece.trimmingCharacters(in: NSCharacterSet.whitespaces) + if piece == "" { + continue + } + let p = piece.components(separatedBy: ":") + if p.count != 2 { + fatalError("invalid input string for parseLatLngs") + } + guard let lat = Double(p[0]) else { + fatalError("invalid float in parseLatLngs") + } + guard let lng = Double(p[1]) else { + fatalError("invalid float in parseLatLngs") + } + + lls.append(LatLng(latDegrees: lat, lngDegrees: lng)) + } + return lls +} + +// parsePoints takes a string of lat:lng points and returns the set of Points it defines. +func parsePoints(_ s: String) -> [S2Point] { + let lls = parseLatLngs(s) + var points = [S2Point]() + for ll in lls { + points.append(ll.toPoint()) + } + return points +} + +// skewedInt returns a number in the range [0,2^max_log-1] with bias towards smaller numbers. +func skewedInt(maxLog: Int) -> Int { + let base = Int32(arc4random() & 0x7fffffff) % (Int32(maxLog + 1)) + return Int(randomBits(num: 31)) & Int((1 << base) - 1) +} + +// randomCap returns a cap with a random axis such that the log of its area is +// uniformly distributed between the logs of the two given values. The log of +// the cap angle is also approximately uniformly distributed. +func randomCap(minArea: Double, maxArea: Double) -> S2Cap { + let capArea = maxArea * pow(minArea/maxArea, randomFloat64()) + return S2Cap(center: randomPoint(), area: capArea) +} + +// pointsApproxEquals reports whether the two points are within the given distance +// of each other. This is the same as Point.ApproxEquals but permits specifying +// the epsilon. +func pointsApproxEquals(a: S2Point, b:S2Point, epsilon: Double) -> Bool { + return Double(a.angle(b)) <= epsilon +} + +let rectErrorLat = 10 * Cell.dblEpsilon +let rectErrorLng = Cell.dblEpsilon + +// r2PointsApproxEqual reports whether the two points are within the given epsilon. +func r2PointsApproxEquals(a: S2Point, b: S2Point, epsilon: Double) -> Bool { + return float64Near(a.x, b.x, epsilon: epsilon) && float64Near(a.y, b.y, epsilon: epsilon) +} + +// rectsApproxEqual reports whether the two rect are within the given tolerances +// at each corner from each other. The tolerances are specific to each axis. +func rectsApproxEqual(a: S2Rect, b: S2Rect, tolLat: Double, tolLng: Double) -> Bool { + return fabs(a.lat.lo-b.lat.lo) < tolLat && + fabs(a.lat.hi-b.lat.hi) < tolLat && + fabs(a.lng.lo-b.lng.lo) < tolLng && + fabs(a.lng.hi-b.lng.hi) < tolLng +} + +// matricesApproxEqual reports whether all cells in both matrices are equal within +// the default floating point epsilon. +func matricesApproxEqual(m1: Matrix, m2: Matrix) -> Bool { + return float64Eq(m1[0, 0], m2[0, 0]) && + float64Eq(m1[0, 1], m2[0, 1]) && + float64Eq(m1[0, 2], m2[0, 2]) && + + float64Eq(m1[1, 0], m2[1, 0]) && + float64Eq(m1[1, 1], m2[1, 1]) && + float64Eq(m1[1, 2], m2[1, 2]) && + + float64Eq(m1[2, 0], m2[2, 0]) && + float64Eq(m1[2, 1], m2[2, 1]) && + float64Eq(m1[2, 2], m2[2, 2]) +} + +// samplePointFromRect returns a point chosen uniformly at random (with respect +// to area on the sphere) from the given rectangle. +func samplePointFromRect(rect: S2Rect) -> S2Point { + // First choose a latitude uniformly with respect to area on the sphere. + let sinLo = sin(rect.lat.lo) + let sinHi = sin(rect.lat.hi) + let lat = asin(randomUniformFloat64(min: sinLo, max: sinHi)) + + // Now choose longitude uniformly within the given range. + let lng = rect.lng.lo + randomFloat64()*rect.lng.length() + + return LatLng(lat: lat, lng: lng).toPoint() +} + +// samplePointFromCap returns a point chosen uniformly at random (with respect +// to area) from the given cap. +func samplePointFromCap(c: S2Cap) -> S2Point { + // We consider the cap axis to be the "z" axis. We choose two other axes to + // complete the coordinate frame. + let m = Matrix.getFrame(c.center) + + // The surface area of a spherical cap is directly proportional to its + // height. First we choose a random height, and then we choose a random + // point along the circle at that height. + let h = randomFloat64() * c.height + let theta = 2 * .pi * randomFloat64() + let r = sqrt(h * (2 - h)) + + // The result should already be very close to unit-length, but we might as + // well make it accurate as possible. + let p = S2Point(x: cos(theta) * r, y: sin(theta) * r, z: 1 - h) + return Matrix.fromFrame(m, point: p) +} + +// perturbATowardsB returns a point that has been shifted some distance towards the +// second point based on a random number. +func perturbATowardsB(a: S2Point, b: S2Point) -> S2Point { + let choice = randomFloat64() + if choice < 0.1 { + return a + } + if choice < 0.3 { + // Return a point that is exactly proportional to A and that still + // satisfies IsUnitLength(). + while true { + let b3 = (randomFloat64() - 0.5) * Cell.dblEpsilon + let b2 = 2 - a.v.norm() + 5 * b3 + let b = a.v.mul(b2) + if !b.approxEquals(a.v) && b.isUnit() { + return b.s2 + } + } + } + if choice < 0.5 { + // Return a point such that the distance squared to A will underflow. + return interpolateAtDistance(1e-300, a: a, b: b) + } + // Otherwise return a point whose distance from A is near dblEpsilon such + // that the log of the pdf is uniformly distributed. + let distance = Cell.dblEpsilon * 1e-5 * pow(1e6, randomFloat64()) + return interpolateAtDistance(distance, a: a, b: b) +} + +// perturbedCornerOrMidpoint returns a Point from a line segment whose endpoints are +// difficult to handle correctly. Given two adjacent cube vertices P and Q, +// it returns either an edge midpoint, face midpoint, or corner vertex that is +// in the plane of PQ and that has been perturbed slightly. It also sometimes +// returns a random point from anywhere on the sphere. +func perturbedCornerOrMidpoint(p: S2Point, q: S2Point) -> S2Point { + var a = p.v.mul(Double(randomUniformInt(n: 3) - 1)).add(q.v.mul(Double(randomUniformInt(n: 3) - 1))) + if oneIn(n: 10) { + // This perturbation often has no effect except on coordinates that are + // zero, in which case the perturbed value is so small that operations on + // it often result in underflow. + a = a.add(randomPoint().v.mul(pow(1e-300, randomFloat64()))) + } else if oneIn(n: 2) { + // For coordinates near 1 (say > 0.5), this perturbation yields values + // that are only a few representable values away from the initial value. + a = a.add(randomPoint().v.mul(4 * Cell.dblEpsilon)) + } else { + // A perturbation whose magnitude is in the range [1e-25, 1e-10]. + a = a.add(randomPoint().v.mul(1e-10 * pow(1e-15, randomFloat64()))) + } + + if a.norm2() < Cell.dblEpsilon { + // If a.Norm2() is denormalized, Normalize() loses too much precision. + return perturbedCornerOrMidpoint(p: p, q: q) + } + return a.s2 +} + +// TODO: +// Most of the other s2 testing methods.