From 50753a3a36064c945ded050a24b0ace1e3ccfca1 Mon Sep 17 00:00:00 2001 From: axelhuesemann Date: Fri, 17 Mar 2017 17:09:13 -0700 Subject: [PATCH 1/3] Create README.md --- README.md | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 README.md 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 + From fbb2f6c6575dd6d46f7c35c80ced58fcaa901779 Mon Sep 17 00:00:00 2001 From: Axel Huesemann Date: Fri, 17 Mar 2017 17:11:47 -0700 Subject: [PATCH 2/3] add initial implementation --- Sphere2Go.xcodeproj/project.pbxproj | 630 ++++++++++++++ .../contents.xcworkspacedata | 7 + .../UserInterfaceState.xcuserstate | Bin 0 -> 171250 bytes .../xcdebugger/Breakpoints_v2.xcbkptlist | 117 +++ .../xcschemes/Sphere2.xcscheme | 80 ++ .../xcschemes/Sphere2Lib.xcscheme | 99 +++ .../xcschemes/Sphere2LibTests.xcscheme | 56 ++ .../xcschemes/xcschememanagement.plist | 42 + Sphere2Go/PriorityQueue.swift | 107 +++ Sphere2Go/R1Interval.swift | 182 ++++ Sphere2Go/R2Rect.swift | 234 ++++++ Sphere2Go/R3Vector.swift | 135 +++ Sphere2Go/S1Angle.swift | 80 ++ Sphere2Go/S1Interval.swift | 326 ++++++++ Sphere2Go/S2Cap.swift | 398 +++++++++ Sphere2Go/S2Cell.swift | 317 +++++++ Sphere2Go/S2CellId.swift | 734 +++++++++++++++++ Sphere2Go/S2CellUnion.swift | 229 ++++++ Sphere2Go/S2Cube.swift | 242 ++++++ Sphere2Go/S2EdgeUtility.swift | 777 ++++++++++++++++++ Sphere2Go/S2LatLng.swift | 98 +++ Sphere2Go/S2Loop.swift | 378 +++++++++ Sphere2Go/S2Matrix.swift | 159 ++++ Sphere2Go/S2Metric.swift | 75 ++ Sphere2Go/S2Point.swift | 612 ++++++++++++++ Sphere2Go/S2Polygon.swift | 117 +++ Sphere2Go/S2Polyline.swift | 163 ++++ Sphere2Go/S2Rect.swift | 406 +++++++++ Sphere2Go/S2Region.swift | 39 + Sphere2Go/S2RegionCoverer.swift | 469 +++++++++++ Sphere2Go/S2Shape.swift | 110 +++ Sphere2GoLib/Info.plist | 26 + Sphere2GoLib/Sphere2Lib.h | 17 + Sphere2GoLibTests/Info.plist | 24 + Sphere2GoLibTests/R1IntervalTests.swift | 125 +++ Sphere2GoLibTests/R2RectTests.swift | 154 ++++ Sphere2GoLibTests/R3VectorTests.swift | 194 +++++ Sphere2GoLibTests/S1IntervalTests.swift | 373 +++++++++ Sphere2GoLibTests/S2CapTests.swift | 342 ++++++++ Sphere2GoLibTests/S2CellIdTests.swift | 331 ++++++++ Sphere2GoLibTests/S2CellTests.swift | 202 +++++ Sphere2GoLibTests/S2CellUnionTests.swift | 396 +++++++++ Sphere2GoLibTests/S2LatLngTests.swift | 97 +++ Sphere2GoLibTests/S2LoopTests.swift | 295 +++++++ Sphere2GoLibTests/S2MetricTests.swift | 26 + Sphere2GoLibTests/S2PointTests.swift | 443 ++++++++++ Sphere2GoLibTests/S2PolygonTests.swift | 42 + Sphere2GoLibTests/S2PolylineTests.swift | 20 + Sphere2GoLibTests/S2RegionCovererTests.swift | 154 ++++ Sphere2GoLibTests/S2ShapeIndexTests.swift | 60 ++ Sphere2GoLibTests/S2Tests.swift | 350 ++++++++ 51 files changed, 11089 insertions(+) create mode 100644 Sphere2Go.xcodeproj/project.pbxproj create mode 100644 Sphere2Go.xcodeproj/project.xcworkspace/contents.xcworkspacedata create mode 100644 Sphere2Go.xcodeproj/project.xcworkspace/xcuserdata/axel.xcuserdatad/UserInterfaceState.xcuserstate create mode 100644 Sphere2Go.xcodeproj/xcuserdata/axel.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist create mode 100644 Sphere2Go.xcodeproj/xcuserdata/axel.xcuserdatad/xcschemes/Sphere2.xcscheme create mode 100644 Sphere2Go.xcodeproj/xcuserdata/axel.xcuserdatad/xcschemes/Sphere2Lib.xcscheme create mode 100644 Sphere2Go.xcodeproj/xcuserdata/axel.xcuserdatad/xcschemes/Sphere2LibTests.xcscheme create mode 100644 Sphere2Go.xcodeproj/xcuserdata/axel.xcuserdatad/xcschemes/xcschememanagement.plist create mode 100644 Sphere2Go/PriorityQueue.swift create mode 100644 Sphere2Go/R1Interval.swift create mode 100644 Sphere2Go/R2Rect.swift create mode 100644 Sphere2Go/R3Vector.swift create mode 100644 Sphere2Go/S1Angle.swift create mode 100644 Sphere2Go/S1Interval.swift create mode 100644 Sphere2Go/S2Cap.swift create mode 100644 Sphere2Go/S2Cell.swift create mode 100644 Sphere2Go/S2CellId.swift create mode 100644 Sphere2Go/S2CellUnion.swift create mode 100644 Sphere2Go/S2Cube.swift create mode 100644 Sphere2Go/S2EdgeUtility.swift create mode 100644 Sphere2Go/S2LatLng.swift create mode 100644 Sphere2Go/S2Loop.swift create mode 100644 Sphere2Go/S2Matrix.swift create mode 100644 Sphere2Go/S2Metric.swift create mode 100644 Sphere2Go/S2Point.swift create mode 100644 Sphere2Go/S2Polygon.swift create mode 100644 Sphere2Go/S2Polyline.swift create mode 100644 Sphere2Go/S2Rect.swift create mode 100644 Sphere2Go/S2Region.swift create mode 100644 Sphere2Go/S2RegionCoverer.swift create mode 100644 Sphere2Go/S2Shape.swift create mode 100644 Sphere2GoLib/Info.plist create mode 100644 Sphere2GoLib/Sphere2Lib.h create mode 100644 Sphere2GoLibTests/Info.plist create mode 100644 Sphere2GoLibTests/R1IntervalTests.swift create mode 100644 Sphere2GoLibTests/R2RectTests.swift create mode 100644 Sphere2GoLibTests/R3VectorTests.swift create mode 100644 Sphere2GoLibTests/S1IntervalTests.swift create mode 100644 Sphere2GoLibTests/S2CapTests.swift create mode 100644 Sphere2GoLibTests/S2CellIdTests.swift create mode 100644 Sphere2GoLibTests/S2CellTests.swift create mode 100644 Sphere2GoLibTests/S2CellUnionTests.swift create mode 100644 Sphere2GoLibTests/S2LatLngTests.swift create mode 100644 Sphere2GoLibTests/S2LoopTests.swift create mode 100644 Sphere2GoLibTests/S2MetricTests.swift create mode 100644 Sphere2GoLibTests/S2PointTests.swift create mode 100644 Sphere2GoLibTests/S2PolygonTests.swift create mode 100644 Sphere2GoLibTests/S2PolylineTests.swift create mode 100644 Sphere2GoLibTests/S2RegionCovererTests.swift create mode 100644 Sphere2GoLibTests/S2ShapeIndexTests.swift create mode 100644 Sphere2GoLibTests/S2Tests.swift diff --git a/Sphere2Go.xcodeproj/project.pbxproj b/Sphere2Go.xcodeproj/project.pbxproj new file mode 100644 index 0000000..a2bd3b1 --- /dev/null +++ b/Sphere2Go.xcodeproj/project.pbxproj @@ -0,0 +1,630 @@ +// !$*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 = 0820; + ORGANIZATIONNAME = "Axel Huesemann"; + TargetAttributes = { + 7700AE6A1CCFD15200606F25 = { + CreatedOnToolsVersion = 7.3; + LastSwiftMigration = 0800; + }; + 7770ED361CCAC21100C543EC = { + CreatedOnToolsVersion = 7.3; + LastSwiftMigration = 0800; + }; + }; + }; + buildConfigurationList = 7770ED191CCAB33000C543EC /* Build configuration list for PBXProject "Sphere2Go" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = English; + hasScannedForEncodings = 0; + knownRegions = ( + en, + ); + 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 = { + 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 = 3.0; + }; + name = Debug; + }; + 7700AE741CCFD15200606F25 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + 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 = 3.0; + }; + name = Release; + }; + 7770ED251CCAB33000C543EC /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + 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_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = 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_OBJC_ROOT_CLASS = YES_ERROR; + 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 = 9.3; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + }; + name = Debug; + }; + 7770ED261CCAB33000C543EC /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + 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_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = 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_OBJC_ROOT_CLASS = YES_ERROR; + 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 = 9.3; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 7770ED3D1CCAC21100C543EC /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + 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 = 3.0; + TARGETED_DEVICE_FAMILY = "1,2"; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Debug; + }; + 7770ED3E1CCAC21100C543EC /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + 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 = 3.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 0000000000000000000000000000000000000000..6dbc870bd1a41aa6f14d40b533d33d3a3c0c9b83 GIT binary patch literal 171250 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 T_n-N%{=fhH|NlF@TmJt5K_JA9 literal 0 HcmV?d00001 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..3ce2f9c --- /dev/null +++ b/Sphere2Go.xcodeproj/xcuserdata/axel.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist @@ -0,0 +1,117 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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..e76a3af --- /dev/null +++ b/Sphere2Go.xcodeproj/xcuserdata/axel.xcuserdatad/xcschemes/Sphere2.xcscheme @@ -0,0 +1,80 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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..6db0f4e --- /dev/null +++ b/Sphere2Go.xcodeproj/xcuserdata/axel.xcuserdatad/xcschemes/Sphere2Lib.xcscheme @@ -0,0 +1,99 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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..1ada2ce --- /dev/null +++ b/Sphere2Go.xcodeproj/xcuserdata/axel.xcuserdatad/xcschemes/Sphere2LibTests.xcscheme @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + + + + + + 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..4cd7d2b --- /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() } + swap(&heap[0], &heap[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 } + swap(&heap[index], &heap[j]) + index = j + } + } + + private mutating func swim(_ index: Int) { + var index = index + while index > 0 && ordered(heap[(index - 1) / 2], heap[index]) { + swap(&heap[(index - 1) / 2], &heap[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..a11134b --- /dev/null +++ b/Sphere2Go/R1Interval.swift @@ -0,0 +1,182 @@ +// +// 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 + + 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 + } + + // Equal returns true iff the interval contains the same points as other. + func equals(_ interval: R1Interval) -> Bool { + return lo == interval.lo && hi == interval.hi || isEmpty() && interval.isEmpty() + } + + // 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)) + } + +} + +func ==(lhs: R1Interval, rhs: R1Interval) -> Bool { + return lhs.lo == rhs.lo && lhs.hi == rhs.hi || (lhs.isEmpty() && rhs.isEmpty()) +} diff --git a/Sphere2Go/R2Rect.swift b/Sphere2Go/R2Rect.swift new file mode 100644 index 0000000..1e0729f --- /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 + + var description: String { + return String(format: "[%f, %f]", x, y) + } + +} + +func ==(lhs: R2Point, rhs: R2Point) -> Bool { + return lhs.x == rhs.x && lhs.y == rhs.y +} + +// R2Rect represents a closed axis-aligned rectangle in the (x,y) plane. + +func ==(lhs: R2Rect, rhs: R2Rect) -> Bool { + return lhs.x == rhs.x && lhs.y == rhs.y +} + +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 + + 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..7dc5401 --- /dev/null +++ b/Sphere2Go/R3Vector.swift @@ -0,0 +1,135 @@ +// +// R3Vector.swift +// Sphere2 +// + +import Foundation + +// package r3 +// import fmt, math, s1 + +func ==(lhs: R3Vector, rhs: R3Vector) -> Bool { + return lhs.x == rhs.x && lhs.y == rhs.y && lhs.z == rhs.z +} + + +// 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 + + var description: String { + return "(\(x), \(y), \(z))" + } + var hashValue: Int { + return x.hashValue ^ y.hashValue ^ z.hashValue + } + + // 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..bbd879b --- /dev/null +++ b/Sphere2Go/S2CellId.swift @@ -0,0 +1,734 @@ +// +// 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)) +} + +func ==(lhs:CellId, rhs: CellId) -> Bool { + return lhs.id == rhs.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 { + + // TODO(dsymonds): Some of these constants should probably be exported. + 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 + + // 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)" + } + + var hashValue: Int { + return id.hashValue + } + + // 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) +// } + + // TODO: the methods below are not exported yet. Settle on the entire API design + // before doing this. Do we want to mirror the C++ one as closely as possible? + + // 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..68eb349 --- /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 + + 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 + } + +} + +func ==(lhs: CellUnion, rhs: CellUnion) -> Bool { + return lhs.cellIds == rhs.cellIds +} 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..b355653 --- /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]) { + var 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 { + var 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..6fc9d99 --- /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 ldexp(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..549d1b1 --- /dev/null +++ b/Sphere2Go/S2Point.swift @@ -0,0 +1,612 @@ +// +// 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. + +func ==(lhs: S2Point, rhs: S2Point) -> Bool { + return lhs.x == rhs.x && lhs.y == rhs.y && lhs.z == rhs.z +} + + +// 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))" + } + + var hashValue: Int { + return x.hashValue ^ y.hashValue ^ z.hashValue + } + + // 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..4270702 --- /dev/null +++ b/Sphere2Go/S2Polyline.swift @@ -0,0 +1,163 @@ +// +// 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 { + + 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) + } + + // 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.. Bool { + if points.count != b.points.count { + return false + } + for i in 0.. 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..f6bb71d --- /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.equals(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..05e28c2 --- /dev/null +++ b/Sphere2Go/S2RegionCoverer.swift @@ -0,0 +1,469 @@ +// +// 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: methods + + func addChild(_ child: Candidate) { + children.append(child) + numChildren += 1 + } + +} + +func ==(lhs: Candidate, rhs: Candidate) -> Bool { + return lhs.priority == rhs.priority +} + +func <(lhs: Candidate, rhs: Candidate) -> Bool { + return lhs.priority < rhs.priority +} + + +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..774fc45 --- /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() { + XCTAssertEqualWithAccuracy(v(0, 0, 0).norm(), 0.0, accuracy: 1e-14) + XCTAssertEqualWithAccuracy(v(0, 1, 0).norm(), 1.0, accuracy: 1e-14) + XCTAssertEqualWithAccuracy(v(3, -4, 12).norm(), 13.0, accuracy: 1e-14) + XCTAssertEqualWithAccuracy(v(1, 1e-16, 1e-32).norm(), 1.0, accuracy: 1e-14) + } + + func testNorm2() { + XCTAssertEqualWithAccuracy(v(0, 0, 0).norm2(), 0.0, accuracy: 1e-14) + XCTAssertEqualWithAccuracy(v(0, 1, 0).norm2(), 1.0, accuracy: 1e-14) + XCTAssertEqualWithAccuracy(v(1, 1, 1).norm2(), 3.0, accuracy: 1e-14) + XCTAssertEqualWithAccuracy(v(1, 2, 3).norm2(), 14.0, accuracy: 1e-14) + XCTAssertEqualWithAccuracy(v(3, -4, 12).norm2(), 169.0, accuracy: 1e-14) + XCTAssertEqualWithAccuracy(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() + XCTAssertEqualWithAccuracy(v.x*nv.y, v.y*nv.x, accuracy: 1e-14) + XCTAssertEqualWithAccuracy(v.x*nv.z, v.z*nv.x, accuracy: 1e-14) + XCTAssertEqualWithAccuracy(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) + XCTAssertEqualWithAccuracy(v1.dot(v2), want, accuracy: 1e-15) + XCTAssertEqualWithAccuracy(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) + XCTAssertEqualWithAccuracy(v1.distance(v2), want, accuracy: 1e-13) + XCTAssertEqualWithAccuracy(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 { + XCTAssertEqualWithAccuracy(v1.angle(v2), want, accuracy: 1e-15) + XCTAssertEqualWithAccuracy(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 { + XCTAssertEqualWithAccuracy(v.dot(v.ortho()), 0, accuracy: 1e-15) + XCTAssertEqualWithAccuracy(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 + XCTAssertEqualWithAccuracy(a1, a2, accuracy: 1e-15) + // Dot commutes + XCTAssertEqualWithAccuracy(d1, d2, accuracy: 1e-15) + // Cross anti-commutes + XCTAssert(c1.approxEquals(c2.mul(-1.0))) + // Cross is orthogonal to original vectors + XCTAssertEqualWithAccuracy(v1.dot(c1), 0.0, accuracy: 1e-15) + XCTAssertEqualWithAccuracy(v2.dot(c1), 0.0, accuracy: 1e-15) + } + } + +} diff --git a/Sphere2GoLibTests/S1IntervalTests.swift b/Sphere2GoLibTests/S1IntervalTests.swift new file mode 100644 index 0000000..e77d880 --- /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() { + XCTAssertEqualWithAccuracy(quad12.center(), .pi / 2, accuracy: 1e-15) + XCTAssertEqualWithAccuracy(S1Interval(lo_endpoint: 3.1, hi_endpoint: 2.9).center(), 3 - .pi, accuracy: 1e-15) + XCTAssertEqualWithAccuracy(S1Interval(lo_endpoint: -2.9, hi_endpoint: -3.1).center(), .pi - 3, accuracy: 1e-15) + XCTAssertEqualWithAccuracy(S1Interval(lo_endpoint: 2.1, hi_endpoint: -2.1).center(), .pi, accuracy: 1e-15) + XCTAssertEqualWithAccuracy(pi.center(), .pi, accuracy: 1e-15) + XCTAssertEqualWithAccuracy(mipi.center(), .pi, accuracy: 1e-15) + XCTAssertEqualWithAccuracy(quad23.center(), .pi, accuracy: 1e-15) + XCTAssertEqualWithAccuracy(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..c8c3117 --- /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 { + XCTAssertEqualWithAccuracy(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() + XCTAssertEqualWithAccuracy(r.lat.lo, latLoDeg * toRadians, accuracy: epsilon) + XCTAssertEqualWithAccuracy(r.lat.hi, latHiDeg * toRadians, accuracy: epsilon) + XCTAssertEqualWithAccuracy(r.lng.lo, lngLoDeg * toRadians, accuracy: epsilon) + XCTAssertEqualWithAccuracy(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..28f0768 --- /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) + XCTAssertEqualWithAccuracy(uv.x.lo, want.x.lo, accuracy: 1e-14) + XCTAssertEqualWithAccuracy(uv.x.hi, want.x.hi, accuracy: 1e-14) + XCTAssertEqualWithAccuracy(uv.y.lo, want.y.lo, accuracy: 1e-14) + XCTAssertEqualWithAccuracy(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..766db64 --- /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 + XCTAssertEqualWithAccuracy(vertices[k].dot(edges[k]), 0.0, accuracy: 1e-14) + XCTAssertEqualWithAccuracy(vertices[(k + 1) & 3].dot(edges[k]), 0.0, accuracy: 1e-14) + XCTAssertEqualWithAccuracy(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 + XCTAssertEqualWithAccuracy(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() + } + XCTAssertEqualWithAccuracy(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..f0c40cd --- /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. + XCTAssertEqualWithAccuracy(p.x, x, accuracy: eps) + XCTAssertEqualWithAccuracy(p.y, y, accuracy: eps) + XCTAssertEqualWithAccuracy(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) + XCTAssertEqualWithAccuracy(ll2.lat, lat * toRadians, accuracy: eps) + if !isPolar { + XCTAssertEqualWithAccuracy(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) + XCTAssertEqualWithAccuracy(d, want * toRadians, accuracy: tolerance) + } + } + +} diff --git a/Sphere2GoLibTests/S2LoopTests.swift b/Sphere2GoLibTests/S2LoopTests.swift new file mode 100644 index 0000000..7fe5172 --- /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) + XCTAssertEqualWithAccuracy(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() { + XCTAssertEqualWithAccuracy(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) + XCTAssertEqualWithAccuracy(result.v.norm(), 1, accuracy: 1e-15) + XCTAssertEqualWithAccuracy(result.v.dot(p1.v), 0, accuracy: 1e-15) + XCTAssertEqualWithAccuracy(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) + XCTAssertEqualWithAccuracy(p1.distance(p2), want, accuracy: 1e-15) + XCTAssertEqualWithAccuracy(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 { + XCTAssertEqualWithAccuracy(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) + XCTAssertEqualWithAccuracy(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..a4acbde --- /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 ldexp(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. From 0edc7caa338c3d66178afda601e794b0a8aa5be3 Mon Sep 17 00:00:00 2001 From: Axel Huesemann Date: Wed, 11 Nov 2020 09:09:40 -0800 Subject: [PATCH 3/3] migrated to Swift 5 --- Sphere2Go.xcodeproj/project.pbxproj | 53 ++++++++++++++---- .../UserInterfaceState.xcuserstate | Bin 171250 -> 244886 bytes .../xcdebugger/Breakpoints_v2.xcbkptlist | 20 +++++-- .../xcschemes/Sphere2.xcscheme | 6 +- .../xcschemes/Sphere2Lib.xcscheme | 24 ++++---- .../xcschemes/Sphere2LibTests.xcscheme | 6 +- Sphere2Go/PriorityQueue.swift | 6 +- Sphere2Go/R1Interval.swift | 18 +++--- Sphere2Go/R2Rect.swift | 18 +++--- Sphere2Go/R3Vector.swift | 16 ++---- Sphere2Go/S2CellId.swift | 21 +++---- Sphere2Go/S2CellUnion.swift | 10 ++-- Sphere2Go/S2Loop.swift | 4 +- Sphere2Go/S2Metric.swift | 2 +- Sphere2Go/S2Point.swift | 11 +--- Sphere2Go/S2Polyline.swift | 23 +++----- Sphere2Go/S2Rect.swift | 2 +- Sphere2Go/S2RegionCoverer.swift | 18 +++--- Sphere2GoLibTests/R3VectorTests.swift | 50 ++++++++--------- Sphere2GoLibTests/S1IntervalTests.swift | 16 +++--- Sphere2GoLibTests/S2CapTests.swift | 10 ++-- Sphere2GoLibTests/S2CellIdTests.swift | 8 +-- Sphere2GoLibTests/S2CellTests.swift | 10 ++-- Sphere2GoLibTests/S2LatLngTests.swift | 12 ++-- Sphere2GoLibTests/S2LoopTests.swift | 2 +- Sphere2GoLibTests/S2PointTests.swift | 24 ++++---- Sphere2GoLibTests/S2Tests.swift | 2 +- 27 files changed, 199 insertions(+), 193 deletions(-) diff --git a/Sphere2Go.xcodeproj/project.pbxproj b/Sphere2Go.xcodeproj/project.pbxproj index a2bd3b1..10e9c9f 100644 --- a/Sphere2Go.xcodeproj/project.pbxproj +++ b/Sphere2Go.xcodeproj/project.pbxproj @@ -310,25 +310,28 @@ isa = PBXProject; attributes = { LastSwiftUpdateCheck = 0730; - LastUpgradeCheck = 0820; + LastUpgradeCheck = 1210; ORGANIZATIONNAME = "Axel Huesemann"; TargetAttributes = { 7700AE6A1CCFD15200606F25 = { CreatedOnToolsVersion = 7.3; - LastSwiftMigration = 0800; + DevelopmentTeam = V89SMR8J9T; + LastSwiftMigration = 1210; }; 7770ED361CCAC21100C543EC = { CreatedOnToolsVersion = 7.3; - LastSwiftMigration = 0800; + DevelopmentTeam = V89SMR8J9T; + LastSwiftMigration = 1210; }; }; }; buildConfigurationList = 7770ED191CCAB33000C543EC /* Build configuration list for PBXProject "Sphere2Go" */; compatibilityVersion = "Xcode 3.2"; - developmentRegion = English; + developmentRegion = en; hasScannedForEncodings = 0; knownRegions = ( en, + Base, ); mainGroup = 7770ED151CCAB33000C543EC; productRefGroup = 7770ED1F1CCAB33000C543EC /* Products */; @@ -441,24 +444,26 @@ 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 = 3.0; + 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 = 3.0; + SWIFT_VERSION = 5.0; }; name = Release; }; @@ -466,19 +471,29 @@ 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; @@ -501,10 +516,11 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 9.3; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; + SWIFT_VERSION = 4.0; }; name = Debug; }; @@ -512,19 +528,29 @@ 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; @@ -541,9 +567,10 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 9.3; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; + SWIFT_VERSION = 4.0; VALIDATE_PRODUCT = YES; }; name = Release; @@ -551,9 +578,12 @@ 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"; @@ -564,7 +594,7 @@ PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - SWIFT_VERSION = 3.0; + SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; VERSIONING_SYSTEM = "apple-generic"; VERSION_INFO_PREFIX = ""; @@ -574,9 +604,12 @@ 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"; @@ -587,7 +620,7 @@ PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; - SWIFT_VERSION = 3.0; + SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; VERSIONING_SYSTEM = "apple-generic"; VERSION_INFO_PREFIX = ""; diff --git a/Sphere2Go.xcodeproj/project.xcworkspace/xcuserdata/axel.xcuserdatad/UserInterfaceState.xcuserstate b/Sphere2Go.xcodeproj/project.xcworkspace/xcuserdata/axel.xcuserdatad/UserInterfaceState.xcuserstate index 6dbc870bd1a41aa6f14d40b533d33d3a3c0c9b83..9998ba3b9edc305f5bdcbf91877a1eda0681e063 100644 GIT binary patch literal 244886 zcmbq*1$lER!dZ|IBRC|ugpeF05F-KvDerK1D^75CcWEi^Qlvmz zic?w~TKMm~O9E8BzkWXdJIgQa-lq^C)5k-4V6IU&_D=-LeK(eA+!jpgcd_fprz07)U52heSD~BG56~^>C+HsZ8}vK$7WxQ7 zFbor6DolgvFdOE;JeUs)VG%5Y<*)*_z*g7>+hGUngk7*3t_^$ObT|WU4Yz^Y!tLPp za0j>}+zIXscY!nEZg39V1MUg;f(OB0!GmE84#7j)Bt;pF0v3=ge*r^BZrYA$T!Hh$Wi1wLIwR>C&I zcEVo5KEffwVZsr@al$FWIl_6u4Z>~0?}QJ8k3@(F6Ujshkxk?hg+wV)Nz@RvL>37mQ(g)HPY#gd$w}m7a$|B!a(i+I zayN1|IftB2E+Cha`;hyR`;jZiA@UINQ1VFf81h*1B61~pF?k7jDR~)rIe7(nC3zKj zHTeYjB>5EiH2DnqEcqPyJoy6oBKZ>eGWiPmD*1czE%I&hPvk$zAIKjm5Cx_n6qG`s z5Gf=InIfc!C}N6)BBjVEa*Bqcr5Gt*ijU%_G@v9>(kRU-ttlNTS(G59FQp%)g3_Nd zfHIIWi1HOBL>W(+K$%FHM43#PLYYPRnlg_vpR$dzow9?nld_Alo3e+pm$HwtpK^fm zE#)ZX1m!H{0_7^@I^`zi4&`UcFO+AL-zdLRo>SRW4mFBei^`?)sC=q`Dx`|2Vyc8H zr7Eb=R6Er{by8haH?=m^LrtcpP*bT5scFqCs3zRXH(Zv*HgDrcT&HheoH+{{f>H!dWL$IdXajG zdYyWU`YZJr^*8G8)aTR})R)vZ)IX^os2^ztnvoVqGtta63(ZQi(d;w_%}I07+_c&> z56w&S(fqUkEuNM{OQxmJQfbX;?P(oo9ci6tU1=q>Qd$|UoYsfdm)4I~K^smRK^sXM zMH@{ULmNvQN1H;MO`A_!KwC^(PFqjgK-);$MB72zN!v@?N83+3Ks!SFj&_W8fp&%V zfcBF1iuRiJhW3H>kxru1=q!3Ix_~aBOX*6whOVU>=~lXr?x!cwlj*7S#`Ko-R`hoC zZuD$=4n0WEqYt1Dqz|HhMITJZ=pp(L`Z)S{`ULt!`c(Q1`b_#f`eOQW`fB=m`eynT z`d0c*`d<1z`Vsm!^b_<;^vm=s^sDq6^r!S+>CfoD(SN5ur@x@TWWWrBfiegTB7?*r zGbjuegU=8%Bn$;Z&4^(b8F35~!^Uti+>8W914bgFIim%mC8HH1ozb4rfsx6`W0W!~ z82uSvF~%^aF{U$SFlI6qGFCFyGd3``Gj=ofF!nMIGQMFPV;pCkW}IPMU_4+vWISR# zW;|g$W&Fx`#`ul#JL47OPsRr(fk|XCnJgxo$zet@YcVBEDO1LjGZjod)68@+y-Xjo z4l{w7%uHc6Vm4+rXSQQ@VFsCb%%03bW(l(ovoCW1b08CA&S1`D&SHMeoXwoWoXecY zoX=dqT+Cd>T+Q6b+{E0)+|As>+{@g@{FZr?`5p5d^9u7O^A7Vz<}b{L%ty>$na`Ln zSTq)$#b7a6EEb!^VMVcOvA8TDOTkjIRIC`5o@Hg(Saz0!b_b_`q3wz6$(JKMo_vc2qhc71jVyAit?yE(fBJA>VU zoyqRO&SmGZd$aqp2eF5)4yvTiK`Br`c!N zXW8f2=h+w77ulECm)Td?57-ackJyjdPuNe{zj9CxnM2_)I806yrxr)R5ptv)8Arv5 z=IA&v93v-=_X6 zoIaesoB^D#ID8zWpQh9xm+Pv#FcU7+-Ra{uIh;2}JeN9Iv@ z3?7pg#jC{=@Ps@mPsUU6qIo)A4A01m<5_t&o{Q(^`FMU_U0yw2A}@*8ke9}5%4^1J z#Y^Y4<+bB=;&tX_@w)N4^Lp@l@$z}ayxzQWULRh6-T>ZU9>yES8_pZe8^fExo5-8W zo5q{P`}v-@!Rm*@;mZ7 z@w@V~_(6VmeouZcei6TzU&b%zSMdAuzv2((59JTzkK&K!kLOR|PvKAH&*abI&*jhK zFXC77m+_bLSM%5KH}E&|xAM2~cky@g_wx_%kMO_YALAeApXQ(8U*KQlU*&(#zsdiB z|0Dk={yqME{$u_V{%`!>`LFn|`S1CE2p|D0APPtVnt(1~3pfIvfG-dWBm#v%DbNVC zf>?n;U=~;e4uMnP5qJghf;xf(K?6aGAXU&<&_vKe&{EJ^&_>Wf&{2>n=qkt&1O<75 zo`OO_k)TviCg>-q5DXH0B^V+YDi|pkB^W0dFPJQtBA6kVDVQUeD_AI4Bv>j~CRim{ zEm$wuAlM?|8J;5Wv zW5F}QZ-SSCSAutf_kxc?NJtP8g;XI;$P%)JTp>>=5{iX#p+cw@YJ_@Wtk5Jh3++OO zu(r@63<%?e^@Rz-WMPW1k+89_xv+&WL)co_Uf4m{MVKkf7Ul?Zg?YjPVWF@@SSsu* z>?a&393%`0hX_XqM+(OZ#|bA1Ckv+wX9#Bt=Li=F7YdgMmkL)3R|(e%*9$ibw+MF# zcMA6k_X!UP4-1b9zZ0Gmo)VrDo)=ygUJ+gw-Vojv-Vxpv{vv!Rd?fr;_)Pdh_)_>* z_)hpi_)&z42qKDzDq@OQqFN%ZNGKACWFomJTBH`mi1ea3kx66|*+p(qZINFT5Y-da z7bS_3MQNf&qGqDzqI6M)sGX?2sI#bxsGBHT)I*djDif89`iT09`iUw;{Y3*rBSoV` zqeWvxV@2ac<3$rhvqf`6b4Bw+^F<3p3q^}WYenls>qQ$x8%3K$n?+kh2Sf)&heU@( zM?~Lr^j7pvOcImD6fspy z6Vt^EF;grSOT<#KOdKmVh>hYnu}N$eTf}~GKpZb_C{7bM61Nez6}J<&7k3bM6n7GL z7Uzk3ihGIk#r?z;;{M_R;*sJ};?d$U;<4g!;_>1M;@RRk;<@5^;+5i6;??3c;vM3h z;$7n1;yvQM;(g-%;uGSN;#1<&;%nmT;v3@o;s@e~;z#1g;wR##;&N<`(Wa;lsrr^|(Mkz6d-$z$Ytxl`_v zyX6V;2J%FCk~~?SB2Sg4%QNJyKd!{w9Z zQ{+?S3*-ysi{zE^b@KJ{4f2ihz4CqX{qhs?lk!vY@8#Fz*X8%*_vH`dFXgY~uNA0* zpdczpidqV;f~Qa^q7`a|MqyRh6n2F}QCCqGD3CMX*yn<<+sTPQmzJ1e^=dn$V=^OgOS70Uj~5z3LuQOaq`>BZ%&48mpS9+Ns*BI;gsTVyy}ALmg=_Zj_Qf(sp?nNd(|JRKclJ9v}k%XBbph_ie^W1qGi$YXhpO!IxgB2 zZI1Ru`=bNV@zD*V)1n(iw~1~W-7dO)bTGPmbdTuL=(6bY=uq^K=%LXQqbEgAj-C=d zKYBs*!ss>8Yopgi?~2|Xy(fBa^s(sU(I=u$MqiEoKKfepFVXj+??*p~ei{8L`gQah zH9<{OlhkB2MNL)H)O58_EmDis5_ODPuZ~q4)NXZcwMXq$C#jRwDe6>px;jJMTHQw7 zO`WaIQ5UOwt4q|S>cMJE9a4{1Pf$-(&r#1+&r`2duTrm8uTk$%?^N$nf2%&K{!V>N zeO!G)eNuf&{k{5{`nvj_`o8*s`lb4n`n3kt5Hv(hl%|%3tKn%>nrMw$W6@YOHjQ25 z&^R?Nja!qTX`o5eBxzb`T54Kpx@a;rT{Zcd0!^W&NHah)P%}vLm1c})tY(~MmgZ~C zY|R|aa?J|OO3f51NOX zN1Df)Cz`jKcbfNFvX-KyYH3=!mZ4>8Sz3u!s+DPDwFa$GTU+bVdbK`nvNlDVs!i8s zXj^NuwB5AX+8k}MwzsxKTdEzb#k3*q5bXr*MC~N)WbHideC-15YV8{BTJ1XRF70mZ z9_@G9W7^}|6WS})RAQR_qA@eZjNrbZiQ~8ZmVvaZoBTV?uhOi-8tQP-38q(-EG|+-4oqY-LJZ5x<7P( z>OREKV(2l97-oz(MiL{9k;TNu7-EbuwPQRn-WXp@a!g80YD~kJjF{FjZDP8`WXI&h z6vymH zn9DI&Vt$JGIp%K6FEPKzJdb&yhxD)>(X;eyJx3pD$LkyF z)AWt>ZS-yR?ey*SL49|94}Gq_Okb|=qwlL9svo8wt{SAMJ9kI?>S8V;*gxChL&0?F!wutQ%+c~yNY|q$UvH7tDu@$lXV+X{Jj2#s_ zI(B;OjM$m6vtpOTE{$ClyF7Mt?3UQAvD;$z#U6@19D6kOyV&!w7h*5QUW&aHdpq_{ z?2iVM!Dg@<+=ki)zae0#XQ*#TG9(+)42=xU49yMch73bHLwiGKLl;9gL$;xZA=i*^ zC@}OkloiD89drD3gM zonezEKkl|ayQNsztNyAygIm0ExWy3YYb;Di5FNS-D`-TUGhlUr1 zmxfn{*M>KSw?@=RFcOUI(jyD?ylH`Xzx8XFqZ zjE#&L#@5C*#RV>e^AvA|epEHV}w2O0+%zcLOsjy8@ljy29S&N6;&oNb(ATxwiq zTy9)p++^Hr++y5n+-BTvJZL;*JZwB-JZ(Hyl%W$o#T4M<;La3^^7ZvE060FHzaOo+_1ReaTDVv#Z8Wz5;r$)Ufle+1#zq6*2Jxi zTNn2*?rGewanIvk#J!1o8~1122NPmKO=J_r#4vG8Q6|1gV3L@mCZ$Pb(wcN8gUM*J zn5-tJ$z}4Ie5N|4x~2xEL{qA%p{a?fsi~!@m8p%Xt*N7_lc~g1YAQ38oBEjgn);b4 zO#Mv*OoL4$Oe0O>O%qHrOfyZhOmj?gP4i4kP0LJsOnXiHO#4j-Ob1PeOovTJOy8J} zktL?Hrpu-)rdy`lraPt|O%F|v%&-|Tqh^AcXeOD-W{R0=rkPo0zFBIPnYCt}ImWCv zTg^6eiaFKX(41y&WNvJ3Vs2_~W^QgyH+L|1G-sK+nR}Xhne)vB=0bBHb6@j#^91um z^Ca_R^Az(`^EC5x^9=KB^CEMld4+kUd82ugd9!)9d5`(L`GWbP`I7mv`HK0f`Fry< z^L6tr^DpMd<|pQt=2zy|<~J6|0$YR@kwt8gSfm!2MQ%}8lopjmYl*YiEe?y{60js% zk}Mr89W9+Koh@A~nU=1WEK4^_wxtJIPL^5R$A6twpg}Wc3bvXj#$32d}}#vIb*qN`NeY2a^Ld6^3d|g^4Rjk z^3?LX<*ntN6|%xss+DG?TNzfSm2VYTO;)qjVzpXrR=d?3PkwcOgr+Sl68T4C*P9bg@79bp}39dDg(onf76U0_{k-Dcfx z-C^Bn-DTZv-DBNr-DllzJ#0O0Jz+g(J#W2cy>7i>y=lE`{l)r+^-t>u>qi@8gKdZn zwGnJY8`Z|KMcITlkxgZbwyAAKTbwPy*1(o%OR^=~Qf#TVhPE_YBU>|DYg`7{X8Yat&i39OWv^xD+Ie=qU0@g5 zMRu`WVprI8_87a#ZnnGawe23e*Y2|?*c;f}+1uMY*gM)g**n|2*fZ^2?OFDqJ>Onn zFSVE12igbOzp@XukF<}n&$lnIFSIYRSK1fbm)Muum)V!wSKBw)H`}+^ciDH_58IE} zzp;O7KW#r_zia=+e$Rg2{=ojw{>c8={>1*&{=5CH{i6eNP#jc8l%tj-)?siM9dQnm z!|bp)tPY#Q?r=Ne9d#UujwDADM^i^LM;k|5M}eczQRFCg^mdduN*!g6az`IWe@DnM z(lN?0$uZe6#WB?}$1&Hj$+6k7#j(|~&9U9F!?Dw`%dy+B-|?;Eq~nz1lH;=DisP!| zw&RZDwd0NBt>c~Jz2gtZpN|9h}{q+0GnizO%qN)H%#K+&RKI(mBdG+BwEK);Z2O z$vM+G&pF?@%(>jT!nwh@(RtMQo%5LUxbuYbr1O;XwDXMftn;Gty7PwfN9Rw@N6yF2 zC(ftNSI*Zi)J1ktTnrb}73HesQoA%RtxM;Map_&LE|bgda=84ifGgfr$5q!=&z0y( zbv1UicXe=ebairdc6D)Ox`M9mu3oNESDCBa)yLJ>HQ0r@=DOy&=DQZS7P=O>DqV|R zOI%A`%UsJ{D_m<`>s;$y8(dplJ6wBQ2V6&7-?>h@&bltTuDWizesJA#J#amAJ#syE zJ#)Qqy>z2)f}7|jxyf#do9bq}Ic~mN?pC;!Zk0RMZE*YCes{nf@2=yn>#pZcbT@RT zxf{8gxm&s0xI4Hr-Cf;T?q2SEcY(XmUE=QJ?&}`v9_AkI9^oG89`Byup6Z_Cp6j0H zp6_1fUhZDuUgO^A-sIlw-s|4y-tRu(KH|RUzU02_zT&>>{@#7f{e%0C2lAjEl85SH zc-Wp=9==E9k$M!KXph#T_ZU59kIm!s)b{v1@t%5~2A*V3Lr-H*GfzuThNrEkgQv5n zt0&vj-IM3Z_Y`?bJmsE#o&lb(JR#38&q&W0&v?%y&s5J0&)1&0o&}yt&r;6{&uY&) z&qmJ{&vwr)&tA^~&tcEEo@1Vqo->~Fo=cvqp6i|;Ja;@ld+vE2dY*Wmd7gV-dER>d z@O<yFH{h-7P4FgpQ@xG6O}#C= z>E1Tp_TEn3Om8=D(3|V+O&7?-cKJ?=0^e?|knf z?-K8F?<((F?*{K??>6sF?;h`d?;-Cu-tW98yr;eAycfM!yw|)py|=wTd4KUf@ILnb z>iymO()-5y-uuA^`v^X=kLF|gI6kgV;1l~~KBZ6X)A?e3aXyRB?sNG(KEJPyuf8wQ zm*PwFHSsm~weq$0wexlKb@65Sa(q2}J$(hfVqdARkFUZv&^OpO#5deG$~V?G!8h4A z%{S9G+c(d*(6`vP%(v3F#<$+L$+y+F!?)YF&v(#w#COzp+;_@%)_1{o+4sHghVPc| zN8erFecvPBQ{QjC7rxiNcfLRUkRSDv{8T@~&-T~y^Zg>f)UWVI`?Y?(-{?2{ZGNY} zw%_NE_t*0`@F)8l`WyS3`CIxk{O$Z@{(k-n{~-TY{vrOM{*nGs{&D{C{>lC+{u%z6 z{yF}+{)PTU{-yq9{#E|f{`LM1{%!v4{@wn={v-Zl{xklw{)_%={@eaL{=5E1{wMzD z{ull?{tp2tKnjop%s^B?7?1}v0c{{QUgOeB1cU_#W{^@fGp?;|IhKj2{+1A%0@~r1;74Q{tz_ z&x@ZQzaV~L{HplX@oVC@#cz+_6@NVbMEuG4Q}L(c&&OYizZ`!h{zm+d@%Q3iwAL1u z=jUI5m=FtMLmY?)iP|MXwAuMtrKO9ZD5w_1#Sjd|2#X;;B!Gk% zF@$(Sgh|-AOpey;_PLxskKOD_c7d-%r`zmH@_Ee-91T47#3XO+#N-rEEWzb+yAzT< zW|zHz!|d|fUFHO@J;7W%DaD=SOmsOMK4&I}mzI>=wy306X>nF|FsUfJydYRuR*k6~ z(nI!@kOERdDkvIKLmEg6>7W>lgpn}{M#X3t9b;fjjI|Pqg$$4pii1p`umu8p6pW2= zLWmoS41_IWgJma(WzKSK-R!L<0cLOLBrG!JL|&d;zC4lM_?ZD#&S& zRnj)Av`t=VUbp;U+q|;e^t{3z`9YAA$%*|On+Tqn&C**1OUsH%vOw{$El^TcSym=T z35q6UmjPpP$Q@@7KPQqxEK%PV**Twi7+uH!K9cBlVgf?U?`hG&7l@hOQ;o;4rM^Cp*B!k zs2$WEiUZ@R!gN>+)+_{@upJ>p7DBWk#2i9wA;ca+99TT2&E#+Z?&-NjeVb+V$?K5? z2pM^mR#;jLY_Ez+5=w%AoKc`u>$1H3yt2GtY1_PNA`jKjUa86E&h$;%J6$ih3tZ5k{nDyc{<$tuqM zRI!>!H6)-`4`4o22-z1w1z7YVs0dTz6pbz~4VJVh33d;b0Ip92=4m|u#N>Wu!9v{1 zEiI|^e0ZrTg~}FUn)6T}s4vtHs^}0|VG6;#YZpldOS?2nZ(3f4vvxvBNmj*TP_92T z04z`8$9B!qn};prA}|I)Ujh7UY9!?W_Ae^TDgo67L4%tUXmTGS_E3BebzM2pkNtFEGh=~y^4$S3d{aSQw_4pvU5|wQvL7UPp>HGRs?3)zjZ%S``>b# z6!rKY&DV6Jntj$mTOs>0Xg#z6+6ZleHbYym+L#CPVm{1|1(pHXvmGo_anLSkH&}P- zV9mgH2dpE`19C9Z_zKj#Fux+LundTb(y}nILAnqOkPzgD$3CMZ3(O$!F1*eI?1V`T zQfv4FA2*QK6!1=DMXrX~s=Tlez-y3Sl-;WopYT<}r4?Yjs*+_{*}W2r3W`C~_!MoE zm0upDRCnzg=mcb63VjP5g}#H1LC3MWSUs#hmVh-_3Y~;b;o=lJizQ-7VHw&G?~)$0 zlH4ztU5?Md`utZ!J7bl0uTbmteG5sb?1`%6=xOZ1as2Lf(4*!y6X1{ z`G6gAD)7z}rWBPl3-)c11s0f`Fby&}c2KTiR-a&6Ay}HT^7Fwu6M>W1qIG2b!bzE% zmk$~)tg54^hW+K_1FKB9@h-N0p6=akIZjXa+IG7wvRHi&-Kd1FLD#WlECox&CvI^r z7%4|;QIouG_(TFY3;y;_rPHbGk=5xobO$UzRjXNguxv4O8~PC%(7IVVc!;#|GgP(+ zx{IY@noH1q=mFS`J%k=XkD({fQ|MRd8IU;Dd!Qyo*;zPgyL^(+k>wkdD9Fky{PY?Z z+<>h8ns=|R`za5Du*O(>tP$1@(6MGRMi{D!7?yt;#Yh_woS#E)mOw9{m(VNdHP!@c zfu&=uv9=eWchGx){~v(oKXmxiV(Tz2SX03M%`-W}Y^roet6&e%k;Eck^&Bh-6Vj5M z8^qZWya8jEk=G-)EVe}DuPJL%W#f6^O=2)VAM8|$3c)M9PPi<1-vI1TOsc$`sv>DQ zCRIr`c=)0%gUVc26>H!C%Vi*eU<5|N(hE2vVqP8DPT>fIQEriyS5gTRpaK6!nW~P% zB$y0+z*=Ihu;%SDa&Zn;**S#jTn0`SEF(gNzjIP`v8ZsdHj!ew_#9l0t4bqNRMowx zMD;g*s~h39{;;4D7nIz=A0vN}E{b7EWDohp5P#}#t02ArfN2`$tx8ymcNgDt{-^R_ zyFOrmt!k_1aQjRukJmY95C^GRM_?r!1C~fw1xLebSOaTe9o8A^f@NY|u`I0HGFT5p z?qt{qO#s`qY#?M^SPl@cL3};=7f}(GgqCVC=dNMle-&hr3V)Gix|(`lNV7=U&qUh) zM50>Q{EOU+0E}y~j?1uwzJX{B^3)h3| z!wGN$I1x_5dSJO&9@Z1

)fSRqz~6=S`zk~MG&oC-Gt-zh+v#f86tT!UL;rC2%E z2Yi6M(u07ADcuORpraT{JU;jVDjLJX|=_{R4~ zIJ**;1Cg8{bYmfw8cFAdx5q%Ld^W3GZVNa}dEEAz6BL{emqGUBZ~?R#E`p2U-f#(A zidBG3M}KSpHV_+xeYG4ehx>#Fs{-y1&P9W<@!=twfK3L!V0Pih?3$kA;y0rt2zrAb zA=7*16&D9{!t3(iNrsa%9jv?s!N2FR@w}?dZ3G_PO!bI`1#RRtE{#*N08{*PE7j~e z3?2cA7Qw?YY!N&X3*pKJyovBur8FUoa&^(M@OW@0fyZG(7Qqv+pma46mugk2C+s8D85b&R7p`tfAN@Y$7(Uy31SP?KSk-flb1+)fv0ty?DlS zY{VjX4(Me~#zFXS4X7j7luw|J!pCYr9ml4As&NWF3)w3H-Od0w)9?fi5P$^i_D@2; z8imX7b;!O1F!5FRd-xh)-x=6UY}OL^27D9#0ltNOjm^d8Ve@e~_&f)E7i*uGuGl!GaBfK#qRmUYsMVF}VxhtLgK7Z1$%TToS zXLxvROl*p8s>@6Np8eMapo*>t7vTdcAUtgOB1C|#_#YD)ksvZaX+(;xT!hH6RhYJY z%`~ot7L5Qc;8OS|qCvEX4v7Kmu^L;0t;N=Xm-s&t2j2uQ@&EPUqixrsq$mfRQcF8j z!2u)9chhc#9-?dA#JejpEBAb9YF?^ zj_p_!MpWA&t+0EMCG3G%-CS2B2xc0Rg>*x*ksNFnwj0}n?OlR&M|vQ+NOx=>wjVnH zzVR6w8Qt*lsX+xUZNq1`s3uvZWi?C{Id}?M7j_Se-M2OvgqD`~Yp<75teo-YvoXCOT+Jpm9gcQH*O zt)2ueOU}y`%Kr|6X#*glH2{RVWhn)NfO=L< zCC&EtIeXsWbp2;YJ!%sHq}l*|kMtiv`TT+Z4ysNxJ~Rea^yO}qb?aBwtEUNT7{OO% zLIX|x!VTd^P5n{1;lx7VzZ(Tf!G%N}s4>(5c;)5*Woj`vxD5o}x6^>%?HZs<+5+td zKDOUN-vgDvZ{U>u4xCg8z>ii2#{o}SA6y6Q+7rNbygi%?Hh+EK3g8ht8D0qdUf01J z;luEE@O9wh`W$`({{ehkxkwD+M(QG|NF$^v(gtY{W`C~;mIxgA1q%2t{*e6ez5@v% zL%>x95H0zU%ZBFVW%$+v*D!xNLRL-g#mG=(7`{dMkL5Et-7+)EQWM+QYbUkI_|yhC zkU469di<=(@F~Dk)m&zq^h~GyQ@QXoNe2pjaC-lAg;BkDj6lZX3*Sg&6fznagMEt~ z#lFLiEkVWsfjR-1h#d#w^b~d)U-=JevyMkTCzQ?YuLUuqckzL@k2iXh0_d{0XAa)(7 zC~kt!YwRt4+X7+A5Deq=(}IP(MpIiuteUe%^(jm9kHylzSX(nWhJP%DpQ#hdz`n0r zIZ&fVHh^Gj{CU}?xUM+8tRf#AYH-cu-!ZB_sI)5T8{UEA2TXj&^!JiLYUG2<$iKIU zk8n+K9I42W5LfQjysn`Do?j&|KhIC=Q3!UUiCLupQC$E3_lnhsRx1F3Diw$!AdN2~ zr-7V1gPcXqA?J|`$T@I#TU`j~9b07;_5hlFa3}?rCQX8cJ<4)9D0Tyg3;Yy{TtY5` z(`$`-GE4(pM~PfPuHw`9?^ytRb*YQUHRL*S12lgV`2o3w+y+flj{}HVP(vzw3j$E7 zc`n6%z+Patu~)c+{t@{J`MEu~Z!QgQ;jvqv1Y-onUyyrXKGvWZX<#w(3vwSCP}5-e zU`gWkK&L}A_j3{b>s`YN9A0JWZ9~!JgK0*U7q6mtj1c;3iQ4&fZ$35H{N=F%0eW^t0 z(10phpe(c&BwCEJ!_;_)(_=BpMV_O4>=B^Jb8H;AVgs|ZsN@nrMvO{ODJnzdr~*}@ zDl{5&J)DW-0tPgkPRFn4JA(&s`;gGP$*wZ!<1VR6^BlqYT!G2|t zc2FG}1KQDp_F_>3at<}3asZ%hF4%sB+bAe5Dg>kk6SEd*r5s#&7kp~CfelKusqEOV z*zXrnGipJt0FDi{gD3~i48W;LK+T?(ms_KX8zzcr7F0-`ty9K4J35kS%c zaJiq_DxrP5_8nB@8$dh@!O&YEc2;(2aYBn0O>n9g0yHAj|NQ&aWf0W^{9Q2Pus+ZE z4{2gfurwQ#I{-cvIi=YJ5Cq3V5cz&~aY-2jQMKT`rf*p>o^A-zyj~ft5<$8X1W_1O zb-`;$nHnqv`mSshxV^1VrDhcrWT}9?o(i0Jz@Gdcu*NI>|NIr?m*aTje@gISl=ez* z41O&D|6w^avmyye`0vl+iseB@LugK)PRUNkjZS=r3tsaekQ308L1Ll$5u^`wsd@P6-ya#%YN2 z1(sEkoC?x_R^&ZU8#oIDaVY}fT!J8=OCfXzQbDC4oJ*f@ss{-4QUE?GfPo2oQbDOg z@F@W$Nch0cIY9{9KS~3NF6zH~j#91>iRkye$WLJwW>NRvdtj9Kf^#$2Ecj zo{zWkpsJN3P$MVYR-_)@OFWksm9+`>I42tl9{C-?*iO+c6u8+e}z;ABA+ zkU9K5KK!n_)lWUg`}Uxw)!qPCyak+Uc<=GlC(1-I>R@Lu+8nwT-ZvJB2nt#q0VNS{U z8()C7i06rCi9Zl8LMrh4Gw~AfK1f{$VNGrjzpufk1;D?WA8^`7XwU=DA@cNzv$Df) z@Uh4T|MH^ z&TwiCx(EDktf}`Uhev4eC5PkumR-fsIEBipcEH^8G1+##+9tbi~AEY!aHLD2-L1AY#1mP!= zSkfALq=v^4$0IUB^TNOQeEGDBwFkM?oPhT)7vzBc=2`Y^Bw@`pH{Sg#{Tn} z5hVFZdJuL<4{Al$JQXR4)R2?{sYrJGJ;_RH1kygdR2kT&0eJ%f`=5#u{61b4-fpVW z3bQRHmBVHS(hc7;;bkD`dCTx#2o{{oE-&d5c}Kv1h!73zr+6SlnF1JV#)1)XfZ${S zATyJ}9-uie-)j$olVyX^EC3Yi8&-pi02Wx2fML~a5RPmK2tT$C7}sov_Jma;C!n(+ z;MjE_PwoOciDy7L@*V^kBg0IX3yZ;Zvks_89IzK$Y$n4^;8s9I(iMaaD+JeJ13|E` zu`sy$ffoTq$wqi5d=Necp95D8ci;yg7}z@mMHmPVkpp#!74aeoNMj@e=?pZZy+Met zVL(kV3#kN0=WWPA5W?#cavK~sUZN1#ZVS+8umSa=i6A&v2edm{f_{aLL1&Zsg@D9g78z9hZdjb0gNiB6RAt6yFgH)yVUozD4L#Dhn7L>Nej_t(bmzvq1~W8r_P z2B$=l!hf9RJ>%B`p*h;{`|;=S_wsKGP(h3!O^`2`DA**pEO-Y3MkEM>!qFgz z!+GIrkraeg2!ap^>p`G{cVd+|8H6I3EZ#1@DIrLVl9rOblKGNvB#)&$;Fg~)9Ru9! zugMVLB;QIlK(-jTw!f4sfNOfO{A>AP`D29uxQh1#uHgF=50qSGU1c6{#@?@dsN$;< zRQak|sw1jrz?Hcna8O~azNw*UyqfNsX_`ZtXIiDUsdgZ6iM^(y z>b$^xb*Ap9?sbelrftkf;3#@uF9L3!{q?K$H)5Hw^od5kPKc!FT*^;StDX} z8*_ov;F&lCIQZoOSG{v4B5<-RG*z0em>K2-;HtLTe8(cNG_ee^Y_~kMYONitldVUs zA8c-0K9FwLfzwhW`w-wt^gD11$_6ey7oAMta5LDs!}*&l4mh;TcU^UJ-A#du$RYQi zwLP^t)retoLJmP5qqu%j(}tFeLO$Sd;LyfvrJF zgRKo-C;AfyB_2pZlTwq$B%MyKmE1accJdD?>XaTSt5cq(x>EBNrbyF9O{X@!(M;E@u-W$JQ1i6rQ=8vx zp>NT<#qO4*mMvP&Y57Yld#i!1j;3?dGt*b4zshKkF)`zM>)6)itq-+fx9QYoWt-P+ zliE&cd%K;r-Jo_S+DqE!wcpW!+@Vc}WgT92Ozt?nxgF%X@vyZ=1iqfKt%4U}s^i!d`_( ziWEitip~|s6^|&s-P_-Ldhcf?jZ2o5!lj){ca-tK0rO;eZ29o=JALZ*nbYS@-!^?W z_lxRR*zaV8v0`+^z5XfvmkuBd$R2QLVD!Myz&nEy1}*vu{;JzohX!i~4;y?JOT|`% zXrW%AQ$uV+rVM#8wC&Jc!{o!TVLuH|9lm-5YedP2D=qd$zv z8FPHBZS0J(e~il>cYM5k{H*aGCv=~1dSdN~^Cyue6;8T5x!&XzQ=+EypYqexrc<{~ zQ%xH)?Zx!W>BnZcW-ORVn^`{d_N*qec6_b*vbmj-LB^Ue3Jp z^Xt!Fw?MvN?1Fa-^A=uRl)7kJWlZIa#iYf37XPxO&61-_y-QavlPnv%?9b(e%Wtn} zwc^N1&&rjnWUD5wMpl=vez2y~nzL(@*6vtmUbkqyVExz)(1!924>xw*czILfO@}sn zH?P~G+cI}6ck7sK@V1I=&$j1ozrCaVj&nO3?L4$AuxsmX%kJfSqW8?%%ilY3A8p^T zeINJt-~aMJ@qxz&dmQ}fQ0AfQhua;#c%U_87nE%+$ zH0NTTj4&$aj0F|dgrbCYv(#t*U|DsLHXt-oFS_TD>5cTWD8 z{^RwZvVVH?bLr22+#U7{^OqU-l=oKNcii9eAmzc?haDf@eN_DD{o@f&qMpos8vAt1 zul0XD`K;ZuyT6tE_VM>|&&AJ|zi_=c^s>dvA6^x_djERN8}XYJZ@q7izH9sL-uu3P zkpGzVr{T|C9~ytS@v-3J$A~_vHtP98V^oQH!-mLt5avN=fy~AE%@2k|JXnX8d_1#>x{;ngM`8WMtS2PRg@BY9(;QG5S^>*DO zdOM(q`x95gwa52!VTy+poPdD6a0Mq?0L#NF)+XRuCw#|&E6af9vTE##&|+YARYRyR zHKvQvB3y-8jbI5{2JRow(h!U+Ld!!i`ado;&`qh zbXW)`hG0?%CWl~32&RT$S_q~uM@InT<2c}RJO>>MSwk=b*dWJ)&)5*m1V7*k0(V^i zH$$J>7KaU+!p7k>3VGaWt0C}n$pW!;fmJeoZxOMj`=SJHVhyZoL9|}nRy$%P@lVBY zPpM#48Odj${$i){&j6~fkRxz`x)Z+~`G-boEYNXO!g_CXM%ZF3lKvWcvkQCn$ju{0N~`*IdYd@WnEJZ zZ5g^6T%e)L(G}=QbX5qh6@s}Tm=}WiOVBmwTCmeyAA$uTSQvsuAy|x$f}qN>6zIj% zvx|y@iMd5Zz)J)e*x^^|$yucp8ATBj?y_J@9GgU(h8@F*;ciStgsp8)(-Gb1tJh!Uyo(=pMl0d(nL$PVr-K3_sL9t1vsa2)7h!9|oBb zEGcMTfL{{fS1%n30U2__Sat|!hfxW^iV!TJfVjx8JOnE*0*FV@Z_sbiqhRVl=rMpv z5L}JsW#^S8<&}avqKe23MLR&y0{ki{1D|vuScXH8R!{Ad*sa#3!Lst=$P#fHJ@c0y zMS253RHA3`MF_W$tm$q|=3m}_A_K0hj)1<1UdrU)rgmS}{(_p7=w-Z(|D#kj7hFYe z0UIUsd-NK59le3xM1Kgu(IHqJf;Ayn8-jHqI0pB70RQMu=+Ed~FfqY@2-b&SV+fAJ z*+K;n0*)3nldM5em0@P}wN`}vKQHpR`R1omxFzD}rNTFh4JyKBDqJuyk?CJ^7YwGA z`Y$y;bBg!`mX}>^wfgBEIpV#9J_VOd=&$Ir5F8tV4Vb2FaY+##tsC6jfoqDqvI>8V ztrD;>@|T9qBtN}VRTcN!|G_HA?HV*FGL2t_r*t*R-x8RReLaVZzC+)mf1rP&AJC5k zhyW810!kndh~QL7CQt}e0*yc?Fha071Y1HdSQc#|*dBr%A=nv$T_M;Vf@_ChPYCvg zU|$IKhu}a61|w4^1lJA0^*{>*7J&`5Bt)TW2wd>X$DKR~B7&G8AxH@_@J2yULUAFu zeh40gU-?c8Px$G0awb@J!Ts;75WG7C?+w8}hv2*T`?n#42^av3de{&&>>n4t0|mDh zdF5b4q9Xnp!IICs-5e1Ey{1|H@(RFB7;c7}Rgza)R2Z%a=1xh~xJBIS!f!dMs)zFY zvON4&3{16VP_guQm}g#0M-J} z1vhMkf3Y1Z#r^wm2@va%WDmhZ0L+#Uf*Vv4`~<*mi6J-%({_#wPL=%> zFt3O-9vP<0e=ai;)+*GiN419BRqs=SML@`wL?kw$9q{-ev=6~eYObjXod`K#??dQJ=t9UObR}dF zx)HKNaMKX{|JeHu_$bQe|GTYsdzakqLdpdZ5Ks|Ft``-h3rg=u2`Lwdgaip72x!LM z73^380Yog=5EK;~7OY_J6&v=3{Xfs{vsZF^AsqPfet!Q~KR|M~`#keJ&&)hC^UQo7 zq-i)P62BzSf_!t4y>~qx=hB*lo&B~ z$sROG9}eY%!bI9m^F>y4&Z0#?o~BE2kQi|PHf^9b4x7UxzoXVB3LJp zU1Ty@pF%3uMi*l+<-UGy*(@|vyEOZX>WO|c(9FSt%Q;#`#FRnRl`%ZHHwqBCHhalf z#niVt_u0I2u;bbK;>n8wK5wK)M{h73Su$}^&=>NP`j@2YE$)2o0<7NJWx++mC7O!m zR+ffgnr`PVwd`KSUB)ftmT{MJs6b1>S_W2J>`JiCh}TeY)ufaNSCrHeVSf1tQW&XE zAFUJOuE#VSe4-pM^Je(SdipWbj(ps(R{PYL;#J%&L|9gHYrq-&+sZ=hB*UhAGg#LDM+&)D+NG}RCXjsqPjmfFpU0|(l z)R?qpu%MB2vx0k+7*l3nO~NdnS!Ff2pB>%}Q{>{)NXn0N2{jrk zHY%AaV>`GfamZUOySS&h2Z*;@jndNPh$r)wMc9OgXWjI7|c z(drWJ1)^k`Q0_!}YAJNN41CExQ{z zwEpJD9S#g%f6;m^_7%5_`&!dtXicFOI|ZzZzD``G)6-q@DrRB@L*S7dH&s zdp6hRgtuc|Brov}-pS{H^)#@e>S_Qhn9H@PB%8~zR#?fj(@F!^V$R)gDhqr^A`4(W z^WWuqtaZ>dP!c=x9VzRbsSo`KWCtE?!*d(Pkhf)H12{c0*`!^I;y%hzr5GG`?f>GHLdIW7BJ0qy=~zcRuQE0kKiY z%^CbjM5UtzHn5S$eeSxr$nv%P95P*X{A?bz)AeAz0jxK!-}Ke_}^z}wX3L`RZNo6V#Nz))$~Mnb9QXb+=V?! z>4ULs*YY=zwOP(z$K%3aBxfC1Z(GS;$lt`@i~%&T-VW9~$l{QYKpx>Cm!-tigFkqtY$frIqZt&>AeTNJfg-_{^q_;6(!iYYYmuIw*tSY>I zM2ZQ7eAEx-*I3?Z&;cch>Kx2aZpj|dDpzzHAz1@&r5bpHKHQfGufGh|2bybOZCZm~3@_}! z6Xe%0;=IFZ;a}GheB%zMo*s-|EUpbHR3T2NR%Qv-&ANJelYa{>kNB4>__r;Kl0Pu` z>s=m0M2$R}1`jv#AAt3dz1J+C@H-Qly>r-HT8m!NN z^;xiP1M73>#0fke3D@OJIGuNhlK9S-K1D!MYu+JBTs%3R#H*<*q%vxM@w>+qO?$tIDg_-(Mh6W1iThC$+p z;kv!qFnqFJ7p%K#GNP$ys)f6rs1|hAGU*nofzk~;VW6&VP?|KiruQDzje$^-P&lZ9 zNUgF?(>VQ?QaRL;#z}Y}iQrqB0ulyN1@xY#fP|4K5&{M(J~mZA5ii!SpE!JWtaflM z+BN8~YNr&!cwsW0D+m)XndL-b5?FVF6(#<2uzt}bOcAD9Mherw`XyMuC8GU3mU0sN ziI{B@H4}>bbfRt=>2W{}PJjge$T#tLn9>j-@=JwN4^72C=6I3sDUU3&8qaqh%y+Rm4ulbDH|P8d|+fhHN~2n$SR= zMwjD7Vho&!ciwmr=1|rXAuNQPjC4LV&RWBfTN)5MB{p6>!5BUKidF z-o${E_P_9+{x3%Nj37!%EJu(0bt0O^h#}Gfo#qik7Y1GC#^MoCJREJ85;JQ4ypr-Q z=_fgigKr!3m_!sYgr@xuj9vdhpRbRFPm<3Tl4fCr@G0?<66X#{-|6puCSVm!BF;{~ z*O+95@CA9<{7YO^gM~_U{z@`QgbazNEFz7=Y_tdvnUe1I0lJS_cEu)u45VW$sFGs5nd<4$Psz91cwP1i_=kv8i2% z<1F~xVGUi0SxHhnbhSnCI*o~$Bu;JiTB$e^- zHR-ZPqRTiKCVAiJvQ|791MFg*I2)K^U`keylT&gcO17mA+P>Zl=Z}~JaqOaY@h9kc zMbfu8mwE+xsoXC)mffJF*GjLAbtauDc=fAUXYqNAfINtHwSf8mP2oCOcM0d;qO9J)9 z*!h_1xuh0YLGWDti&1!HfR(qXf7%-#4a9vHE0ANc+W4Rb_t)z``;tRvgeDzapY#y< z6|D%|vBwS`+#gFe2jfv_HBnSpOIkgL;7!zH2_qr_lk!QD-J)-PWF)z8F6;-u;ZE?SK3l3yHyJM_Tnnwol@WS!3A=FHY8A@)Y)0wh=;tgagytPMP_ z3~RUB8#p@xCJPOWvc=Wlus7t%R^{Nw?v(R;7MEadzdsN}9E=l2jv75~@|3A%l{#QF z65tDV#G*?C^(C0T_#^%j2zWY1J)Vwe5U2c}@(Ui$MWccK{rmd+^c?8(_3at(go8c% z`ujzCh6nVG^y$|h+X?j@hhsj{??>Gi&!i|`K%h}cKm~V-2XjZjcG?H z96=-pieWwB6m6CV_s2~LV`B73KM@;lkmPO+cD?uT_G6Ci-Ny`?7K36~H5`9^>|kvl zis5-V;@-w;D`G2p_eCzyC3{Ez0il6|jwi=9GgEQ38;@&nHV$r(8`|y|GBh-7_y{!S zaOE||)0cYe*unkvTp}EE>Q|&Vm;hWixNu6t1H;jN193JHv$9XmKp#KiYWwa(hoq5n)_v9a7a z`McyMv?p}XX6=y44E;IvSU{slLxo0?N~gv7RPT31Xm}Ap@#gqt2m^K{LV#Usz217G^(JehbtT>+|A_TT>&J-1bQs=yPTOp4mcevauoZcL}?iN!ua%wiw~sQ4da)&IrvqLlx*F%3WC zVz&lA-Pn>Y60bs_bn#;G67f>;GI6Q6OuSsYLc9`~C@@C?a}+Q~1JetbV}LmpnBKtj zSu0*GUL#(MJAsOC-9Yv*VEO{n54cNkV_?a99GMmH;S9Y2Y~4h%m82p z0&^TNgMdLp>v&*J0A>gf%F|;o>8fk>aD4(cIG|lYas*=!u>P%p_nY12YAfslb%36TcUK5PuYZ!dHJ0e-(F&zlpz#e*l9P z^E_Zq1Li#P@{Pc(0Ol@WUIYe7-bskffM{AAPyN_4NOz_t;_0ert{D-PBsSZ7F!Vj3 ztG9%lnh}S#IDrU646~GQYMj_&%uh8!oi>}zg~_uvyG_DMJf|(khQ4PRFy+9YFIovq z44A6bHrb}wR7-bTTTHcNrUO$8OdZC}7y_}1)2_ucjNDcOx09%2>O1HF-{}}edjRg+ z?@)o1(QOCYI$}|P4YkUQMq4Lfs)_TK0mReR)rP=+I`*Fpok2_zGMedb!=wQX^3PVx zbhUX9h;JP*GaGIPriOz4*@6_G?|NWn5!AmVh&1fd9OTb-l{mJZku7Nww+{~X{)i#0%krirvS46n1#Tc z3d|y47Ox?@yX|DGp?DW#-sIX0T%aZR4;~5Agx%JZAW*#67RxG3YB9+DC??ennuE08 zN+?=Y&Y`)iw$qdKDOSc}hCq|;Oxszuvu)=9b2=~$zyLy_Ax@(0eA@*K;v@odCNO9H z2apocHNe@5D-;s(Zw8ELyWDmq;gBnUIlIwz6)>2i)xuj?hG1LHEU{g`2WUiK&K<5W zC8>Vtctk10NI~n{nzTl9XhtYk#ZL{a)`50 z+KzFF*1tAl7X=l(W7&#s-B%oOL}$YvSka?r=iVplzjf|?u73K`Sc1KHu$L5YLA|nm z24Wn{)9yk!3$H05msQ-0we-(eNFDvhmLDvCV2qZv=2~TIp7n5R7{R~?AlCN~Ybn;u zpJHvmn)!3kO%f{ZpS}&+Scn>bGCbdxttiI zSCPM#19Khzi&?Lzv2Gwc;$DKL>4{Tu6C*BJ=+7phdj9{0Jl87FK4WnowGm`!+hf37 z(P(=Dm@8Y_)1S5>S~S_D;*fE+=WNg0USK`|=4xQB1?HMK8k}u=Dh6CG>Z(^To}B=8 z(_)IYH`#th&1a(Gl2%jN-XqnNz+9hHPHFo%QBIi#n%D4-F|N;SU*XZ8?Q`1~wl9IX z37DIKX$kNVEyUO1ZEg*#y*}y@PnWg=RF^53^@*N0CWFp%Y$}IIIwiL7o+)wJ{d_xLo5FU z-O49>zIG64+(CfZ=~~u-eg}e<*YL004Akn4pn}ERx z#DlxHoM9#6{iPdxB8M z*+=66zkLiak2kvo$UedHjr~OXBuunno+Ky9sBE9l`raP>n_!a zU!&8qA80?-4n)`&p_nhRpJqSZ-T=%?nvA~;%ywXQ0Q1Uf`x*8#$qAeN9OBf!3d|cQ z<|yrNCVcw1<}rMF>Rau{yEsTP?w&8*K>VMjxVu}=#!ENs%Yb<;?r++!v|o)rs+HWa zVZR2L*W=!+{W^P-#d`}HiR40!7wMH6mt&AFhUqTf^MAsLHlO=Lv~a`?*M~ww)cQ}zsbG@m*O$| zl zzz!hp6P6z4151it*&*8Q%nl{B+bkB4egfOeB>?N~5IFz&e0+0-Li^8jGAf9@t#MpC00M{>#PM(iF7Jq^ZEV;(19@ znN*EIR;gU7kSe8^R3%N9W&kS#s{pG4+XmRSz~%v)k9q#mOsPhiCDlrG(rmyoN45ai z4#4&V_7Gr;fi3wj&GVN|l}^Vze`%4lSXv^T25cd)ZeWXmZPz3P6=h4eNQkGiLRu@`3hbf49=1Zd4U4kb!~cUt+0s1{-d{_L zvZW2c9zm?2)>mRnS|v8HJ^tP5(|xU+mLAaS6xbfxrY}87M0zW*-aS`DOV3I#n5m%` zQ4PHete>c%ezYQ5dIb~Tq*s9r5Ps3p?4&oO4_dD(K9WA>zSdL)7C@k?2m*^ZEoT|3 zqHjVuekpyMQ8>SozDMEw5!ev0VIrK7y{d?oev^K;d?Voz#=uhfISA-W6ed1%1bJ?fw>T6s)%+JXazHDpMRGplj`ss1(dz|Xzb#kl$8^KodoP;EFN~ubG%9 zvmNIEiV*Ri#P?upDuwyB( z<+KdgafKFbP0c4v)D9=rVjwv2wT3dQ1~x{OncnP;SRU-S$#Juz5!fnVaiJ6VE4XgW z%Y$tmtww^W$yCF~Y3L-5^}7B!iRzyNS0C(HPxaDC`gk88?2H~>ZF9YpR3EHwTaHH@ zj}rOb0_^NY$78@^2E*RU_g2T#3Hg2o*hHp-<2lDmgu|Y9yx@2d5G|6O3+%j=j+ars zcK|z|$Twa#`mdYzjyK7Kyantj@%`HIuH#c8-|sozcYJ`D)*m@OcAz)B0N91Vo(k+D zU>5_s1lZHiw0C^w_*@VjUpl^WUpt8djYVQ0lW0SRl;H85A21&UIgsLaT~<(rB=d=>y}o+8@EVI3Z#;&bu$+5IuFzJ!KG9m z>~jI{{;1q_9!6EerJ8DRdQlRbK4335RSjC@F0}=mA?Hz~E^~r2;*1K2ma`MsD}lWV zi*}owM>~62MmmoL_G)0SCt}=4^oDssZc@#z(R6<&ni);b0nUNWT~3?<#Kt)Z%{c_dM+4v{U~i6_bIxhbN)vOgd9|%E zKPNeB&7N*nTWdT$*EydMI1ktrEff#y>%Ry+A}4CLrUYrHJI^BPY*v5kJllDWfEgOV zt^#f}*5Bf>ijk-1ywG_m9-%uga$f92L0k>&8enf(>AcLj)QMa8T0p2hZWLh)bzkad zo!3})uXbMRT<*NidA;)n=Z(Ok8@(M6j+CFpB)sQbjW-ma zNp`2ExHkfeN91;3Uk3JmU|&kIAe^^4*IB-CYNNdm*xQL>xh-pV$axo0MRz;zao+31 z*6slIE@1Bl_MW&ZayB|Q;ZYS5#;$J|j(3SC7jMZqEvcjQdQIn6?c9sKmz;YglB+0o zlyjS|nG7V8epyyMCR3Q?7CE<39kapU$XA^2VM&7XRp)EY*PU44-*&#^d>7bF zz&-#h+9sQUeF)fxfqev6v_2kP>wKRan^{VnA3HyBeroCN{0!K~Fer_1&%izj>^5MZ zBiq#Tgr$lSumyT-wzys^$HudB#2ill67f2RR@Wj#C?W@TGX1(AX4~~6=x}Azv2?71 z=`(|C=U3NP6Nmt_R%$e)(Re*GKGQk96)p1!kNfAwitDPX5F6O6rTV0&YOvZh(~$bi zn~ONd*irh3NgdU|x*AeAs|!9B7dJY80`_q%*0bP2v=d=Xnw+~W50XE?K7ok|1Flv>Jw`-PUq`1C6d#Di&=K%972>xPFJEtUPC2< zyw2`0bO>^EvJS~PJg1wXL*54V6{2H za0k%{uMw1kOjwX6)=9cL$my%AgSSl8LC$fyI(UZ~D%lo6Cqq>x&CNruh7O*?b zXLNx+qljgqVYg(bV4KmGG^GpZlzwgq1>{_a3O47AoHKLI$~imdoSbuW&dWJJ2i=jc zfZYY`*T8-Q?6<&v2kiI2{s8QcYjf}dWTIemF3VY(v&_;x=L%qdG8F8u!2SvBUqr$F z{XYe(F$wM&d!b-+Zbk*0g8_!0se;XEBHX#kGLrlO_7`F$<6o#?4>MNrwwybNg1sGB zd=>40-&(0)@5$MqD_FEFP{ICARN_WW!T!;3QFEo5MJaaseXM`r_7)_oJ0nX7(p0}fAqgz=V3Ot)4 z&u?IyCg;tZw{qSFjss2r4ol0aA(ZosrqVc`2q4xBqn}zg2VvL|B!3=7VlcwTCer(v z{<6(ndUt8k%Slvv2W4d*nGuvE!n<@4<|JbYe$M%ai105tzvk@D`7P)7oIi5@%=s(l zZ{Tu(%LUE_oD7@-oC;hU;MxM0w>HZDTEkqH{b>|k5Kfo0N=hpv5AwE=A=t|<9hn-Q3bCKEGHRg5! z?x5C*aqi){-8C`JJ%Wfa*B*s1w^3^|=Y}-t%?%Ri^+Hy>oSPr|GiX-BgxyW4bKkJZd{} zpR8m#8AdJ1(aDWbA?|6cL~ZT@6yn^v+}XJ&=g!Hk&z+k)FL!?KDZqJv^8)7s&JSDw zxFB#L;KIN~*5)pxLc9cp_;eIv04{0>G48)8#C?bm<0;_(933>)_HG}}y_g8`CBPkN z2=P+Fn#(LB$sgd3!hM+hg(BQ&tm3t~*AW3;4qUIs-0Oimrj-JGbM8uAfLCY&d@PZ} zRhj_zP8Z-_4f2&nhwE~2<42z+7q@;C;C?N=i1Cve#PlQZE1JPwe|YJUCh~k=?gq=Q z+>I#D59B_WyBRnX!(qs-cvk|kta0Q+fg7F_@5p^P_Yoq_xsL&N0&vG^;tbsJy5@U= z%;J;44N3^`)4JRZPL;cO@+^%t3e}n-w@1yV*!(tX(EL^(gi?u5kp~i5rz5{L&aO3G>ov8Qy16iyeft!$6tY7rSnz%QMMY+?3 zNIsY~>aw~R7wh6&ybBGnDZotyt`xXwz?A`4zS?EOyF4YA!{v10hE)MvC2%p|s(_mf z+{r|B=}x$@2D%eIVoq#+b=|yrx-j~OS}`cjddhm}UzMhOMqqU5=W>sfZ&F(yqb*!Y z<7ST=_2x!oz<}$*fQk;%n@T&SWpp)Sh}TXx=r9{4eNyA1(XqJ%1+CtAz95C}#E6W^ zFK`taX~1ptM^ie~7Tk3ZF1V{baMK&czvAkM7bW0zW8Va^bDziV=$6~*?q3W-4|jFPtR`1C*Ac+Y1g>Uof)*`T)E9yEjCELN( z%Z0@*B(pq8PvUj8eTnF~1^|cqdOa1r`T9y6M^<7GaC76L=Q=?bJ-kC9ITYKno-`(P ztZRJytid$_xP>%L#6`{(T$5cWhNl9zBp$qXO>@g3xW#(~^=9;Xl5194 zBL>2o@ys08+yrIwfIHowYykqhxfbGuzSw;OaAyE_R)W0KT#(wb>kLF$a>a{lwQCJ<7Xf$i{=;)~-HFUB zUV`91XgaY7=;pdd19a=3S{N>fM`RD^rkDe|weQfe(;=O^v_JH)!@K<_;M^#T8?xX5 zKyO2(WZ-M1VbARu)6GUO-5R(CyN2l2;A(K%5$7#mwkYlIzf&&g-orq3JL>4Z$B!5} zYV4%RQ>In?_ky}1A{se2HZ*qvhc3(&oT!Q~Tf=l$ECfyOs6X6R=yYWwGTf z%Qcpbc%A2V3xP7;ZB?xYBdptSYs@;^y3l%_^#bcf2j6a|?4Tb1$=rd4hSK`G)zP z`JJ`0b`8$0kS$^lVh?7!vc1`{?09w}I|=Wny@b7-UBRxx+fScnzhrl@->~1|EsUMG zLlM#~!u8^g=Z5ML);L;1rT)9|H#fQAXJe<@;KG!vTV40NHo7*s9&kPA+U$DB^|0#^ z*B0QG0=Epf%K`5T;7S_Cx5Eewoxj3h9HawDQT|3 zJ7+5sK#F>^eerUuO08iG;Uac4dS?jZVdwyzWzh^T(tqkRXHHq|^w=y!A=S#EiPena zrWbzc1Q@T90T4hC3eHViGN%3w7fI4~z3F<(^|tFB=Pj=HT<-&i9{&x%-3T0}Y`g^A z&A>ITaee6e$n~-76W6D%ovzP-TLIih;Ft-H1>igZayNmj`8ch;%%j!UfR)No&7dVn z7cJfw*0Tx&0;$eN*7-u&d(DQYl!06<(=!&vSc30dzhU{P>wDJ^t{+`Lxqf#2;`-IK z8#pXM!yFT|Y1RU_4!GNayA!y(ZgKtY`os07>o3>eu76~Uj2R#I0QUfJj{^5Ja4!P4 z1Gv|LdmGbI62W#E@$t!JXkn1)^977_e*+g@{D(&ZQrpUuK>(E zPhDtl>r3Tfdz>fVXmb5!VwMs=?P0#U(O&Lu&1&eq6fbDGbv@Bn?jd_HTTAXK7t1BU z-3#1(z-_=}Z`mi~rNJ1&xgWSqWWz|$<^Fe)z2zt-d&@@xw~;1$%e_pJz5OACvyBAt zLJ_J!@uVj$z>6Ujiv%M;{@z+n{jap0aH1DPyO(FXD)a9dk7kUeJCo2_cI4E+{v=aMVQ z0As*CWejjeG6;%1Q?4TesgY;NwZJ_C+_S)KBLg{Eo}&%qIpAJs)j-mDuNjql9H|zS zkrxnx7XtUZLGWU_B1wn?QE&4FX^=4ylGH<-<2YNZhnR*X5o8?a$e8VoB7(-j%VZoa z&DDjZYxK*v8(m+TGFQuF1nV^u!)xSgwLV?}?ll@(k*_Chp=t7}(bml-jFm1SO)^FZ z%p~Mid7akB8^FC~^l=C21Gm~YjXv%+=_8$aH^>i=KJJ${%A0_D2e@~EdymZ3X89ql z545X3{Ex2r<76OD0QZ40kf+QB;y3P{Gt|j8V ziu|hln*6%_hWw`dmi)H-j{L6tp8UT2f&8KTk^HgziTtU&Q~pf;T>e7-QvOQbC4Vh{ zBY!J@Cx0*hApa=;B>yb`BL6DymVc9fm;aFel>d_dmj6*KidA70R^b$05fo9eDRxCt z9EwxPQF0ZRA}flbDs7auN}iIh6exv?TPaf7DF-R-m4lTIN=K!Wa){Dd>7sO14pk0Q z4p+JrvB1%*_QaMUFTIr=6qa3UBR{AJ?m3~Tp zWq>kJIZhd*3|5X;PEdv@LzQ95aAkxtQW>R;R>mk}m2t{=Wr8wMIZ>IUOjf2SQ zno_2eD-}wm5>u*_>B+LU~fzsywAUtvsVVt87!A zQ=V5|P+nACQeIZJD?5}|lvkD4l-HFvlsA>Pl(&_4ly{Z)l=qbnln;UX7`RV?L&x9? z;JyOxYv8^G?t9?y6y|5(eg*C~;Qj#aFW~+G-U>VmJP*7Gyd8K4;GMC&3wQj2${|fwX!2bdKU%>wZf)xZ71Rewt1Um>05OP3pfuMlU2828i3P5m! z&<=$5Aanqs69}C_=nBGNAanzvI|w~NC;_|{PVj>e1R)GU6ojKd=mo;DAoKyD9|!|L zI1YrtAe;cgP!NWLFcO5(AdCfJJYWHZFbRYyAe4eo20{f0F%YJMPz}ON5N3f;2g1o9 z)Ppb&gi}CR2*M%|mVj_N2mr#FAe;?&iI;Fb2p58IF$kA}uoQ&LLAVlxt3kLHgzG@K z0fd`CXar#;2&+I?19+j9unvUVLAVoyyFj=Hg!@3aAB0UHJP5)=AUp!XqaZvE!jm98 z1;R5RYy;tW5MBg4z!Y|X@G1zegYYH@Z-ekI2=9aNAqXFX@F@tNf$#+gUxDy72;YM6 zJqSO7@G}U%g76y%e}M282>*a+1(5}j2T=sk4x$6Z9HMcRkCji9PnDg@XUgZw7s{8) zSIREsYvmi|Tje|Dd*uh^N98BwXXO{=S7o>IoASHzhw`WLm-4srk7`k^DxlZFZCGpShcs> zNA0WjQ~Rp})Pd@8>L7Kndc1mqIz%0+4pWD#Bh-=VD0Q?tMjfk;Q^%_l)QRee>Lhit zIz^qTma5a#GPPW-P%G7#TBS}`XQ(v|78`YcCo7F~jg}PF0Qdg;~)ivrZ>RRnS(52~Bhht!AFN7OCqqv~VoIWDfMaf z8TDCpoBEvky!wLrqWY5hvbtT}p}wNNs=lVauD+qZslKJYt-hnatG=hcuYRC@sD7k= ztbU??s_s-jQ$JU~P`^~aQg^9etKX>Ks^6*Kt3Rkesz0eetG}qfs=GmSfmi@y2M`Yf zu>`~rh`m7U1L9y1M}jyO#K|C*f>;jXNg&pOI0wX2K|Bq_GeEom@K{4!3gR^&UJv3; zAg%y$6^OTkcrS?e148PH4}th7h);s}42aKx_#%kgL3|6uk3ifB;+G(P1LF4}{sbZt z^9P9kfQ<#405%B_-P@*uEe~w%!PXIMoxye}*t!9tKidLe3xn+_upI-ozF->&w&TGz z3~ZyoHV$kP!8RFem0+6zwwYk71>4DBn+vv6z;+tg&H&rlU^@?Nmx1kauw4zd>%ev+ z*c!ps1hzF`yA^DAfbA}@-3PW!VA~9~Ens^bY+J$hEZCk0+e={E0k+q{_7>RQ1=}ZJ z`y6b$!1fc^eg)e7;e+%sI zg8c)qe**T;!2T`Re*pW>VBZZ0>16*0BnBi2q+E~`klKP&2+~0ybpWX|NZmmy0VxF1 zksuudQXh~8gER!B;edcM(m0SNf;0uBGLT{*RfALmh~OgCgLDc=i$FRZq%%M|2c!!? zx)`L(K)M{Ht3g^0(v2Xk0EG3BZU<>SNcR9jYe<_w+6>YbkRAu=DUh~-^a4oRL3$0O zH$i#_r1wGk7^IybeF4%gkiG-NaFBil=?{?p28R_KJUDFNaDc-F4iy~v;3xve!QeOq z99;p?103DKQ34J>AQXV(NN^kjSlsUz0FJ@n7y?+P?-&J+vEY~pSXS>S1xE!qrl&;6 z)W2w;;geE%_+E!I;z0~@S$xexQqe*1OY3Q3eIgLohN0c;ES1Nbbdyye4zFJu8k;+( zx+1+hk#>id{Y^CKZZCpx+IUJvd9|b6`DHa3#^j zMitT$jd}vXU?dO>2l4JGUpS2@eA^!imQ_`z5S34f`X*bV@ZzOVz*kjSo<>wC==b@; z9{dYu%NO=VO=c=qRT=U4%d5gEM75(teOD@1nJ`RN{F#uBJ#kM*+WQZs^4R^=J4y#B zXG)iiuC%kCS`&&*5A>Fdj*lk2?IUR0zwA#jH_8suyDOpH?arE&qMooXTvZlJV<^~#cP#1;`@B(4D20^*l&IgcRRyt(M0vvjzbD`!%#8{m;Pr+5 zArq>+D6-MAR8 z(MZZ%9Yd-5yHqaUUyL6YyHP%JF-iu7-B0hC$4gp=n)KZA4w2}BR+44tWDxCyDV4`$ z-^pkk$8_8%w5rtDoD6*prG0VcePyM=O8V4JuQ7^hl6Lb^v@fAl-ZQvC`WZ;ZZF@Xz z*S1fKVpLFPn5fCL6RA`lxyMCG*MIS~-=ob_MmuuuYe#W~n_)y%w4>Z?WxFNgBcX9T z!H_3{5#Ti0j+9kI%6w&^RN0`bDnkhiijRv9TvQ_gr-Wh4{`;$4EFa43R$ zDH=$psw#x$l0RG?OVK#9DOKwJ+@TEPFnx!bN4vvRn(QW_1-0w-;0=(T08T;3k5-R2 zm|~^rb|HDa%p0vJOPPdIDOLH|?wgsHCb4e@!VzBtiAvu$$;t#P%0pEtM4e8FDm1s* zvr}OGur9tbgBClBc34y@AHPRMkgmlNs=+s;Z0?ZS>FKL~KJDS4Z0ETm67b=c8%_5L zeBr3aABm#Oc|&-KDjxns(&Vxt;)zyOAa|t9^Cgt1gZJk=w`3GvPCLZQVzcu?(BO|& zR(ib^X^J2e3Pk;$XxQtEph<-8Q^1=x3$dz-m?u)5{P$=R{BMRS+mihhBRCT(65>=e- zRB1$2`eME`2KPpY$sa}~j{l>Fk4&B(kswr6`9q;p=l?-Um8Uf}QD&W+#E=*l7@FBtH8qduSCM2dnKgsVbH zOJ3nODOE>jy-h?hgchm7T_CAPEw1MadU1pBMm*t&ClU%p{plMxMgsh?s`AQI$$pR0 zbWEwd$ExPh=`y5k6VvNz(|3uFX~VtuW40uQi(0b5KBryw-M4HyyF5Od4AqRAe_JbUcF4Z%9oRuiJSB}?+S06Hept5ai2iiBHxesPamX~GPq;^D zl}Dn1NGM$s;oBi!Wo3CZSrfITdByw)^DNgabg^b*nX__Rnq)OW>3c^ zZE4amKc+RR%&lR(VZ0c&_F+iGi>6u9hNA}qw4)E)a#D$+3C%oK42|AjHU^${&6}St zSTw7dpPZdy$Y3S4EvAZLYPO4=Ih>IcamS!LTHk2xrp+sPy`r)LPXdxmhqiu7)wI@F zpy|9F7a|>lB!iqrXeZ_SpQAAF=fOh;JpTxzvF{5<{OM7B^kXA2Uqv%j)r%5U*_!Zh z`p7i}T`@PEefrXNt4ihLOgJ@F1)8=ypmKUlFaKSb-aN&t<7br0L-yEwMx94}*9_}5 zgbv}PQhA0+ANy#~8)G(jCw+_~Y5z5?zyUC`9fu$SNqVc}XsfkZFSzMZWm|FyB9`y) zv$N%H&4)Gj3H-?4VUG`8`A8JG8=ZqRxvs1XRidJgr36pgPNPK4$(E?h=X<`WHyZW% zQFn(zL8LAkNcZ;f&@qIkdtpzCvY~)P{M>)zAgx6WbP}D8`K9s*6OK+5$`+(%558#I zI@-^IY?XXwqbw!EA{dQELw-}MEFAWghUP=!@cy0iQSB zB*XQOlt;_MsS9`}rK(~7FCZ=&rpS26>9{X|UK<|Qn5YVj+>v?LFzWwgq+jkKGKvR{!*$0X=LMI%vi zdW&gW9`u!h;dHG_rp{Lxs0gH}lGT)|OZL&UZYe{g?K;}YW!cQIVSy!OOJ%SK*VAs6 z{Rd_k&k@Xc-e2L3c~a7a+TKU0x?+E3r0LV0F81`YHvTH}&RRAh61<$4b00BNGtBoR zbY$0<$F#Guz!NkS&r06%o}hg#&t7w6k{&b%Fwqfr7fiW|pvCA7mwEq?Rjpd2`oT0OKX=hEPa+!&Vo)ya{30uZ9owgs*j#uwjStEx?=}P1?+Se`F z%UVUyQ{gX558B4liZR0D!;}L&|1?vhkupqzO5LS*QKD|mn)x&5f_kG7zZb*(Va%op zMbUgP%QJ{#3XZ=V1JTK9>3d4mZRX6E6+d9m#ymFg3+?QV?2;Pdxo_z!{s(P&eYTUk z&mLh#MU^*%c>_t4n`fa^-JLBTX7&j2Xa|`u5b&ewk9g6?@uc&iY2Z1Jr$pUbDxbK& zGytx6+-FZ)oIHv4xS>=YVAA8B_HXYteBsBKYL=0=0VPqmaO=^ z!)a%aWh;)EqjgCEnh;i+p!b#@pov9&-ZCtcNlA^(>q&`vqEtR13*j(kKs)}bS!i~a zpXZ}pY%P`dc#VNcpf}z{2Ae%EOxu4tJAv545p3SkwE1VVUAD|xG>T!OkS~Ja5uXOWV!yZ7iX{T;~+k-iH$PTs8ra))-8A18EO0WH+nvRg2^4ruZXyC(!0!YE7Iz zU5n90OV=kOXuI26v&W|sNq3ULmFX>yr7gdb?H=F4(O&D4$i0~IJN&Kh|g8qm}Xarx6hJ(?v)Xm^dO4V+2&vNf)I^iQ{ znCg3JkH2TBevF*d^!I|F#P7qT8BFZQniQp3CnZ$%W_q5w3_O|rR=Xct4zIl)@E6ztjBZv;^o&7^QE6jFsGv`$YNm>T66fgk` zGZrGK&C|3CW}TErBIU7^B!zsA5>=FS=}@aGOv9L1ViLh3i4zr3tk_Rh^Z9m4(?RB5 zWwxtAOjncWO@1!z?qKuUlRfd5VQM64yhN3cxjB{JhIZMpR9=`>U2jDM^PVaL;i}|y z)>Ef^=wM=<7NQG<%`ybBSle5MwE-y>Wqu(g=n!+;J=-bI2sx17o_5#;`OXmPKC@@n zR99#>Hq@8s_X?EFo?SxjSi!p#>Pz~^=ErL4X2<4~jHoG_TU9q_R((nJh#7UYvEieO zXVZ_8o*Lc%gro=RD`%F>tDjR+U0YEzuQFCLt8DI!6!S3u5K7ab=8jq_BN)q`*kO|x zASJ|wwy&b;KdrSt;KEDU;;Ry5bWVxsr>KpRCw6`ASiRpXQn=iwhg=ineS)JFl zoJE#@6m8t|@0daQDrFd0Z`!wS|L+Y6^Z`s>^ZM~X)-(sF4D&z3DXEnC11M2}eKxmE zH^ltoY5SpU2c0>liDmIaQxp+|rcIXE-G)a=;9;@&HBx zQVp~GF_fw!v!x1mT0EC0XYxsgfxcm2^$;=zI%OD<^kbezIBHVkOQ0aMQk$g6R{lgv z)zPi7%+eKR3nzp5Q)$!394Owva5sXQq~sgsS5TsQXIB}PNvqAE&GyZ9aWl&SmXRCz zH!a2XLy=gh9P_Y~7k3t=s(-dr?XzcwRa@w!q*679QZ+Ek(jaq9bP8>C(1GJN#CybA zn&bsoOo=*v|C+(NJD%pA#D__^g`O* zh-?y~6UH3srk0X>@-L&^jLMc{GD}Ayl!*2_B0_jgN@@t0S=1K`q`2?-S5l(JWVM7D zW3>6pX}jaH1>?*N`jdYXZF_>b*_g^61~VkqDTysIW_ac|(LPQ*V5+POG!i^eYzf^?N{z6g=NT92GjpB+FjMZ<96s#j|_c3Mf;v{-~^+* z!dsS#x0?SPrRt>CTq~B~{KQX5neFMz`!a34CQA##oM~R8t=49{A7nl-Y30#DMXVwi zO?BYjrbNx&pW)365$*gBXlHY>jU;A{9f$ph9EgR+UIJ8y{#q!#K!_ZaV&NoWBqy6t zpHiCUX3O81SEaS+o?%Sh^QRhiUs9^(m&&pIrH_;LL8_NCi2S#-^9B2pCon~9UU@nM zZ~jlTvr|juihpIK)wLO*lk?C%)0R)mmZ?b8yDVIZIlC?R;sKHt zfJddt<5rv1lxYs4mH@UcgeS8d~Fbk~)^zVh--*(AsGxv0d*1$( z!VGcXf`e#h7i7CPW!B7C?vIFdK1^aka8pDw_L(3>5!GB4_J&ie--1q*sEf?4-_#{+ zKCwyKZck?|1&7j(E@@4W%A}(dUnAwfprAW#`m$_=1CMbc=oW-bz@rmUxgcJ*f#{Vf z6<@k0KA~RDf%f$sk$Ots#;Jat;>-P zghLhORjG4zB&F&qb6BZVE==f3Afe@fNz*0|rz zr6VC4Nq5hrE4#t8kL$CtxHHst6bz%S-nj2>pqcGbFq*c1b8FJ)o6k%VW3&{lDwse! zS&?OArG-rSf+@7^rmQWX45ofTIc<4$w#U~PSND`v610&t3rIV@jv$to2SFFoPpYEc zO2lIbL{m4D>6D;bvX!lUS1u7xWkqTMMnMgw>elQ^6_R+!+?FpmnRaknw!nlL&TGCMmd-cD1ooo@z|; zeh4I{WJ{!PA6L*`A29DVb&pPGMuU@*#3`l246=1C?RE3Npi&YSBx!Js2)UE22X)&H z9iMO@R#uhj<=sfBdU!v@CJnaAFbOMZcU!XFLNc5R72HBwek?1S0LeDpvu~%ZK9Qx2 zWjGrvxSO`Sb)PB?S}Fl9xSw|M^gbG5E!=TYu$lJpY-^^+keeh8oXHe>l(zd^cA6a} zGtmkDlImn_rH#MP8Xim&|CS7-ZM5Z=vW-|~2J*r*3dB7?(3@xgQ4$d_FdJN?+{bL!^JuEl#ydy&sejL#3PDXX16 zuWWj(m!63_x^CW_ida9qGIa?3qF8I~(0bBlPqhDfRnI25sP%^J==Bt%)N9|)Et@kv zHn-P;NT@Uv=!r?KHL;%6b@kC=|Dkj1>S`)xlvUT(_o}FyRjhqiTsgNm{&}(1fB#rj z*}R&$NrxK+yXiX_ERV>`JNnDNAE8gAHDI1a0@1f8%tM9KN@3 z7f@jd?RFgEfW8+9N z0~beWH-EN98JnmlV;8{wQY`SoV`#&FXUo5tCzWJod+>&CpD*h1;_cjlKrr2Qz}L$n zm7!p&HtwfWC0h0|KCAnD8Rm2l?S#pepE6raLA*50hj%^&qX=V+SK=VxPnw~KZ~MKm z%G79Q;ZRBxm(9qHRX(nc#OVxiO0yLUN6}7%Y!|(SkZS7=e!Ug3vhvjHDhkI_s%+U( z)q*Nb)Fu1zx=OrA!UU`oU-wmoDpK!@E1XQJlCoB%rl^F4Wwc%A{yZffhsB8)*whTu zWGz)iJInny^3V*QBw5TXIhI_DtFXGT*5Yj{JgIPIVNKyI5M>Y*5LFP{G!@nr&bD+f zoC9K85c3+21~I>(OQ~FlF2Jm^ISc#ORm_`(m0@G5XT|F0md%=7DtBr2)wt^V>hhXc zzZqq7$|}(Q9fh`gY|5bO%2*#P2CT227ppIoyEc1ibgTyFC{~&JR_8tsFTTrr+pzKp zi#$C#B3{Fi@rwff9vwYPcF>V5E(9`?C55LIo?h4hVj+lb5Q{);*Hm~$;h8v+vq3xv z#P(z)2Or3hJmfqtdPJAylNNbHI2J?-^({FOe~yNGbo6=S`BH_d?3fN`K3 zXKe&{YgK#%Zx+6naRl!det;wR2*hJRJeG{0cSD!XeK!Aif89~9m1|7u3q?aqCL)st zdvx@N;*EY@_%&(ti^4Apzbf1XVqXyZf!H6!0ZoP96n=}1eh=b65RW5`4myxr`jG3s zgF3DJY4W0|*GsfUBoZVl#EV`=GN=CGwvYk+S@>7s--Z8xcsz(FfH(xip-pb9o3V6v zb07`_aX1;!hyyvGhZhx>&HcD~@*=#l8i#;ogMM5fyc#2dD`YH?JJ;QYjK@sHyW6_+ za6APdjskHs8P6E9>JK@WcG&3@b=8EiK~zlvKeic{W_Nq{A=sw-V0Q<1M>i_iaUhNd zaRP`Fo7|n%2Nez@n(Grs&%8;jo`i0>5c5_cfu5Q6TIK7=VCPNhRY zVff*m^|AIB=uL%van0cF& z`+7T_VPwSkVx8a~L8uzy9_k+E9u8s!h?OA5K&)zVk93d1#To<{@BX!l_(KruL9FXzi`o=xgU4er!HIv+x?idaRM=}${8Zww!g!9l`VXCkHUgtc_ z%)?zISvK9X+;cLvSnr;TEzSqA4#e4{#gl1^;+l#Tnl6k+!^8jzC3Es(H!*Id5{W z!Y0>%cshs;q)DK2&IwKD>P_OsiDc5zEJ#p!yZdf3>36vAbgy^c1>%_?o&_RK^f^uL zd))V8lN&%h7sT^Oljk4ENq>0pjG_mZ|Ddr?Fc?YS|J+;LPvTe{zdCk1eslZ|Yy+?W z>>0qG+2r2pehSC(42Tzkco7-P#rpIt`)KPA_%(aq>M+XpQ3i#|a^P*ZmQh>-XI6yFYM$2;wpj(JsCM#4DTJAG<%% zo4N|bt7%gQ&R5y|(N`B=_)(cQ1iUFIF@$g3KanAr%IMGTUvP?dgLo~7%gGR~(`I9v zWBO5@ztK4mZ+FKj#v8wIi%c~7cM*$?nkwxgu87y?luWZ+q!zW&Te}&=#zbo$eRb*RcdPW#1`}IAQBhHcOrtI8Sky^xY9)wG ziKd=?;f!6IZq-MNmzTt;I=rYSHdWNE=!l~3Md&G@6M?dN3y5o*ii(R$EZvLH2EP@= zb)>1=4yc@Uzw61dD|(}H^Pzf1N63Rq<_o4+W<^I8^(MnPx~NytF-6CMcn64gg18>U zyPAsn6!q0-=57%0p)+#;S^4ED%h-t*=srX+8efp1MWb*8rnXqo=%O+D2<`)MLws$~ z7VGuGgkpVd@jk_PQWh!N)*+oV54^)vn(!UHZH}VR(k1 zX;&0|e;__}R~9YDM$N_Ix}xi`(HlYB3gT0w(WkXWyBGaE_CD{FMR-@Z4|iW*z!TNO z50Pa1q^PNAZN^2pwP>9_mCu5>EomwrnSL}g3tJ9E!z4(9m#D=Fy}RgsY|dOqZ7kZP zH}^bTGO3r}@A}oI%oH&jF|)E` z>6}z0NrRZG>MBW_q>FPVv5;^a5>622&xE;-g-@F7Z~7=MtFvjIguEeA%t%|4 zZcD0FUR#rDC)GjRBuF>~3BM5TX}PU+UM=iB6(dYOvc3MS5V=jb%>)y1RnkFt}YLKEDui5D{013 zvNlg@jgYD<#iTY#ZDm=1hlH!q#$Dpt=YRI7)Hq*uL>(qQl9W^KXmm^Jj*Y`r=ru^V zPDp>sqmj6OR-j+!QCKO4eJ1*{P#FerHx-)hsL**yWLH#mcT#>5+Lffipos%bCB)TK zmW7^vcG8^nvd|dDkaTyca8uIoq%jDpKEzKZjg`dK#Dk`4RP1B%3m<(_S!?69O<|Ro z^js3z7Ik({OhVg|GzByXpwUqKwA4N{#vN-c$dcNJQquwqPdX-lB?9_l64@5@k$yD^ zZA;P{pkYA65-6vP^j6#8k7S{}NI$fJu}jpX;v_OEsxv!jVGdN?R z@4NSBVs%|FO&7duSu$66U25!nS&_7w8mqRQE~Pp?JIW}zf0PWJ%ij$0gaJ*1~WWT=;Gc79n)lU8Hg&8 zLrKRGNqrWaNcvgsV>4({BYnJn^z`hmBUfQq&rjY3U2z-f=Cetc5KW!_my>>z(X616 z@*80dxAe&!`(%36&5C#gNq-t>0i^0c8sZJ|0!Whq8e61cYcH=_g)n!x&Lw-18FykDB>tyIc zaB7irLsvr%wzE5E?gLFdYG-|^ouk}u4*Y_Z$(fCf?1YOx^cy1HrlF6af4Q;`FbqV@ zL7-^>nudhgNS3`ME8Z}JJZd~Ql-B9kM2z!eU2CZU8fF+ptyLZJ(S|XIJQg$$fTjr{ zHRa^4=IC+YTAsX^ncggUq;RQrgHOYnvhKYzc88pp6)0_}n5F!RqXFl@zPz90J zDp?_lp+@Y9VW#2ba$EU|;Z?+Z9W*UL(~2-#%dLEO=b(NA+~At8QvyH z)nO_zEJVmfplJ)5b_Cg8f*j@U)_Tzck7DT2OHLBy1Rh3%A1c+M%MInORt&2R@@hrX z5j33;H{yM**b$n85fzMPqS$!3K!o%g4YXiUO)(g@7~}}U(z-vB%Z;if$0sXBr9Wr}M4^tw zf8hWAh)mZ&c0`*bv&m#sR9lsNTe4zQ@WARJg-g8=(5fAtb zcUQ8Qd>29`XC>E7&Q87)G{ZqN0yHB*GpacG?&N!9sL`Mqqd@(OhV#P68Bgw3{CF+^*-SACHzavD4O&6+(BxssPk?4TXkGx#1kg+@P9Bjw5+%kFn*^H4 zB=MAgQ=P1LzvTV$^E7mIrB#5>E4jMaV(cOy`MG3i8Km0L6O(ZnM0B18n&~mUv;LB` zo3#tt6P^thiNLL6r+#sCQ75^D zAu-~ZCvQ(CbER6tl)NVy%@qyET+qymK&|&$9~zzUq6CGtHxVP6{C)C4gi_6%Bp*sX zEJMu)&4LKj+HEVwjB2r1agZYkj^vZcXUgrav&rXVq_;s+5=FYWp?BYcIN7(!iuTl% zIMVfMYYY6VupWchJYU=4GmE+VCV zN@GM*9iEg2Qkq~Z9|X-h(5$CcZjgt^@M31|!-y6NkarkDT}gE{e#*2*)MiSnl=kIz zYKN4Lax*^y&Bu{uuDz%Wjrp^k#Gsfurj)KJJrPOOHB9N1k}D%^0?p+w3Og#L zkc3rJSSgV56h>nULGw9izMv+CGa;WZyr%Wa!wHQsxl$5Emu-iei zBP#4p^KGwc($w6bl$TOoLo`+2CFS*$H)OP3pxGToI~KqAlYv)cHHBwdMH)6gWg#_8 zt*$8Los>n`bxT0A2Q+)B>%Nw{j-B-6-uz)WkG+0r9%BdrEf$ThQ#IHYx=N?|wgc!d-G3aic?|jUT6cicqR6vXswKw#wq}2hI0UaW4tg z0uO#K4Fkp5!^5yMWiKM7>`K|4@>L2-^&@BwfaWLA94t=xI^`P~=@4iRD@gyIr?yVN z`1Q{YU63TeV*Q8&hf*jIr9NIKQZNveatbs@L3518>$p5#)4Bv-&X&71fTbZ=eCx|X zp8G-(6O(c-<#M@Q{9DQuguM!ypFwkyU{4Y3+DpQ$oE29UMq%iL%2+6t5CJvD8LJ|w zv68W}v5FC+xu-$%D`?Ju=4`RCnlT~HY1D$|9B9rH=!JjR#hN*xsrWu|S|n!{@2*C9 z{Jv6oL1PVLZR#YodO2eqW1`HSOQ4bB|8m&w{EFq~dOs+AA6*t1%|^;`7*mZFqt%!O znk%6B9W>a-e-s-tjF{svV)X4AXee5*!D#)zN5u8+&3BJ@Z4z}2#Yxa?Q@|W=bHILz z%ywhYn2qwNt{;qd8t+1R?g4EaXempem9qrSWmgT){Qf8w6;Z4Rts+qfr+q2h2F50c ztEz#GO^pv?mk$PQJZPmvffB)e_dv7Xs>xlB+1T*RF}5~#KqytEY3yk1B#oXn0ko7T zhy*rP-qGfO428RhBTzZUUgfqb*VtRK3tAm$8G=&m!m=vkt8SE`yfLjBV5C|&RgGjE zV#HcFbQ854X!%I1NHcuhB2m%|zU+u^XB=fL*H{_Bh{g(Y7Fs=Mt0Ph*XR)pBcP->b zVKO`--*_WgD^>5`IK_z8$~YagH9$)l1#L~z2J1(^{-yaKCBQMWhcv;NL)j!etCZ^e zqsX(3uOqT*Jk|Jy@l9FWTA-~R5f|%;#w6DBNsWxHCo(QDzEf@^7a130BbS0U5ww)6 z&|iq!jDse zBC3>pjo1O>SH^D%Q_Z0;?lXQTcVs$fGol^2)2!b%c3RY-h{k-42aObtQjK63j~g)@ zMJ%*~))7U!FmmeZ7k5cw`(sv)#xq7L^D>?_o->{|UI48Nw3(oFgVs}Qykx{OFM{%d z)~7)Io0{$O+t)O|q?BS}mL}qHm@1j7p%AJQz?5Lp#5qkky91yN(ujo=Ze2OiHIw4i zIK6QKWGf|sw0ejPn@Mk~iNLB#$aIIPmfSm8psgG2=wtD#Orshjus7r*Hy`&chM8qb zF;T+9WHgyfW)m{#PSD;3T5Quj#U`r>6CTvr_kvd13xF2sU+nClxys3j+vRDJ6%C%5 zyrxjOT`iiju&c8{TOYLdQ&%^TyLwj1{A~kdC|thdjFa}rnV-z)=S$2v`r&OsIVrFp3qQgUaZ10 zbv8-0UTP~mQ+E^AdYO8Gwi#$Cv7nU_3+sf%1IOOc1#M(qiuU04c#0*5@I=lSooec5 z$|qsfbe3t5>2bsz0@{|KrR0KEN-nHppVEd7zl5+ijWBS1KA%d<2 zZFkU8!a|D)3p7xp+>LS$;CTB)O7`FuR2py0`=jcQj?2d;S`L{$F>Nw!Hf;fIE@*p$ zwhw6g7MngZ$;%;a9%#dX{(o=MuRT4cWu2ZK{=SGY?J?~`3DhUmcc%UF$n^(pOn%|e zsj>O3WmhZ^Q)g>BWTN$uI@Af1oS4_OT%-GOQwr+&Y^+gMjK1klGv!cBf0^UVmCThvI~26TK>Gw}hZmdU&6qSD?;688b9M6_<@JuambtdvJ7Ylm zB+8?>&xcM;GwEpMfLrXbce0`jakJ5EMKsm*f;r8cj=f_8Z6RoZdgm!=Za9~H-ef9^ zb`Vq6WCuf1y%Js^R%$P-*=-IWvTEeZ95jaz8O`f6pnaB*pCe>U*G=Xx%G*^i87i%0 zy|f`C;_#dAF-w(SYTlN)fmz-gql5|1aCxIp!`#eF+k@1FZ*9iy zLFRU#odDX2)Wk`$@QFXXxR8^z4F);UA4CT`c6c5!Q~8&wKWOe|#_})n1g3y?YHSOC z8uv;q2{ShPXYOkrfKaM>)jZIgFGJx1U`7<`Sp4dm=6AZ_irJfu9f#Z4BQB45n7P~x zy?L}*&d_UL1nn$@3uoxx-Tz#l`jXnfyJaK&_KbOaxkLMcc>?y^B+$+VE#(iiQvLvk zc1C=&t~d&KKP^!^NQyf2x6z$po=ttHmOC-OY<@)+_%+a$N+O)-U&BJ)o}UuCxc?*C z%p&uGa^c=Gzb(VP3EF590ZVee*)(dnBydb6wt1=fJwj6R7|idRSE9hHK|2?;lta)K z%UoWeFTSlQHcZ50Lddk^;sMuTb)`CwCKOtl_gJ<4k-i*kff_4FD-y-CWiA zQ&Y{UsfcL>?MI-c%z^e3iOlS@yrhIXFkSb zahbLmv|9-EQ)yT)2`_eQmM^z7+beC{3X$E5IQ^;FsrOP-)#CoC_odcD(ECBV6|~z3 z^m775C-uQE-mEW!dNB-+n{7!GM);T7B(()KR4sUz`cNun*i&1B_Dj&xE&%NgsiEiR z554w@Q&xirNy{80UUco0+A+1eSR12~ADur_M@!Nk;n-vo_`J1nzh*4zD?V!KdPQT7R=&N#XOUsfr`EKXfsZttx~eNS%TQP5JTUuofm5hI`P zc`n*}QKutyP3lHOOI@3~E_HqC2GE`W?a!b+3EES|sUM|&jIG=R+Fw9>np*kmzpAC& zpSAg-SoRpoIu6%7s6`e`qN_BTX})cWoC zrB6S3Dxp6cVf>X;O0uiV_-866*~zK<9kemK11^jh*Y)MAQ3gckkENQ0Au-kB1Qyo9 z#W^hkXs>~mat>N)hX7W}H_$&cQ*Icox^OY-4aEi@EHy1sl3h(&OOgeX>=<>{#eq)R z9>B@ufA@zWCGX14PIRA!#bU8x^DI_NnkC(m0lF%niw9j*&{Zq8*ewoho(psdpriP_ z4h#AJy=CosuX$E8r>qFFBwddOYQ^Y1i)f*OFjWWNa<>Hw!YucJjsYD@qR5@%T(?i=}ky#feuR_OJ5YX zALwd=?hX>Smc)Tk?vA*gK?NAflClL}%C}J|4%)W}?jXxh#8p+SmSL7B5O)OV>VPhh zaFb+iO+S0#c_kQ+CImsHnu|arv}+k_d8XVReAe=u+=I!WONs5lgO4w{SDFQ}3nj}W z%XEZNbtx?~EHklvvp{D8otfH~O6^HcgPFqOyVg{$XGnMjJg{S zFN-DYQVT6%R1=++_bs@Dv8)CiPHZ~)q?;uQ+rcR=tPF9%9}jL@Dc_PbyN2 zq-redEwqHOY_M#!d}R3;bY9T;K<5WtpxCm>f=d`wj=CV|{{6Fv)^+=Cp1*lLln5_` z$CU$C+C@0N)3O((P~E#|`P%Xg_Rl`hWr2H<2KOVU2_Sb9s9hFgBMTp(Cg<#W+; zN$zpfY_u~VqI}NYb^fTDowQuDR-*B{Zu!&lmlZ9<{h(_Ax`v=@RBWwmtrF+7qV8`D zx(BF{|LT!L>qC}K4Njb~adR;%2{EkO5Bw2u#+f@ZH5sfF3BPOF#D)XrS6`mBEJW1JwZ zK-Zf3xQ(KrTDMAvuQsw?vXP*D4hqt3_a}k6CeDWE}{)uAs{y%x=<55|$5m z=gGNp2U3w5>cl{4wiH-tS)>})v5vIjvdB6HbUi@VlR$gPpzpWvwdOFagV}XjG2kX3 z3hQLyRYfrq-}R&AkVoo#(t9w9XFk0EAwp~O!y{nYU93nl9u>wH2|^AoHKtZ!lKN`sl~JEH?_1Hz5Q`oM-QZ{&51smP z#$}x(H*SH6MDMKYtREwks?%Zp#JUL^xdn6upc_h!942?+^y_sql^xhY%pH=`fqFg= z1^vRhizHXo54){jA?RMv4F}x_0v#!XzPqDkzFg%&)ks+Q9P4*m_gl*iky#H~F+^rP z0=m(l8$*as5+eE@moj^`>5Q9`5DAOFBzGQn0mKsMq?Hyg>XZAd6&Ekmk%ge6xV$ph z$Kn?qsQSs$th9Jhm-$aCE?&|qf$nM0QR%<#Sy|>~8qcbp5^wNu7LqxZ z-DwGFQf;tWFQ##6SR0(C2i-W(Jx_?^m0sNN=kAcqMASMRMq!{Q5|T>0BP|J=s2Z_M zGo&TU(oO&!1>zNHcXhe^?47OtPVF>n8dU|W>Yg-58de2UzfA_+lqk~q6-x}&zmoec zrkFL&pO!^XY8o-EZd$esH4Svrqfm!VjqZ>qA4L?a5!33WHKHL4s!EmgN}mm5htL= zhP^>GCm^j$S`R`~i$kaNOzVX$L~Zpd=w721zAm-UYdtk)h+kG%At`)^M_;3Eh-ibf z{%KSWtjfH!!D(0yoHi76Z-R~j@VX*}c}=*S?()N8=W>L z?a8#UpqmG}V$h){zMwb_(w;)ZXF&HB=-wv8l7G`H$FST}D|RS5CU9pC4IpjrkIFJR zZAQ7hGc)Z)sW4ZEY+XbHs1)WdOYrY*EYC|UURP{)+G}aFR#B(d+%#OP5b2kIZYd$j zzCE6Gc;afLqAtp+g=tF>N>%Hoy_>d7hFS(Xiq0!gqu+Y8XY(U64{(Egq*bfa)*+N? zdOU4?+6EcwJw4;=|k+W8jK0B92<-h7P=C?E~|HX~dx{p9dvH8fx z>0Lign4(O>Xh~AqU+M7#rPjRks_E6@oaq|SZ2}#Y_3O6CI>(*UeNs(aNO~z4fagWg zLK64;;$I?;VmhB*194SjD(SbU*F@Y}p!*DTTM2iYgzH?<<531TIAMJY#Z+)SXh%nY zDzsuIM0#>Mx!kJ4r(4p|<)#qU7ohu+gb#msVmRux?!`#EcBWASxau5i5Gr^oI~vbq$@~GQE||@NYo(E#k`4 zZe6z@u3vn3C=Qm7g0pzZP-Nz%cTA^rgz9`r?~*R3BXr+^Zhr&~efg<(*_7hWn9cI( zx#{G0t18^|$I{X7Ce`u-=zfeqq2hb`p8Gmu)EKwJ&|pWaI@5=w52x-^<>ZL;k=T8s zLH85r4pR3WQaIV=hy5ieCLV)~!$Q;{PG*x|7a8uS(xr@qnnRNQd^%<%XjY>ib`)_V z%f79X4t0`LY2E0?v-GLyQZ_;rX;wOBBk*V$-Eq*JAbG<_%X~I(RU26;N6UcI-$5I~rA(Z-*Tb{lGyX}3@od(^n1a(H9 za?7_`vr@3z{B&@TFH2%GMk$bjk+639nsiDYL(Y@8MQO&NL|NN112H1 z>l**wajk@e+grnOW|%V42uaP$%t+73kmO{v!04iK?lj-AXyGtP&e)?fGu#=0a^(zW zgiy{bU{F+!Jmz^HcMy}xWQLqk=>8eX>rbzSd$+#aeRhLN_4Kf%zwf4%0 z9*cjw`OHj7&=?;hqge*k0;y{DjMf=g3zX3gm>R&`POXzl?boqW^xL~H!8w*qSqDtw zlV%U#7B*ZHM_Twu2Gs+p?kmXXmVxy^89jli1x#(itwU0;)34k7TAQ!YTn42=MqEi` zW8f9H#F77q#a7Ck%gDkn-<}ER=PdFFV$OPOr4Z}q^;vJCQ@6~ zre4ORjLGsqS%9%3Yz=5H4KH zv`jPIy8GW|U+slv`#8CZ^;bw?AHdS`O9tV^)kA%Q99XlxoRa z#;T0f*moZS;{t|a?2M#Ou`cR`GrNXUjGaO$7`rF0qb_bOkI~Z^8#6W|uKJj4$@mm; zw*un@hD!MvzdR<>FIKCSC`%i_nkWi|AZBz^SjLWwJ=BG2wm4&N#@7h?Eige~LIf&G zprhP9QqFs2_V}gntC)obCE|Ew{Frf=Fjck1k&L5qU)BXC8!?qp-f6DUdDj}XHEG5# z8RyDv;rWaU*uqP|+yx9p&lxFtj&-{^DJk#CV$-6F@*vXbvXSBb!xo32sv5&q$yPbe zX^RKuK42(z&eSKd*B-07q0aiTNW_3#|3VQ;N`%y-)hyd`ZM|^_`U2Ad zn2t0AounafF8k8(tQ<)~c2MmRnueJ9VH;>0f|#magRQ_eRBquTz{FI_A3AktXA?9s zct{W}!@SXhoNc3RKwYOc+iXwSo|e(N0Mj)p?@n{=>Tk(v0i(;2?3e8Y+Z2RS=i5}< zG!%9QFx`OZPQvz(`L?*;-Z$w?kAQR%ln?8GDH4uHRK_mqY%km1Ad%HFAGSAba}aqh zFuj1uCFI`HeEXak*kjb7==fsfg!EyQGjH1#6Q)|NnQe(}DPk@I21h54F#AcEmxLDG zJBt#g9}h*A^dT)pBW{>&mF>eg-(1^j+Xuk(2WH@0+Zx+iU@d*XFG4ZV7q9$WV>wp&3481yX~s&58E}{b=#k| zzwB}LO7_b3D)xALRXaMoBY+tN%ot$MJwQL^X<(iOW*ji%ftdizBw(fhGYuFd`-{N5 z1kB68yb8?gz`O}e5is+BnGeicz?1;<4ls*>SqcntZ3Qsz1G5Sk%U;`F$DU|UvK#El_7uC(ZnB&0sdkIqYEQGL z+cWGoyWQ>pW&<#rf%zPmUBK)GW*;!vk3Ru(1eg=RoC4+yFc*Ni49p+E`~_@PV70(< zz*YzLc3^7*YXH^=Y$~v6z}kV$1l9*^5ZJoF-UaM^z%~H(fl?H=E0-uTBXjf}{)cyc zw#v^NSWxEPF}rd*A~X8`k^ql_3o?a zj%3O;hs<;TGh*E6szr2a4R=l=-%_uECo416FzhShERb{~}6bYQRC;pGrO zxsi~WnxlV2<&`(vZRKBjX@5+<1Cg2jKcifW+=9V*JaLF zSrxg2w*2w?4|g`(M=GzGT@m-_hOfE7FpX7S^2+~qKD8ll?CR6|qAv`{_q3KXTyvGzy`7_P_#fpf*I4z>8<<;5)|K5)Eck}E&*nhMi0A?dF9|806yxQ;B57`f6 zlnln~tM?8j)y+U+H8^)ue7WG?Kd>{!@0&;E=3 zwEb6LwgB@fFrQI-&)M=R_bFbU4NzL7kEuQD#9fAX6+q;4J3Ya~GR*X~P5J@pb38Hgx`|5IRFyM6i>#z)VM3aUuwskBh7&| zie)I^a5$U}Ob&eq%zj|Lrx;mwmMDeXu$Dr)ng^3j+42x|Vhdgh%j|cEDl+2`)pg)? z%E+i6fjJ-z?=o<9MR&u6n(z1^8VC0 zPhiJq6D#l#A!G%1TWs#kQRu+^P&Cs@Q5S`L&VhG#;xJwW=8`gu4;H-1OGz2p^%ic| zM8`C27j?N~iXDT5)aQU5#%wHhOn1z{c418O3NTbJ!C<{amnM0=a_gf;9WrES?hqP* z`H*Tkgtf~i&m%JmUokX*Mr(WQQqjMXu zH43eC4Y+(AN|ppN&XYC!>e(>JDaTo)rfO!?an5lbsd*9Dn!wV=L{{3Exc0($OGaG0 zJOUxyHi&$}OT}=0N0ru9hhm#b%^1gD&NwGhs1C4+z~XL1oN7{@f(8T4g=^2%v#6kb zbx8+`(fiz;2~OOWTSiCJe#gliL(r_bpJHXYauU~Pp~+Qva!AMtWIJcA;L8(m46d? zRgu#9ptG5?Ij}hIJ-~Vi4pX0~*r|1BRw?C>#bLNMPR#zQID*bj&WEK= zL!ko%hgf*otn8Z2f97J%BVmNDPOO11qk)}0oxPm7z=nVofz6`13K+8Kapz#EIoZJ8si^a|b6>t+Qs-366>iNF&e1q$R8z9fG0rFB zoX$dEak|_?bLL(`T0gqD%Wq?PN_#ZP5X!f8`o-9Mtn)eN3v#x<;PlGS=fmSNA?W2aE22%B$ZKK&e_hFov#4f5ZFe*Hl`lW4p1e6 zqzo^te8+VU?o~zS1IGbVv5^uX=NzZBD@kozymNu`Eflx}*e1X>C4nE5x{e*U;e*Rh z;5aK7tY=3MSv;e5~eKHtE(%DLM4f%8M>8s}Q) zI_G+>gL5O7=KR?C3Ae+!*}28}Df^alt8<(4b6}B>4*}Z>*fzkn1GWRiBR@L<`!KMb zf$aip4zS&U?FnoyRQVa$K2YTcVDo@|4B}CR4*)hF*vFyDDqx2II~3R_s5xIcw>x(@ zcRF`DcRRmw?s4vQe(n6m`K@!G^E>B$=l9MZoIg4bIDc{;bRKdZb{=sabslpbcb))t z1h7+qoe%5>!0rI{FtEP?rv=UgToAYify)7IFmU65dlk4Pzwuj#;o+`+@T zmlGp#o>$(~QhlZP58hOf_>c29<#nxd^!MM4V4k+V`UCs)FB>y+UQ>|U=IC4B406os z`i3D1S0&{|?Q`_4{>vBLC^g}#ro5+P1+rmk96(l-lOIi}BSTkCjH!5V3CjB($}JiBgJfFHeCU^yaHCQRy5#9+JLEHK3^RT2&^yzKxgJq~QQ3S_9&x;}TxZX zMR`My|CU5;ZWyI@*_8M6%F#Ev$w8&rpcf-7X=PPk_b>+Caxw^Z0xO%#JxpG~-fgK6#C}2keI|kS% zfgKAhdb|MaQzfpxak$8f^SJuE2Dk>sIbDN*eYz0qfD4VlJ_qbXU?-86IGMr*iEZ^(mfOIpf{xd;Id8Y1ta&z=X z)$cSNI4o~SUXT8{L)1|EcOQ{Iw4l-Og4}^aupRgvOIehjgY*0MZ(Gp4AlIGWyZ4Y> z2n0HqP(gD{o+*-En-J;2IrwrO1Vy{!C#<`4@$8 zSBhOwZ3+DV`z%yu<~6xZ~)+H+k~ISieQhbo`VbT=y-Xha2E-w7u!>(g8 z-Hu9hE56b5;b(qvT_(DncKzx)<2vg)=Q{7Y;JWC#1ndG}-vah+U~!?l5ZHHsT?Fi6 zV3(A*epBf7hwED08l>A_z~XLhS>Y`Mc4Z-zjI*n5(d{PamdPO9GEt?ytI#b|K)T_q zmi{2kE0tvmuV(V_r9v8)4DD9 zot5dP5GT`?>6FNp>5#~_8p)PvmdN^k8M20-@nzx(cqN(s%s^%^Gn6T2W@XmR%+9D$;qCs&rpmbZ{g!$f#h%s-zd1j1u*^9OUZFkRPMqr_ac*0=oEx6` z6mf1u=E%%ZnWHnuWImZWHnT7jJ-3~}?gAFI^jE;5mfj2O*T8-Q?6)PExLrl!+&I~u zW}*t-7v|je5qo;@7Ugb+axW3(&~N!JOu1Kya`#tC$h<%EO`6RZ*8Bq4{m3~cJo580 z=M(3Of&HN<6D`w^73AE)%*DjHcZhQbB+e}%&i$myxyoI_uX`_Zl}xmi644G3(a_gK z8vIl)(bi>tN<>?qxgm37=0}+yXMU2oDRXlss;DEt9tHLou*ZSLcqv9?PXc=i*k4LA zKU0YIMJDen#Qe!8J0J)Ze9 zaSTW3Y*8kT(76h7?AOe5M3#iivy$6(p2%`RBFlv`WT{@Gr)9(6Ea~r=*JQH&A(8F! zjhaOf&k!{Vd@0QMTN*Ma>L*uQ{_D{*r& z+1%B+Y5dqYr@JO_l@zjZRiMf>h_8=q;}UMM?IzTk+l*{;rvg`5VVgS**~U+gYfV28 ze-~O(`iLwO+%C5p`R2|9F22a^0j_Gr_~s6{MdXV+h&qj{Mmmk3k2)XqnEPJh7^gvw@tCohfRB~oX!w~%ZmgRraW{59;BMk>>VDAO%-!7G!u=3%I^Y=K zSl~F|c;E!!^uSdI?zR$lD}`h2-0j^RkYk;I!=r-nj}f?9z!`u`CXQk1`j$rf#~RJu zi#V1G-0fkG^+k@k^W6RD2RNM2cgUYnmgT!2CzcHY4nHvsSEqt38|EG%X*Kt7(rR2H zGRBPounE8=m0?VE{)nZXSMk=uWlrv=+|SCKdq(1%@kR(?&P{ORi8m$giS9}6$?hrc zsqSg+>FycsnZTKW!^vd<&I()_aOuEh0A~ZvUgDmmaPAe?05YQPH-N+Q5oFHc2E&Px z5#{hGf?JgP=agGWlzRs_XP9zJh;mEg*0`6tz68!y*cm@40Mb#f50BzX_iEzYD&X8j z?hkR2O*m~H z5jp3APSV$36D7*joaoTZ*g;auG?=h}HX%AD&Uajxr)5W>%N@lZm`)76vX>E`L~ z>EY?=>E+4w^aidQaNU9H0bEbudI5)C1^%)RaD7WWN>^A22_x6lReW&$xR_8H&9Y? z(@Du;?U#y@^F&xS+w-c-vR5RQJx(mcyc1f}{4zWZH?hcrw{Dks=6dFNiaql^3p{Um z-u9Gu76LZ}xB}pY0yhk}Cx9Cc+z8-C0ynC}vq;fw@4DN2FoETH54h1`mW`E`3UmlG zhlfDlvZnvontC=8%{~HdOqgbykY=9E)t1l?a8F8_jXu$yvf*+3!n2*IhJIaPk!J^R zP(iAF<@uVZwuh+plti^}h-y!(Qtgh}`laC){^&UHDn_24OCK0NL<;0=0lf$$n|uLFnc%r}9X16&bsbAg)& zTycpvOD3B4E;*v>y$`tgVWPbij_7iWZt?79c-D+~hMw|*Fwa^d&&th>dOLVK5yv_L z_jZx@Vc<$C$gwWoZZgMmB#td4vh1(9SKDtG+n<3d0BH2{nJ_wU+1}U?dajoeGxDRO^iXW)V%;8ad z)%!XT4b{ThA}^|ibrmGqTrZ}z=6dIm-CR$0)4PD|CZ@ktTr|#Nsb`Sa1mX8C_P#4K zZmGn$kBM=Z9YgcCp$t*O&#d&~Im;#9Ro>O!54<0G*Lc@@*Ll}_HvqQ@xXr+A0q#@a zFyyxtxNX3F4%`Mh}6Jmft>l*16z?jr9|;J&IL<$m`5LX~T=-lH-$36W z;#@v(*NS|P19!cGoEz#JPK-(LJs~-6e-dLxN{sod3}dR-jMu*zrdpxzX_;!cdfg6u zwsr~XG?rD z6{^klWcXe|s=Wq0r%;WrPT^dhwjJ}h?f6#E;~xh-d~XxgN`U9XR9i$;TkKmxKfnvX z>*Y@*TSNWr;gNjbhvhy+zLmh=R^(d^e2ogyZLMzuskn8h;`rMo6}J&p9A8sa#qk+> zZ}^3u`s9q2Ps&*FSaCknhhF;xe6|cv!_VyWQO3%*%eUM2m2Z!4ukUN$H@#+mJ9k zrna$uBc(jT_;r3x(rJEH(rJ#0JX6DuN4=N$Z}->q-{G(2ukEknPxL4Gky|dn7xHf4 zJ-~Z`_W|z*J^*~M#GfMTG=HjVK(!@tPJcS^cuqh5K_=FfVrz7zHIIkl-_mLSSf}~@ z$TB~6w5YJmk2$Mie^#{+{Q#eZ^||sV>a-e)vhd&QuZKMIZ zJbOUm*1|0S7ivm~~)A+}ADNZPUtNyBV=-TxM`?G68% z{yF|4|6Kn(f3bhQe*y6AfNu|c2jDvb-wF7Kfqw*e^ur!4@xQIGZIQdZpA_4>z;_9= zEk{}x^dh$9-eTL$ux%}|Z5{Ak!)(Kp)eYG8sedc64Xt0dBL6nvyH}8H+x@$UZ99l< zJtVg6Cbr?}7%J<~>Rx@1FlF}nzn5vYU!qxWq8aWMKqswdximZE|AlCF*nh--)PKx> z+<(IVv;U<36!3k4&jY?6@Tju-13v)xfxzbjKd8iiTA|rF|9Srfq}e6l9}m;40QeEW zk0hFnx<#{_pjn_2(kxIJ_`wRz0#%V_fogmr{Qy6Nyfo>vuw(T~4VA$UZ~+0?7T|#& zS`@%V!?236EpU6F7Lg_)aED|-pFq+Ca90+RW_TIWRPS-tQqQ48JywU`YzkN;+67W2 z+Ks*uLiibb0B+@j|ILEcmVz>;89mU13c>L=Sl)W znRbD??w=^q7C?%P3)2pBs94QLkv4wHE!y1-?V1qnngaiPn0C#Hb}jr_^aK2O^4X+M zblVn$$FW_Y1F;V6(S)J^+M|gTWL@U~ZpoS(c$9S9B+_w#9MW-3OpVd92gQP2n-Dj3p@cl20ms0kFR|Z z_*uZe1pI8^Uk3h_lE4UsYGdT|K>*44YM5%ThtmiA+*?e$8KzAlrcDMOOK_$A=z(d- zv>Ox@1!f0cA(r9LyipW*75ED7PY%2pm`g01LoCA`?Z~ou#Ihn)mQ~suW=u(7k<7Ap zB$mx1md%q`R(4x+_?hJaJle1%up;nY;QheLz^cINzz2a318V?xE%4~Hy#@T+z+x2)K zBUmd~JBUu!N5Fp!{3pO~0v_k)mIcA2pdpwXObHr;Cg48>9*xir;CBMQ3;5k+4P2e* zJdi*u`ED}#=#IA1fhoDY!Y8UW&+plt&QHPPL^4&r-fD2}u)O@CLrP&Z=-+(^eqRkf z{QY*_2lvS>kYA~MJwpXPNB&*gygmc*$P)SM>J3r8dH>M7{=M4f78KyuhlIcHEFW2E zchiBr^W|?#Ndr$4l(I zc~&oFiUb2etZJGY3EDAF+4sPr$I1n~^c*l#oswu$r254H$C6l@u66>JUs9^m%^|26R6 z0RQd$VB283VEbT)Ai7HXfd3BoBf$R&0v>v+J_O;hRpWxKlvHv@xQ9rm^)$~Lf;X`Y z>YrCIEHC$o9Q_SOOB&r)eY+3Ijf|}t$!@@5wXcWUt-{Ij1|!_d-GUfqD++c8et%K0 zC-8WdcaFY#!w&7*k~|$yo~8x41BS??>JwKmH`q6b3)LThKU5elHVF0)=A&8-4hRke z9+mllxxqof$AL$Uez0(27X)kC3y(I=>z$X2M_1|b?}Xc;J5Gs$p}p`FAak@IOT9Mw zn5ABBJ!vHrZujuusM4>j^h0n=;cLJjE}UqoH?B#kFGFy0oNrO^so>MWXM)cLp9_u) zJ|7$(d?7d?I59X0_@lrd1O7PhCxHJM_>;h&0{$1^PXqre@MnNOyC^s%I2B(pJvbvc zGx%a~R`8|Z?BL76SAahU{6%{4CEzau{~PdEfd3u%tH5Iz{~GYusYew`lo|epc|&^k z&mWT8>!I#=?sK!;5q`HfN1y(fOtvy#Zz~<3BwetE-_6mR%K2X7y#9ESrSz5BSy^6h z{YGMaUxNmX8~Q?iuUGVnjr}3K79b1%tx>~32>+>m13#W}>TM7T1{yVx{z-Z&yZop8 zs!vIPqUt|MfLY}O2Fib`Zuw6I|NfLr)sndH=LQ!B(fKdre*KCkKXoTthp#yv2Z`>E zrGN2J@Z-|I*c{wa`WIV++e-i9%OKuTAFJ+m1$WE8kR_zDpdhNczhX7~zTkJk{ixxE z${nLX~;Jqrqdr z;~>O?P!)t~#QySV8e^IjJynMBsEi^GMfj*lS^m=`|F0k?LLCq?K=9qN9fOrV z4Cz96%0N*Fd#!p=2z%|e8`zFeb+jF!+d?%!!2i~Y*pAR0p*pC@L$yM+LAV`+nsY;m zXgh>E{&%(`++1_WTKY|ueh8(b<`!y`$(TMf)?|bnAuLH-5ORiGq0EpwK~%ye2dO}{h{(y2GX+E+!7{(mwF;Ww(Q%o~^j zb@d$Xym}$UJ(DJ1s3EG?P@_;|5K=*~kb12lJ0GToxr1`?mc)TQ^XN3e#O8SeA2Z#b zHxRElFm=o;=xf4BJh=N{{KqhIvrzL=dM?y5)T;C^+J@T2D85ifocN(mAlPCQU#N5F z(NGuac_#=C5bToTYpb$W`7c*Rs0G(v;x9Veb`Q6|N2s@=1w*-EE$EsT>J#c4$^#)2 z1Z0Hg|CbgFK;6f}tUyf(mNE(D2Y0T-%05ghqx&g+_xA03irM2n4Y>^kisk zs4xT|Ahqj(P#^Ttj=q232meD?Rqg9HaD>$sbLFq0!lU(k2#=I63XKP$Zc%6g2-!E% zSyM=7O$|*0;Z6|ljq0qKA<8g@UJT6w;VuyFo*SA?I_sYQz0P_wL@~9PH=)idCY^O3 z>8#0;%8JX;*NO21ORpW}=~_Z8SqQ@Yu~TMoXh~=(O_@d@Gz6i6G-dv~WO2cb=& zWp|uV`B3G{FVsVMMRD4%lCN39Oi0_H-iz~%d;%e~&@k4Qq zxK>;zt`|3m8^w>rkHt^KP2y&8i}q=ng^;5PE{p3xr${dV|mhguWo4(d`GqV<6yaWB>>QLC6OIEhH-F!5|C)p#X%T zAPfWH2@r;ZFam^;AdCV5?cW#>o&;em2!$Y^<$DT*r$KlIgl9o`4uo+aJP!hzju${c z(=ic*NgzxHVG0OSK|m`p9fTPm%mm>@5YRNd1j1|(UIyV65MBiVwf5^EyaB?SAj|=w z2!y#H%mbkqg!v#W0O2hV-Ugur1k}{;fUpRJ#ULyJVJQgjg0KvPNK-dVvM`@G}S}K{y4%FCd%-;a3pOfN&Osb0C}t;Q|O3LAV6MWe|P? z;R*=9gK!muKR~z!!gUb-1mQ2x$AP{Q=qrQ13h3iOUlsJ#K%W454d}I?*MXh^Jqvmc z^gQSV(Ca~89rU+>z6R)T2YpS@-vRnspsx-3I-pMkeG=#mpic&U3h0fXH-X*^`c%+c zKyL+o8tBtOp8WR^bdf(3Fw=G{z1?;1ATMQw*dV^ zpl=EKR-kVU`Zl0%3;K4TZx8wopzjF!PN07n^pAkPGyFf+&I7Kgt8c(JfVeTZw>S_* zQKp0hM-l?20ztxbMFa!{0U55^mRMKaS|@JYqwd|-R$FU_tyQbteQm9+cG_X5ZNKMa z5d>HJz5P9J({RpzpL5SW_vT*tFtdEPSw6xnH<{%l&GIy}e3V(9ZkA`5<(X#rXtO-a zEYCK}bIkHFW_hkzo@bViHOuqO@&d8DB^o!Y);L}}(9xuhXwu@&CJ2c)7&snS4j`fRtp|`hlo}J`;tcT# zx(HKTv^qf?K^#&&M29waN=;Hn>my8RXZ4<mT%1`V{4g4I38px;c91DX9jlMj zafC{Yk2b}{>mv0jc5|AFPCXr+bTJxisO)Mq>0{|(h)C2Xm^6v85z#t(&lWaEZ0dBe zYE87xWQa9I=+GBoiq{zpiSb&U!;#jaLx4k74J}bd=n1?&)|jA!@pePKM8{S#>#D=CfSNMJm>T1pn9g@;X3#w1+^nl;=3Rrdf$DgX zzIdJ15EExKMd%#{bQfJhY+Ve6mg_I%CpuZ3YBX6RZcNrkCYW$KHbNaAVT#sk;??o` z%GmW3BHG_6LX)VEjxZ(BQXQM%2vl#;w#}VwqqJI6k}lpzW!nQ2Sffx|A~pI1ts%mZ z?GVw{rPK?l*2I|zL%V>&>H^fk6 z`e@45T5?sws1nk8-6>5S$Kp$?79-ZAOG(hh3J*={I6b8pooaWrpXeKSXJ4aMO;hU3 zTB#;n&|!ulo&vO(=7_0U^lu^Kd_J#Wl6W;C<&zMvkBf|Us?Z40sBM)->gWVre5{C2 zRJTQ397y2Bcf%SyU2`jN>Gew*zGP2<8<*cdZUO8?g=OD;^RawuPVJF zbaBy!)EHfCf+^Zy4XnMjQMB%Qx7Kk6V*+_YCDd!HoHr*)M+_R(D>B&m0AS0y0B zM7#Di$`j!*iHz8Fn?&2DRoX_7HmBKmlxWeaN(;Mnaq5JDc0rkKJ`xu~6vrln$4Y{IOyDA#AhPYHy+(5Na$HYLb*_-Bzrd_Hu zwfx;K(lQHKW^8+paiT}d8hRKLQY$C)649!2l~(t5ZGz}fwKk2qIJIR$sABPC(a@)g zWPRK~L#)og0Iw`Jt2VpQkBLs+RXW9}wFYC#-Mf_4;6c*8bUZ0KR4wRaH3yJ;^_e01 zbdn9sOv}hD;y7zblYI({PmDDsa-vCMLrmv(_g9HV-j$6aQaQMBl!#$`=`|cP5>oFxF0B<^I#lbD7#}Yt zr8|t+AR4!GYRtjJauBi(nmDJaXp?B_Q`wZ`)gWCwM`|OBE)K;kTcV;FJtD-8WNa0k zdRBJg=w;N!C+Vm{yCSh_F`C4hhsnNj;kM%rH#k&Vowv_0&Z&GxH11fbrg}@0hMB+~ z)xCGf)}`2%k1T35hG^?7Z*P1+G;UHU8$)6ZrZ~Mr+F{Y4d1V9Pkcs0}fGMqD5P$76}7M z8by>~Fho~5{+<>so$AqGZ5dIe(W|17Q$0r6moPDEBk41-+|#kv6-Ph zEZ$>-qfFisZQ55_IY_6AV-=%M632eAE{TtIv^_7{hE}#EAA=JOw6hf?$CE^H{IxDc z#8AN)>J({}iAf(&W)~GR^N+ePTt_WQ)R_fsiz-BTW{w* zM6rlXC0b(n$qAR`kWyuxc1v{W<=jOZuM?$Jx%(*G6-!_GYxJ?|c*olJH_^AXb6<{5 zjBF-6%OS-cbSX$d#|1Da#yGv{GQ+bZ5kcp-}oSRl!P3*!FgiiV(=1#5 z6s<&uDiN?&LyWpgu`1e%c2%mPG6I!G*`;<6t-CsBOw_U%FSNBBH;tx&k*xaE32J+v zPNI*Gb07P>XPJXBGA?)t6$5UZQc!ni@MDnutH&l(^y%iTMXbgdSC-LY*Q1N&RGn|_5mky_RR(l&Jo6H9zjN#>`iiDLGGk^M zTW4NzX(icWzsb6vVVmj`Np_X_UNJy)^1h=}YN zN3`|6dt2)wl(R~0wB6(RqKntvyI7Cw_STE;)7qFA>9i(Wa#zzunOkQNyX57fZO6OY zVOhLY9yiNH^H_CZAvcmnM<>O2DSG*P`FW``GPu4{>}5>jPUoD-nMI85XwJAzTyUXw zjV2>kDH8NUIBsYSu?cEQjke>K_Sy{=wSnfaMTmR~p6B}eEq ziBXnRh%549aU3mX2)nTL4<#%`(TBYUaY}(i4yara5&NljVH<_8@CUWbw#japg0#X( zb(EtHw+Klg_m?Dg^u>XlvsEqzshu`m72Ab~0S_j^F)LW2#j0AQ-?E8dcWsxDrg=zd zmWj#knq?$7LbXT83w|(p5vi=7SngOhEVOou_CJJw*4c~6GQM&O6Dyi{`*je-p@;C# zntIC-hDjlcLo9QfAz56BbNF{u$a{by6$!Kz0{c+0x@bnf;{axN^;sdz_x`~WBh3_@ zV6gW)A^HW~zn?H7UMG&=wnd^{z)2yX&w~jdJ8VJXq(1fgEuojOs`c73`_Z z?VCc%0}OM^;I!?<+0RF+R^8h|Qs9HxlNcLs;4)RT!4T)DvWues1B`KNY(;`=imyG} zmxZ(%#$;)6Nx@hz+ph(Rydv|`vRLce7Eo1=>hIqrGWV1a7oEuFYof7=q>qH88Y-`5 zN%jpHZ50{+tJ$CEwEV16UjEHuh#Jvy&DNwhvSTlnnC$$DFYFQdOh^c*mY=E;Y{OS= zj4=(;rLyN1VefuTbg!P?s@=6FaRf|c@)vbxx%O-Ce_izNc^~y!CLUB9Q#oGRFX=13 z7Crs$(^DU7xmAs|tkZ6+;yck*d7rM9y-&-|uYGd1o?li4%K8QTdz8LZh5*Agq9oGok zIgQCkRiMM3nLmWQ(E9|bQjCzu!t0Lhm%oIN`=z(0o?+61sJF$|axt9^!N|neDyx++ zS0SYOaIZ1OacU#+yyNf?<}M^uKQ2|3U>wLzEga(#9alxd>WY4S?-M=i;mCF~g#9)F z>sf4NBElL7QPt;Sy9um0wLN;+*Mi!Vl)DbLutq}C-3Mz8lB|VhD~YhiqW|5;jlI8^ zb8MN>#>KI(qSbN8t6W5eH4}2~o|e1FiD%c;Zb?faVaQ#>iv;WSL~-HLy84b5%a>RV z-IiS~F(KOSec*%B9)fkx%RZ>W+6X;&AEedw*!;F_ojKN;cDvgPd0`J>cNKXKvpNbf zs)rC`bIYVoOfZOv&2CQ@AxL{KL55LdxMwV9e5RMydNi?F#04a>TrIUeLSXg7wNtTH z5ok@0WvaCZ@l^{U-fKOw97a`oPpIm=4csU30i7s9HK z1b&oTSX7YC`Jd&y+Sc24_F{1`#u#PP#o7;_VL?L9;A#%uAx8`-CP~(c;;s|U%$#<{ zscCL_Gjdd?j&0MiqfYt=@zt}DoK_@G{Jr7|@^Zv)cC3jND-47Ta@+0r|55H2KC&q2)+5-i(6b|b_tkS;#n5brRjzYsR4TI_7XiW!^g zygaWw+fvBcN$o%#w+nJJnz)IM6$#sj;yR?gQZzzv^)vei5Ukd4HJR*)JRmiq`VmPd zWJf=Q(bk%_Iej zm66;IX<1>awcAk<-~Qk~8`!g7SjsDER#eOsIx&lvRsxytC6ukwV@gnuK{YNuT^R|TWR zVZ?HCvIvetLxIrnFtgSjk+5sv2$o z$8h8a3#{H9l`~HWt-f@>dx<+LCoy(VEH@itq&kY*!R%vZp%57P5CL#19(L|@-1TO; zhequ7+hsoeAB?mtYVFCh3tT1y{;Tnl7;6z2t=2d$jfAZf(jMk8>J&qZG;ten495)n zc3Rl#|7stS-ny&mMW z2H$*NcP;W9VG}3dN&5JNM0Q###o4Xf_F&dIqz%+Za65}bpuJWc6}?l)tA3h%4{x0& z!5FSr*$vw*1l@CJRCUvOT-C=~f4*dhjO3OtLnQn3+Iahwi?Dq{X7z2Idl<*wj(lbd&unD%?c~*$L=h3Z-NT)-Q z#e4DN7ZYDrF0{@GfhqSox;Y8-O3UYFl6ZE!?i9@Z3)WK|%dd5e1Nn)I^ZNdR(31F{ zwKx`FoI-GifQXJ`F>&etM+MtS;?CL#v3F!*ZgSq;dQYf$&;eRwti*+4PN?(|T=LP1 zOO#CfPJ3}53IzcMquTy_E-fjv|f4>Ev)y2J74t*`$TjPx@UJsN9Id$cdz)# zrsLO2VV?>C;l>hfqe%Bs=cnaO;zkXzVI)2l=M-Ca#wJafB7W#%EdjAj#AKu92eYZ1 z8rkdgb0Nv_0Fr8yV!1I#{Fu}x*2JC(t5du1FNN^F|4F!A)K@}OM0HV?yGaOwzPPwl zYy?`(w0p@hS{-4zdqaHS2Q-y;Uxj@uBx)X7VvH`v5YHV+sED%6Pj(A$3VDi$mPcX5 za{6wU^3z@XwE8DfY`I~F8?LH5YneG>joL`dKBHaYFGAwr>UKSdMDfeQXfE~JegK56 zwr?so8GaYy z2r;q$P7K>(8gb*4^=B;A&Qq1OgzOrs(~&jDZ13loS4HU|gx4_IA6&TQcB#r6LX@6D;=upnzwP{s zp9(qlm6T0|RNa4(DvpenEh}5SD_i_$Lv6=gt$}M%cB!p})R=o1YRpV8Ey^k3ka5Qh z%I{SYa1_o4|zeI&o(! zSJuTv6CL+3icL*>TsjLa#{V}h{J=OanO_t+uEi+33I%chZwg`tiF?NFH*YJu3+ewY ztk}^by(o`2^y~D|Nla*DDSorXx0_$qe zet|32`iK&TJcW?we;;|)m7`Uv^Quv)61{_}^|szmo5zz5EDxs;cRq@XpO#~Q^%j16 zO8W@`!S@qj>1bK{iW_#SN>B?4$_J9b{vr1StNEe5?RNxDKd@0oJdhmQ)nIWW|6S~f z5^}=sXAd{_*j51IV51g4YN~RFvGNfiCa{{Asz02uhu8Wr4CmiSDq}={pKASWH=Nru zQ}tqGEBb8~&QOa~>hLXql6q)5EX8Cxte4<%C*({%8mOpNm zmzm{L&GKnx`3$ps)-Gk1GFzFW9HY!t<|)T2^OXh4LghFmJ!hNc3(fK+X8B69e63l& z-Ynl}mTxl4x0&TT&GOx5`TiqDFr+CV==Xheu#WSBZMRI7VOIm1?^jI}X zA-W*{z&<*)Uqlc>g2RG?{4|>2NMr_LR%m2kXlRv)FSSW}qMD>&U4$l>$ogp&0Rcq3 zPncg=WTe{9KTu8lLe;_QJ`v6~mD?nlt4UHT0(i=In7>Gy@UBk?xl`->`t(uw2M6Pv zI;={Z*4ZRISxu5Az+ayc-*-{G|Ly1 zNp;Nf`DPifA-Q8HJjg)5AdQzMC{HV2;gJCRW;D7W>rOK_D$giil^7m(`8&V;;;H|4 z_OgfMbtTW^+@O5JEMK%i`KDRE_(4J<-Z!JXV3t2!Q%I)p(l(pdrFd;u^r7-&q39#C zeCY<|CuaGw2T}ByP;}KSUtW_Uv+^suq6udCii@=_)w;~@b}qVhsP(Q{_MBbucgmYW z@%LtV`3B_=X8EcIQG81%{>3ck#ZA?dK12DJUGXHde2qh~-wlttE!`_=df2*-z?uy*2PkYDi3T@@w~l?nrvEFY13oECTqp3TB>;0i>=~q zHrW5vUgagsh|Z-if8=ovJ2A*VLa&XnVN)csw|c1qEu|rF{lQM z5byxNJsVW9We8?Eg1YJ${qhia@UUsa$gRE<*=sftx4s!|mzmLq2Q zQL~&`>bP0{tXck?S^m6Pe!?t&!7P7qhial~lGI!3p_-z4O!c_x300ZIV--)D<)_Lz z@YXH)OJ@1YX8CEe{1vnO3}4MnEwQ{{herwWuoUa(2;S6_o*9>yRx-MvXzY-jVqIQN zR?etAp0wNCxrM$sIj4xX0Et)l$Q@#mc<`B?$s1c+IzOe*@ugLBWZKxA zJXRCUoRx`3=jMqMqH0EH3-Z&8GE4YFlbpirg8a#aPm1?zWjcToGooc;mgQ`Ns0`fJpoT-_4 z`J7pP-YmapmR~Z<-!;qMH_JaX%Re^D|7VteYL;K!t=g>GqS~t3rrNIBq1vh1rFurS zTeU~ESG7;Y;qIDQe#0#P+AROZEdSOl|IRFDI=pF?GadeDmj7gy-!jXY?S8GYL{%NJ z9dute$seiVY3{_PH+R{J+mefW-{UnF{4FMpqMq2RSe(o{Ef7@C*)*KxVoePi{?+s1 zQK!w!%d4*Jq)pjdY@pSk?19~h4~)m<>hfQ)$$#4Eq_4;n7}i$h3@&?A0@UR+(-C7ZI#+%{E1?EZx^ZQ9**ePGk|9%q~P z)m8n4YB&;~nZ-}Gt9$W3o8%9;x%$46t>@{L@0mmHU4rwuP3%V|`RE$N{;R@rq$NH; zpU?Y>#;4`oMf(k#_D@Xmr2jAN_-c8PQuPpiXVZ4YBv1eU(&q5hru8n4{$x}A8T+;W zU#cyR=1t0~sak%s>HFLyPyPSX=SZh5wzj3tpQ^uknStuIilyrpX8D&DwKU<9S;m&d z^|HB%6LK<2vXinibF#8ac!SqX=hv}>yNB1U*0sK+>sR7M2s2)+(z;Q2DRO`0cvzgh`@^&eI7OiUWvm`FL#Ag;36qSfO3Gy< zW6P?rWwkE0n+>dIuvMiczO?p zI^GWwK0W*?Vfu{lnc=g{^1sdUf6SiJMn84WoAzovCi*W&5kOT#uer8 z>V^`QoIUQ{P{X?!jG2XLJPV?LSL}7acWbK-%UdMUB`zgA3s+ z!^?@jr<>WcmL>X~2Uto(k_LMvYQ#G(?EN={Z?2(#K!fnD;oHm&zcPE)F?-f6>k_Z@ zs(zLc{!IA38ans99eyDEpmbd%IB;?Yg~c&#X*OG!atN|yr5ui1(lOR^d3S$oGCd0$>;hGqRv zqw{s@CKMGEl$6Q#$UD_<(4=Ym4qm=JdaJ_w_Vezq9TXiCYe-BTGIT`x=)wsTr#z9^ zCBHN;?_xubMsm-_DY0Az6K7CKUDCyyM2E#1)g^o}s&1hG67@nuWnU+=TbI=pruv5{Hw6R+1(($;t6$cjtl_3Up^C6F zkFrK(Ey`M!u@lwAYNfEp(VJ(wV040z9hsRXo;q)T|42{>RxMHBbtVvnZvSvJ976a_c^H@C>6ji=o zl1D8%{+ckUFthS6a!KxfA#QNIafmgS`GhjX`Y}@^HX?_qr$D^lsvA(<3Q) zM65BUv?PraRCQ5N+N6z1DGB7x@>#r&&=L+2e2cf@b%d60bYly7KPb_&evhPK!!1#@ z$*NSiaTrg#jT>wl$!YGLiQAYqDxrq2`O2D}$`m6ZBhyw|6&VSava)l=c;x1d%`d>% zWbv8+yc9DkXOK*umj9sojD>0GoZ2?#ZUQD#&J#)s#b@z$(Wbdvh2`@wNvhMp)|aoP4)(jKqDZ+{R2s)0 zufSe`;@v-DfhC&!|Nd`mUa7EK{I3HvD9$ww;@`cne_BRbg!ruB^ZxWSozeFBtMNHW zv3wpNNiLlVOEmF(euU4XN^=v{eD2BTU89R46Zzba&+lcI4iuk*CCRn<$P{%^mR6*T;xn3nH_bmY7CiPa63sCfcRcsp#!T#Z+2(XGJ0H z%#`O^N?w=ezWAEtwM%~NRfl(8m)wpHyV&5&l+pPMTNf6N*fQnM%)-jQiacb_jQu56 zn%dKUum3mxoBVhA|K$HkWefjz{lE3Umk?YU5~jQcRee4@n6SXPr4qn zeA?yu9RKWd^s8Fo))-W+a8YmRwu%-$me^{_x%QnaK*Y-)Ez#bdsWFzQRnLTT#fXeO zm_^QVs?G@3>{v#fM`w=!kJ=u-9w8oT51B_t{t5ILz^Be0ojjsFTJYC_9zGrs9&H@) zh_d8S*dekf<`{bxD`&A({%WrT;a@g&P-K~%(ro@Zk1tU}UQ|Zqd?seJ9OqFbYskp7 zYO>A=cAfUPTE?T3gghw`O*zs8>?k6xVzlQomv~iaV;%RxwgTBuOAc!o32yD(0^Ib> zOa{rzP3;!qHo*2-%vRz2BjzgW?Cs~)pTG8Vi*QqjdCb`Zq+8=9X0)2EcODs;?n-wb zcb&VBMYhE|_Xzia?vaw0dw}@fy|?=#d=3+>N_ey-#~Vq!w7N)alGtuv*5{}9>gg5e zAHdN~teHd`N&0E9Wi2FYewKR?$6LR16aT6u)nh%?RBFXhwKGR+KfDT+R2(yPtN{#C zf|M$az>jP)Swv)~NM>n>*$vr`vOip0T^hJFb!q3))up$K!bR;e$R)vLxJ#DH zIG4#T(_H4etZ=Dt+2wNB<)q6SE|*-cxP0w$%T;o%@7mn8qpP2*!d2^Pa2?`0+O^2_ zao5?d%Umm5cQXf^alPnz#q}H4-`(8Znz(tn`H{teZi#N`ZsXh@cbn^0?zYYCu-nUS z7u`N}yXp3Kt%kMQ*6LA9Sxa9lrB+t03AJX_T2^aItwXg=Gecji^^3c^dvo#=O5Rf4 z$GAW0KG%JX`yTff-OsyUb-z`+cI{TRd(`e*+fX~Lc1i6SwO7{ORr`h77iwRt{ac;- zbvo1utP@#hNS*vTQ|m0Nv!l+5Iv47ES?ABX^19x2m30kuGwM#NyP)o-y2tCDt9z~P zpY=TJ`PA!MFQML;dggj7>g}#~y52|ieyU%;ey93j^#|9_sb5xqMg6_?U#M&HQm%Dc+7@-+G5@>TL<@^|I8Jezn1 zcp5y%dd~IS;rW{9^~SXucWta|oY8n%r61XxBpB zBD2L)Eq1rK(BjvYZCVa!nb~r7%RMbGwfv)%S1Vns+*XTP9clG(Yq!?Etp~TB(0Xm_ z)2+X0)3lAMO?sQzZT7eMpsh<=-?s5>A8os_?b)`s+O=;N)vmDJ%62cc`=)*K_5<3F zX}_fXiS{=-H0jWCh#n%akrVyL{kX*IVJ8?Y-RljQ6iyeYz%ho!RwB z*Xuqlee^z)eRleM)U9E+{@uoPtLXMlclYj!?z!F9bbs5|#W%z^+jo`kxgIV(`t-=@ zv8KoQp0#?0^~~>C(equu27YS4@qRn~KJC@KS9Gtby^i$yu6LK-LwYajeWv%{{vrN( z9Nj()XdLiJz|?@_0Y3)%2BrtD3VbK1VNhhy6G2CVehBUnoEf|}_`Q%OAu%B{Lr#VK z#cM8$c=_Xv&@Q1PLsy5st7xi-Q_NMo8dfVz6ZS;db78+L`zTA4`<36T{8YJIzq=mp z6P_8qDg0{RPJPq*Zs>cZpI5(;{nq!p(!XQ>wEh+SKO4|_K*oSA1HM#uSC3KeQh%rM z*A!_EX@1ozwU23EjBt&JikKbomaefbQCF_}B(hUvR^-men^7TAk4C*Xu-3pq0~ZZ^ zSKm&buHUY|`AFy^k3I78paz4CgUSbe7VR5d6n#9#CFYTsr(-^f^^VPtJz|gydc)I( zkK=se#>G85*nM!^;8laah!2RL5`V_ngeR%)F#eLDNm!8ZVPdz$(!^6qjgp2XZBP0& zIU;#+@|Bd{DUYSRk=i;nC-vx%+C!3tY#DNEsBY-eq1T3m44X0R@^GKw6NkSxqVYMd+){X4`*~_xO$1JzP2oQ*}CQRmX|KSvLbrL(Uo0RE?jxLJiGkTD$S}rtJ|)A zYW1&cGS*yJJ7DeZb?w&8S@*~Kob~T*h}v+tqHD#njqV%AZ~S6Y(xx+;6`QwjX}x96 zmcO>Tvfqp3&FAA983spG2Sho0^A?AGTx zKDX-mX3sA;QSZdG6aTz0`Gs3A7QXn+$*hxCPnk}A_)^MC@4P(t<+n}`I{oS^k*~aT zMt$amSNp#D+-u6$j=ipU{m2`A-Z*?V>kDm`ef8s*_ z3nwo|Tzusn{X1`5ioJCHa^mIp-W~StmG?5<@M`+iuRkyT{PwkJUo`w;*_R!@+d^V`+mb^Gq%_XEB^do$(cwI7Op_~*yDKehU4>(9YIpSl%y>yux` z{_^Lqvwv&#+qT~ozn}Rd`HwIEoN&AL?G=A@|LgeQgZ}>LpRxb^!(Pv%$yIi6DjrR! z^53+0>~Z@JW5r`)f2oWc1KH+r?6qZB_ckl0vcXit_w3ksO07A+?rvFG=s5!)EE%PV z(&N%}X+Eon3f2%?q;0GrjAKc+lj|;46MJ3v zH*4Rlb2I;DLCpp<(>04}7S}AX*_dYR9*O_C$gKaCp0xej_OE7B%Wm0s%AXBC#oMU4 zSnz!KiSSwBFNSkd%-?2D53^?@t{Rv<t zam3@rGE!#r33i(m;X zgO#ufu%+{QsDMp?O`U&cW2_~p0oh&V!}EYmUHF@KLud`55C$sf3;63zA8#$Nd*{v0 zm^V9X-t0bl#{fHY-o(S3-7{}&^~P3jZ1u)gZ*29(R`18)3787gVHV5*Z1Bc@@26oY zY=dXv9DD|UvME>>ngP1IBDbp;(G6!%)Zo?Dav154QO%2lV*704L!kI1OjuHFyKi?}NQQ=iwq;f_H(q z`uq&Pz;Eye+=jm`8>vz^7yj)A?obD?vzrHaLKE--b}zf70CDU_e!HQw+c%Qby%Au4 z_hgs=#H0HGxD5E-{U-bf=k~iPa;S0D9UrADrrqB}-fZX@k4o88Q z_IL$eh1cOMAfw0c3_0S{6Z?8LhqmAi-M|-+)l&_~>zNMYVKFR$Wv~LuVKuxBY{jk%In(`yAem?_o^&_smWZ(+) z>D3sT0yg);&R)c_R~V>(vD7OQ@}U@}z;qzCy$%6>_xcsEr?)$hliuW{cME6*ZJ-@= z0Q~RW3;ZDvf}sy6KnW(m{@&DWZ+!Nz4cO!V2#f)8;7?xtcLVzU_rpOr4CKZCI6MdB z+y6y41up|J@c$ot3e=fD@$hGi`QL!A;am6~et@6g7T}XVJ_S&30Ud#O1jIlVOog>@ z04@M>1LzY-J_2bUNG<}2Yaspywg<*UATY?ur4VIeGrC9n*ro4`FlO$UAp|433$a|ne9hyp!CLoCF>C_rCO5tIPBg3uN8 zC@cVKFlZ+{1IP~A2lyLw2#&xpcoklUv+x$Y4a6hp9k>kd0eKAikqb4{TX0>V9)f#= z8Z8HyD3|m%~a}1=|1{gRwDqFYJeda1zLSFnJGt z4c>q^;RE;>sDN^A*Lx@=jIpdcYQV8)1AzmTa7=mpf)M*Iz z^g(~0de8tGfhRNpFX#xJpbG>*KNtWSh=52K2#-KAq{2`b4kk#0bjXA$KrH*5gYP6Z z;GiMMp)oXr7SIZaMJO={RY6}sXDB*D(HTm8g`y*LG-LyD2+f0hD1=AhF?a&Z@FYx! znXnL6!A2lvp{L*ski*b7;BB}7?*Q=%C0?P|;5vK--@^Cs1N;sDST5R03K#y3-HNWz z9k5@~3;ZDv`UCbWuwOwg735Me2vPuF6vRt05=KD=jDr%OUKQk6flms2Qp|#-uol)s z1#E&Xum|=5V@QE-iX(6g@JaDG{3S_YEx{YGGt39DH4K}=215cQK`LNV7&e7X1Y#J5 zzA*HKp)Z^o3x5f|0qVMMTcAzf`LG;T!YWt;>tGw~fL*W~_5$Om@5_LUzJGE7lKl7U z0G*%qi{>^#$syAMxrJ4Y5Ey^(%r>m;jT3`s(*M%z?Qu4;H{`ATIrgOF!b$ zkGS+BF8!W`=ivo73Ha3SG%!B;eFUGt75EH@U%xMaI_UQ`PzU{P!jCL_TLJm)p9|9f zANzj{KXXyGDfqz%C;;pnFcTKSVpsypfH)1nz5&E`z+3P>P;Uc>ueufxA2qVn&4C=M zTSI3+wz@Zjfd&$Q+^I8QED$4g2~bCBa;csUvw`@i=fl%LJ*ml?dL`@v>`=c9=io#5 z3;vNLjST8RJ0MS*AW(u9$dASd)Q=_wh5-I*(5ZO>h>Kvl+I+cGwBe z06EgU4wvC7`~bfLb)vx*4Yp{pMe7RKqs1O=AYh9&3Wk9R(jgPF06Vmg!IMCJXy?HK zz$Ps*NI+&{9cTuv!3X?70eyj*OB@XNn3xFUGjTMK^F-pDNPH9VF%j8`P-lrJ z-~;#`euF4Ip$GUuZwLVVND7AtKxa}43<3N~LT?g2CE-(24xm2?{Yg^+ z{Yf)`IU;E`P+LjFHwinEj={6=GJFEp;77Ox*p)ZWC4tDa(|%4 zl65c;1_5zN#>QmoE}3{I6O-g@7z25b596R1N&(xFcfff-cM7^v`T)MA;8zO$Q|O;U zpOhk?c2mrNUnx%kb(^vX@GWH-tbl`X3eLfM@FC!1%BS!-d<%@#l%L=ipq5hbH?=7Q z0e+<>0QRP$HB0qI4Y=Z-E0!{+)OFa!|;B|Nt(3OhZRBC958`Orn&;Xi23!p}a zbcKGPg`q%9hD-$f8&UyV;Z?Wg@8AdcS(1jzfEpTF3(z&R9yA0w5YM3@ zfc&8oU>@v;*Wd!Y3)JJ#kKuptC8z2wf!Z6UfG|+OKp+;wN&q>-CIjPVSQ$JC2_z%oGZ@S}i^;m90L-x1_t1pbYPgHbRMX2Lok zpCiyW;x9=uxd3rBA=^YuO~lmH1n6(-4Ai^{8K&WY98)Hs+cXvmp%_Zx377`ds)?AG zh>3}qn6S;X4R*pauouXWiCCEMbtLn`Na|%|YiI}fH?k+FpdY9q0;1p%hym;wX#(sS zi9I8+XJjrg_C}J!k>qgXcpz^h$<;`5G4cdZ2P1!zq%`80hQ2f(@CV|V)(5DCv~bYD zK*0Aj^rfLMEgtYWjd7Az0k6YF;CPw#75pPfqlo1wVmYcd)P?%c6!2+OC-8=D&;xn_ zx<~be0iXf=9EF}y_%>=3;M=Gy$N~HrMGi+j3Xj2Tz_(G<;V9~GR5`4HbwJEV9fxz0 zlr95eobCzOl1{AC@jbmQ;D35FqyoCq3!n(dXF6jb{cWIj)4zwGB`KpGG>85`zl;PJ z3D}xJ{|xewLHsl5n=u8Z!z`Es^I!`c0Bp;69$ti(fLLa{3D}!)0p5X+;R;-ZYw)8a zW#UUFxyvNJnb?xq5aiGVuq)FWh;JtG%_P2=0T2wKpagQ8sfTz-gcKMGBY-$(jsbF< zNgOhXLuLsO>&)4(1h&I2*aQ3EARLD4@B{n-e@WaP1FpcB9bE^SK}#U+qlx?I?m&%? z?hV*DnmQjn0JNY3>>Hf`Nq~)`Ga(yt0sBV30&l^GK%7Psr_sb|^f&N55UbHYOHx(? zXa(rZq6V`%0Wr!V?^%qmEb1+bcw|L`0R{uH$UmNy;G)IpiS+eK`$)TFIeSa>z$c8)yf_ASVqb z!Bm(Ivw*zkEQc+y9nhDv2lm4uI110g>wxb$jF%kdgq%x2404D;4l&62AAAG9Nzxef zk0JhJ$n_X>ju{J#k1;D?9aO+(*aqktgRU{?8iTGe$AO%W`CF25$!BgT3;<;1M#4ZK zhq=^GF7=a}19?yYERd_Q9|1bYeg>bz z58QVwfh(Xdzb-U@M$j0VK^Gv0`M%%>{tyIxAPlfCpIqdVgM4D2|0Jw~=ixQD4Cu^9 zXZ|&~4)~n^GyDpF0JWR{kHpPP!1yiD!eF3A3#I}&D_9A;fjku)gu`$Q@U7rwpe75b z$pZW=Kxe`G@FAeD0DT4D!Ab9BisUf9EYB9t${d>BYxwMJ&yQ|L+-e6&;c3OnEc9D*Zo9G-&{@DcnjNkzo82w#dcfUY8R6``vLT}1{+ z2Xqt_!+4kkQ{Zt}469&0Y=kYa4bWGFo}w4v6r6^$a1Mw`(K|pMipYJ@b+`q$f!G(j zKrJ8##l)uA6PiMEAP>dep(pf)0O$wQeeolJ&Bes0*a*YG1fzgB6=%a3!0zHDfX&4_ zf%&8uJBzWi7(0t!1?()o0N7cKoyG6Lw~|yML1SnG?V%%dhHlUUfgcw#j^36MK}7!b4ZX^;=&0H?=~hbizB%!LK8 z2-d+a*aQ23S{wf&5XcpMhN5}-!Q$^qGB#Jg-GklV8DKn;~) zd)X;C4aC0ebvO&>-~zk@m*GA5Qj*NwfY_N!VH3OpKSZ-5v-*#x>me~5upK;M%`0e_#Q?UUExZ%LZw3GJaHFh-{NfG_lfIH2z|=8|c{ zAP4Ya8a_jIzFzrSv4C$gvS2*m$BeB&zGwU)Ni%ChJs`I;Aa*m4!LvZFXZ{a9gKKad zz6Qp{Okwk1@Q)KFz|XS^ELMX5rT?{F;S7 zPsso~pHe^~5Syo{o2Tge)D9pfPto?NQ*atyg*V_Wco*J>k3h)!6s`jCnC$^RpaJqR zdkl;PaxuFY$iwW3@HDIjbj&8svl(x5e*nhDJn}xTIkW=mab5=?e)GJc8}xu)K&{P- z1U(SHd9e@&Mj(FkQeY@x$2{s{-Y7utJao>x2I!iPy!pwH5A%We&3_9%giiq9=6?=f z!VUOSk`~l~x+GK|_#36KD?9#$sZ< zm{>3F0e-;vS{w+$&y#pqdl6MlkUB5=muv)bzGNFPewPsMCC7nyFV#XEV9!$eFHM4U7!BDl1|EkwFdr7eVxa%h zm*4}y?xone^jG);{*t6+61ajp5bI^lfmknV1MR>IIzbmu!Em5%m!W6bE;t6*w~X8_ zI|(ntD{v7m!+XHkT1MQKT>;|1>~Be0j^5?e%W`7B9No)_-*WQ4ya5pZ<<#yt zFb^#Ef$q=)i0|^=5C8*#`D{63fB6u=zU3of6cj)qjDun*18RNwTv!0aXZccC3mae~ zY=P~t6HWm&v;22ST2UWb0&!W<9f>CR@ed006JD)0cxR~9F!B&a(@VdJ`e`s&>xUlj(_FEx_l5&$K`{8IxJ5H z;$BYN%kjB99YzE3EhoO^#J3z9%FAI5tOsl<-wfDOj?d-zT#h~E_*{<9<@j8F9G-_4 z;S^9;<X91mS&Hy=GgWfgo!*%!?zJnhCAJ&kcHGjZglC&0E);5C1&~kRfxNCGuj_WgVK@fQ!3#kC)}01)uX_jH1^it{KGz97zrgQs8~&E0 z_4T14P}A%2b$tgQ?(2QP7l`}%Kp?N{snzuc$N+3wPkh%imey~FU9bo6ef?#)3SR&^ z*MBWZ8zLYMCcz9~TyB^H^Ie-X7NF)T$bZF2cnMyCSAp8v=nB-<#zsKAH&R<0sjZEzftYV3<{NuL zFHk{0Py;q@Bz_y?ArX=R`!BVg~x&XZdwdWVFj!L z>UPs+*a|yf7aW4)@I1T@Z@^i23qFKT;8XY<@MRMKeLr3TWKHv-3zc~!Z>tYOGV`uYK_yUB!Z-83cOs#Fc1;0tsmfBDc z7(ZL&Kt8uLg%;2nsJ$)3bV~@Je+#kNQU>VUg6~_7zzH}BF9UkF;PaOAfc`D-!u#-t zByDX2{U8b+ffz6VzHChea%? z*!mK@4Hw}Opnoed-%1R&UWc#XR}l961^-CWHsZUj5U9CrPry`|4(Q*u2$sMyK;O1! z0e`olYa9M#B;kp1Oai~t^g&BhQ|S2 z+t&egwjF=BZ-s+^Z`)r1^lhiMwx5FwfS=pngD(O9wlhYy--Msw7x*0*E8DSeM}24k zfq>sTMgjTUF&9<>I(MLR2Ql73jCbJkjw5g!i1QBOyyFz$`;Pxf(oT1vUUr6q4&q@L zjDr%G0Fz(};M>lbun@@UPW;@7&Yi1aEue2F`gZPv1Mnie32y^_?tBM61!{WdHNfAU zKLbAQq?UJ*(_OWJxbJERjR1Rg&4CqA4r^dNY=>R22hg{RnC`j^__XUYK>jZ3W!Kk$ zj$PEuE+PLm{3A)vkmqMwK^F)BCG>>>pao+4i~$CN5t1MUhQLIio}W1i=iw8;uV+37 ze0qi)KJ$$v5ejK{UGM-;XbR1NI@;|Een1`V4g&P=M*r^qpn(WL_il9VW?bza0cns8 zxiA(Ap$MLUsW2U8!EB%&b`z)F#A!Ei+D)8x?}HO?23`kpvzy%Pz6c)yvD;1Tb`!hZ z__6y2kel6qNfO>)RM)Y6{G zKrQVdk9)9h&qmk+*trKg_n>IcemDf!xCa~eVB?+(z&P3St|aXxrh6Mh8)y$5fjsW@ zf$q=;!XO;_K@5<~y~Dr+#Bpyr5Wl@efNgun!;>%rh~ZvhxOWL`fK9L!uyODAa2x)X zq<?e-^blm z2z}S#E5NS981wifI}TyTp^=ad z*WoN+#}V`%=?UcG2zrjJ0QwyH5dM&)qpsi%b)i0x=cDbw3p#-}P;W=OLk~~@IX=o* zKB|TYhy>z$l-wSr&W=umN1+U!gc*QuNAcw-z8u{Mn_)j30sJ^hY>(o@QG7W1D&WJ> z_u(^1I);tMu<;l+9wYb1$o(;Lf2lmEWjNP3&M~d-N6AnXbqZ*^9pP$38~N7P*j}TnmDAui>0^ z)$pBllhNTixz=rG7x5hCB&RvYMXqp_>)hmN5UhWO=Xf4_us#*$}mD-nh&9F-&43oAC~E^_lB#gKP{yc>SREH>zCgZ?-Cf!rIK(43aYzCo882BPl` z!|@JoIKo-ZbBQD#1i?lbHp;M3hK(|8%s?jI#y)J6XQMnDKf(L5@pE*z(L1wIu8neS zER7yF{)8RbD9gs7tUwPNowF$mzOzZkn|d&S!3<*r6Pd+a=Cg<;EMqxQY{719vKyQ1 z#-@05yh*2<9tVMs1X0O&o)>u;cZ+hDD0hj}!;RqoRpn8wni1-bNke9CkKJ)~F=@3;e%)lM4AZ>wa?@Dxsgv zwW&u#da((2-u(aX-4BB3XLt_xjD7*%k4{Sl>`U~ge9l*Vi+e@eooF+PHlygeG{8Nh zo6-V%6WxXGM9>>^iFWtsv5aRXi*WB~_l|b&X!nlZLn0?Q#aS-kzR_2C$de%067no5 zkb8^!Z+V3h$g)MhTXu3M2)5?H|9q?5TT4@ha@0ZQtuk+wd23txGMrJ!x^)7Rk!PzM zTmS#O2iTDqy~pS-Mt3nUB124S-o$Rie1o%Mn&9~{o*A=%HLS(mW7cCHF*}eqW;gB~ za~9{tTq23V4OR|nO|H4{TRp)ymN757{>$_ zvkv*=wh)W_al6?=0@s3In_jocw5>XgX+dk+(V4DvM^D>4cbgsF=2_d!a@#KA(L=mD z#b+fCU-2!au#53!`H7z~kN9f*j(f$o!CCPg@h-(XE8gAW`_P|3m`S|%Dt;6Tad!M6 zl7e7QDn7^a_cX?V;D*uy>caF0FQbD68$;8qaqeG7Nl z+XK(qdnX7I+%@3?>|BDg6Wlk!XA;~u!F?0lH$k2R_f4oneHtNOf_@WPGl z_qW3S?C-#SPU8Oi-G9IPA1I2u9QckOD1*H^FcW7TSj;k3ppOH(J)qkIx;+q!cj2I{ z2Xj-9!W5%8-{9E?J^NsF8Y9m^c@D~Rurp?V(CiPI<3XJq)XBl6*w=%%c^m{r9~?@C zUJkv?E7;>h@9{C_a_Do+<&g6Zm8KjOs6=JjV^0q0?9f4u^Dk#O$2Gjmhi>vT2o5Jl zuZLg4d56=IflSzk!&&(fvpL)wxeuG=VY57}zr$0R#th8rusu8+k9!?Hz+q%KEW=?L z4(svo#UMBm@&c)NmDe%TBhEcyu1EBI1#>^z6P+FH#{fp6x1)MH>fEF2gW&HgT;nEpxW_}D1VN&X5}zdn&PsGv zqO%g6m1s_h=9H+fL~}}fhaBWWcZqq(PeBS(4EvL4e-gjqTT1aGW%-Gpu|tW~&}Cu` zYN69aeI_=h87*l;dpgk-T_^UUFYcN+m|?hcqWdO}XA<@$(XJ$#L82KXnn9v268~Z) zYgordHnWvDcCeeh?B@^$k(kH{PH~nCT;?h_xXpi!cw) zZ<3j;WanMp=R@*Q0CWDQD8(tkm)OmJzT*eVP@anXLREgFIyI?7eHzh}7PO`v9qB@M zBIrYZ1~HTojAk4YnZk5tF_#4_W*IA3O(YwLVhgcsXBT_e$3c$pH^=#xGo0rVNnGa^ zce&3ao(943XLye1d6AcSjnt$gBX5y~Y~h#sRH6#M z@&|uXn|d^)3C(FmTRPC0ZuF!#{TRp)hBJz>Okgt8n8_UGvxucEXBBH%&nBXYVH-P% zCxHVT<{1BQlGB{yB3HP^P3~}yhdc>_6Cux%f)_}|tGq#4GLVV4d50Y2A~$)0;NRb= zK`rXifW|bVC2eR=C%V#uUi4)EgBiw1#xR~qOl1bMna4tw@E0pt!#XyynXSaJgWc?9 zKZiIlb%TN4FWvcNz zHK;{h8qk<#w4@E~=|oq0(2KqdU@*fN$r#2niK$Fy3Cmc)>L56q5_dYAo;S%%J3RAj zANn&0ot?dlp3ds&te(y}|C~9VbLKf`o^$5;)MUl3ozKa8%wrX6SjWa7xbQ9o`Iw>< z$M-KxV+OPFy$jcQ%+nya_zchS2aRY#b6Rmb2rh*rBRMIN{gMtYjbkEHg5Yu%J|r)8 z|FYe`tfR|waNgyG=;*TZuDry{yhdv5_Z8h-=|E??ahNll<04mrAjz|n+$ZTXzTj)z zEy>-I+%3u7lAM*~URPhhIagodb?RWwS6kz(s~ve11lQc>+Kb3@O`dDRai43Rd2J^0 zUw8lO{{6aLy>3^po9lJ=zwY0!JM+3Tue;xk577CIPmuA3j5qAajrsil-`)SlU+Cbb zY&SjgrZaEq;HG=rbmmR>xapZUJ@b~C+>-s4f4`NEjOg^1Ot=33@1A+fGjHd?nYTa1 z`*8a!?CtGQ$a&j$Zp(4|8t#1Cj@@ymJI_*%-}r++sm(cVaR+;O=V1`s)z@9myleJ% z?d{#6LGYjK|7FJP|I0>B%<#WH^kX1HIKdUJa)aAJa8J&Ar76o#{LChH5l;dKg5bV) z|9%mQ;ePjh@BU_d=Kfyxb0`QN`2GX=AJn2Q4Oq-NHW0;@Ab6OSJmjMQA0zKW{XZOy zoqjlxtGM^W$3dX7;88Mq;@*$!$Rpi88q3`vcv2_nMX!(Ldb|qf zKaOD=JA>fK4^+lypP0du>Z~OepLz2C&$#E)EO@t`=H(*_(VpIz$ zB|9@&#&TA%HV8wX3wS(VR1m(Hm*RZJ z7x>PLG3>(~Up$QOyd>*Ob#cd+8qt)^#Iu+E916lzzfg-h_-rblO=S+LwzCtTPn8h( z)qnEw5rrs%oq0KyZTRfVKKn{e3g9!Z_{=MxFak4s#l2sd#ax~S;j6lN^<`coH6xkE z3}!Pg2wzLatN0IYy_SabjKjab<~gq|U~v$>uH)C8{dz8PlZW0o`}I+bWdhE9!?|xb z_YLR1;oLVG(vA*vrdtsD(;B8ug?A#gvr{`e^=M>E?GCA(o!Z%HC7md8KtX(8Kv{w zbmo)J{L&raZ;l6HdUsD>h{6=(Q#?EUO!SprpXnC{VFo!fyhC>0<$ZcEgkhLN2H(q= z4xh=W*NpG+0XoXqhkgvicQPL1JQumbwIF=6DBtoOKTw8vWPS4=PI5X3Guf$3a%S?* zX8MwE@XSmTnT&nNB!4E)&vcu++~-jczGeU4^4zz|QGrSv<_zbM=Ph|MdtWk_LXONn zpSe8#=46h!0S;&U7-(JRYRQQ zco1g$gGMx=Ij!(_Bb%9IOXLKnf-t+h*(*|sD*TG9*<*-f2fmlX{d3r(9CjkdhnP}-f<8f*FBz}mKKa}=pMLT=GoR1obB}!PkV%PJ_mEULb_o6TQ z$$x~ioaYirLHNw@p{J|EH!ISRT{!Tt;i!b0X)NY+B0Q|N8%NTC^+b)mml$(kVixF|Ay{2f0~ zhLKFe=Rcm!ydW(69v@SLPxy>(IIFO|Ei7YU8H>nYBpWgov5!S^W8OvFwTOKzVjqia z4#J|(V9rI&r>J`u)osx+c-MB&*I+8;<_w85cezYe#QUgD%ZKi-5~t5 zE%xct-t@!gKRrnj*SN`@ApEQxzwrluQkx~1|7SY+Oeddh4Z;#SDPd+MG9y=s>~z6? zmC$vGp^U&g^SPb*JS8vTd!PHa&%FzuPhmQ)Ofl5^2DCfAq zWqj{To%wye@XMd6Of_`;<#{f01$X?a8T$IFD?PA_U%CHRGJf@lr$PAjGgPH6^=U*? z4swb!c<$Gig7BNX6z4O(K;Ca+*vA15BgeOT`u08S*th2QZGQe@6Z-siD{(2oE0u4GH>Z%KJe%3IPqT+-Z1%3ac(N}5|qbNj9}KJ#4!eXtYX9Y;ss`RsSs zxfO(^yho+IcfOheUk7)W5jn_qAz3E9}nq9Wb9Cp63Np@hWwY@dxMs(2kBg z3c?>#@**$e-TARPjnUYncy{^S?8WDQ zGVh;0rU;)P|4&m`$YPeUA_yy_;T`0tAV-DwS-=`1*+5hfR?LWhujsiIKOisiR_sns zdee^`=%S*m70tfli6H#h^MCgFpY`~2K?>80;f!PqiQdySDvQ(C(vMiP5sH~gH|Nr-y%;5&k zuM&`qk-Die{}?+}JnoL@!eD!x!Z4^t3S_+m{0X; z{D$vTuZexEegt=}{tqXEu!euD;dwPYuZHK<@Vpvv9N-XkriOn0%)*D{JPg2&aQq&eprY9qf30`%?c|QXs$I zGz;seB?J1XpBZzk@2vXnQNJ0wsjrjzI`rFRVf|>#v3@)W$nH1G!ulsU%{iP||1wEI z*x-5WM1xP!O9Op0&_@IJZ!n0Vj6n7V=GI_3vzW_#%%Q;&cHsSP@H7Y;+J}buD9X3! zp`jic{!C@6QH#1Xpb`3M*c@|c=ryc3?^ye|W=>&?xk`FJMb+~)S8`7GvOM$IG9SMxm_;ct$kpXO&s;ySl5qvmGR z{6P@5NP`Yq$kd_*?%5&&XSSHYOq|hTK8tWx3%l21J!aHmGh2zp*)5LZ???-0wYbkC zo(7@cISc*HS=jPr%%`RKv`k9|-XuHb(()6&Mo)g@Ec6>^Vasw<;aC2k2DLD!mfe|) zooVU5Ezfgyv;i}v(>wpRV%YrXI~{g?Fb_ zdpgmDK8$1DF0zpTd03SIFJE6hHDam8pikYF!=oXx*8a{KYZMsr5C?sZDZT!8vW5(tC&%nTRaNFw$5(* z2AMFMwr0~d2f1)o+k&{O-%ktM7UwfcVlUddU)ws=rx8tQK}XD{t$c00uWftNmtl-& z4rbKWjM|z}+qJC6jM}) zPa>E2F9`jwo5J?V&~^Kiyo5QoH|O?mB6IsJ$lN|Rh4_*mkhy(%Dk6J(ncK_UUhnPe z(U8Wpp(_I!#aQHSKbdLFXAydDZ(rK4#68;Ux4lj~q`-c4D1x&)IJ-k-YEX;1G{9LM zoYg@;9Xeso9n86dId^bY2Q%#8{v93$VaI1lMOw1)0nX_75$@R0Ssl%#)hfF z_jnM5os#h?cB#|5Se&%;-P>Z_QkuELKdzbde z-lYq6rb|CYAafTzcUjIV*0P>WY-1#n-)+M0IA+qE}I!#%)+_d z)?v2Y%(mMOoYPH5-ORRIA}9ElGo0fZkAkqfp1SL&dm7TCr|x>{{x0wHA$j=-``P_d zN>Y*E&{ubTb=OySeRXevzPjtHyL)tZkM3RRi~QZEFc*Dwm%F>Zy6da^2K3ckU)^KS zS9ka5el`eum}8HOe8~?uyNA8%(SXJ@!(R2US3R87!)$u=q7VHUh~9ckW;S-Khn?!N ziD;bH!+AXtIKW}_){gHS+~7$N_DsPGq~cZHAQNx%4%vB^_t06-5`2%{>Zz-q zy6UN`p1SI3w|eTTr>=V1t)9B-Y34oU?>T^R$lG%Xf3cD^m~~IH?it4pcCiQfdmh7# zdnWN92qTh_9CMD)W5g?%bA&lZm~%uHvXO&46sIIQjxgT{nImM5kU2u;2=k4QIYQLgx|Y8!-%-BV>+{IYQ^fj*vM*X1^;LM#vm-8kr+x?j>_C znS06HOXglO_ma7n%)MmpC37#Cd&%5O=3YhkoYGXH3cvCPf6|a9G{>IwvM0Ud@70~b zjAa&!S%!V=<^Amy$rfVS&Q9X-UiGpcz0PrqM?4L}-p}wH&+{6oNk<0kNN?GD+YP^a z8TS4RJ@>X7z2)v*jta=#TkhU+_pU`<^xivy1+2w;-1`!@c@l(uUc^~_oYf~IZ=s_; z@9_b7ab}-_d`t;SQJ&v%MxO>W#=F!QIk{xJO?z?%Rz)jAk5i_npFYbm=!Q!@kQ{&MMra?`C##1U>gXgWP@f z-8YH5+~*Ox?-$}8{W9|nX4tPII_o!>3Cv^;^I61F%(35kHsQ>ETd`;Tbk;8syVdU& z&gu6w2>U;S-Rf_*`oBhM%)5UEGU5FG?~UswJ2<9!_<(|~*wMF#`Q;;aF+F_QsiGN1{~ z=|E?kH=qZ-=)+JZSkb8vXhhd$W1|XHSk;XGf?J%`WdL7fwB*jeW1((ThW&GbfOC~4;+Pl2Fg58KLhnM zQ1*ec50rVJza0b3bl^62vKyHPo+OF8+~*NbgK*F@yvWPE#v7!?J26Q9K{5|2!WVpv z84oh!L1sLt5>;@ALBCT2@5P`-v_{v11~8amjART`nZa!4vVcXbViP(abP(^upuaiJ zzjz-8xx=9A+~f}b1>s;b7@Uf9=zH*o*p0yj_!v7f_)F}`;O~%saB1w!;L7|-Q`+G@ z8m#NV-HBij_GIt~bU%137O(Djfcu5m92hlbdZp*kL#5xIwEAsccJm3ye%Lkm(Eoe$Od&>yLSj)&TB zzw;Unm3?RvWFIQK-**j%cBKb8AFA`A-cN4|9w}n5MKq|HEV-=Di%I=V6nW z$_!>RkH1)nJsIX6!`x$-9Urz0*@vA(&%^F=pGQ0m!r{;GB03-b8s1C4D;o|^kKDs^ zQv~}l{A;|I!|nC((o{m{!+%Bg;Wem9BU+>9;R6_q9T_hB@G;0fT=wCz51-2d79s2K zYdj9Z5jn_%8IN%82u!H1ZFaa`8a39 zQr5B_9gWz`R?K_E9*%OF>)c0IBXl)VS0kT8S0i;bGBvsynSnR?nBQoK_ju$)%x2_r z?9|BZ?7|r%?bJwf8F`$4Im0eOLATz6FDhBaZ2zd-|!vI8(ooKsKT$< zh0%3sP8a&npFs>oH=`#qh3U-1d`9bPv^^fZ9$k&r(P$lwK7#C{bv0V{(Xx-0ee^YM zqN~w%W=slnHAY8cbTlR#vip79aLh*(!Y+>~#;3T;7!f_$bl7bgVMOwTA<1&$%tmxkF?uO&+*2q1s8-o~+%;RRG>v6J=lYN}b<8(bv*W;qtLJTsGJHlD6as&G@?mr#`;rL|O z?eQt`ZjFDLSINy!=yH5thB1lxcz?&QVl8%a{3f=s6X%WJ%YF`WoGaW7!U@ir@Dk=b z;dRoG9{o%(-wEbB!F(rtKpqO?y_z7$1RYJ#(F7e$s6}0LG{H_zXhSo0u8rO?2Kw=S_T{0+`FhqI|+AUV5cUU%S2~Qbk;;WG|_u7(R(ma zHxmain4yed6qA|53N~OS6U}5|EZf<~LCj|2G5+BM7r4pOAep z%Sq-sX+LsLl6#WelTMR_eVY6%sd$w)NQ?Ji@;k^nS=Pz2PX3U*6yZzCQkiP}ju}rj zGJM=WwjHlYsseKvF z6y~yk#VkXvsTc`dO}Gr)E5lUS`NVL*^MW&yaaWPGp`T^Nc*mJVReIit{aHu}d>#o}s@P4Uv6@ z>@#GZ(V1@apcj3RdB%9=pyL_KS;bn`6T>!kvYWl=e1`lpE@HMb%yg!i&U_BpXX<;V z>@#JbDf`S!yiHc@=1iGqeoiTVMDCeC@iV{U9iHhOo>_@n&pgI?u5$~yXWr*g5YCc&mfW-Co|TGMd7U@Oj-F=~<5T3G z^%eS_CHE}3XURS57pn3rwP{HgeU9vN?8}_D_!>Fpbf70gnS`0nS&mNT znCTodo#UK2+t^7wd)d!Hj-#tNI-2t&2AacDLErNhu@rsJ)Au}m&+`t=+e$35&pXN)^gQnYa?dx<`N@!fzTETWo}Y$vWJK@t zbMO%*_>LbagY5HlK413vvd@=&zV~N-JsQxGZVX~Hwdo6 z^L0MoPR>8cY0hyGJ30Rjc5=R*obL_`++l&8T<{|4u_FueQiM;i9}B)f-wR4pjtcxt zW%R#5_65!8OmF((9a=Di;plt8WTr8L+011Nmyl;+7T)7yKF3)LD^Z1C`GY@cNE4c) zqlIl~M|TD=7UwK9--YJ8(0mt~??U}7G~b2hyU=bf+>G-V?!s&r>S*CLZla@w|IehO zMbDCg7qFj;%z4pknD3(OU%Vw^(0`cd(yCoVVC{i=DUFd5d+m_(>2h(bW=NElGv0 zmb^}C-Xb^VyF^Dzendxp!#Z4|qa~H8fisuXr9O=?>m}adCEd}}k}-^D5_(#qrzHzn z!e6XlHD`P@|D)UnBz|wgvU@=Qs%NF)>6g@9J!70vil^fhf_NDjH z`?6p9v|VXc-NLk zA|q}VIgVXgZkLvO@0MTWGPi?ph5cD!4lB%Ig!Bt?cF)r*X~-=d8HGHSS^VE9~sbfMh&}vsXHMr5#vl2UgmFm3CmI9a!nC zm3CmI9ayQOm3CmI9aveG%G5?zD`i=!qm|u>Kvye=GJ?^JWdgcdIh&=dB^Euc)YD2m ztvtft=xL>0T6qciSIWQAd{;gQ!d2$GDm8jqrKeSTT9uRc(9bA(cTHe*Yk$3d~ z?B;5Hub#?0Rw3_dc~{H3THe(=k$1JctL@5ac~_sney-N{>RUm$CLkp*@d~e#hRkHe zuB?%LjqGc3Q;;vP8*8dklRDI=5lv}_Jy~N<*2um__BFa*GZ_1^W)_QC#tK#w$rfVS zj_hmV@%LtpuGgF)Sv;4 zX+{fLqxZG4uN{b<*Un%eOZbbGtYI@-i9_zSyO4XW&e#6SRsQ4uxgQ5%q`o8Nk9-Na zBlR7bnsj8~9rPS&HzGf$6hBgypO8ON?nt>KYoYtd`ZS~!_B%3yVT@-IQ;|P%Hu6Wx zA1Qz2YV;my*CXvrq}-8za|SydX-6V;9(kSnJmP5(t_yh*QXSle~OHA&T$` zUsDo0zRo??xyQQlRHYX7d|e0RUZ?MMJ?YI5hBJyWxXZeU*qwDt(EU1@*U7vtfdd@o z7^gYMMJ{6o>)c_TJFJ&^{TpQBZS=j~o~+MBehN~Uq7>&dBR zzFu~}aUQPk$3O-%j1kDZUgq_Sh-3?~Y-bmHID+2SALk_UuRj}v8v@=%pBsLo4t8`y zdz`mHM;mmsK}Q?LGJ%=QVLl7d)rMs_dxNt#?Bytl_*<~y6#qX6K1|CE7{3OUWy{~CYd+= z#LrZw8nviP0~+Bjo0?HY}f6$2b^g!n5z6@Y6`i{0E(UX|Mblf3&5s_>qfn)rG+|j2whuqO}N6Q_p>*xnO z3c@YVVK274Np{}leRRFWer&PRTfCQBO7I0=qx&tgZ_)V{9dFU`7JIR!6>aH2PkPf2 z`M1cwWhmpAfzG$A#e2DB6ZT_E4Dq5i^r#xNec5;GN@$1KDhV%#BS1*=($?qhbM^O%3RL=xAz#a*5T zVeB*5msod*b%)rONQ3OLxhY6tit#CSJ=U(rx~+SH>VO=wF8I-~a;J&0fs}4M^?>Ngf z9tGh}nRhe#ZCwLUn3lH+FglciQQlc6z6s-f5?I z%Dl51GVdIKUEVpJ1uSM6D_BhwTZl#OopSG#d#A2vrMWF#jgFOi1yyoubq zvLN@a+!VxK?2>s`Y06Q7O8mi})W)9dvM0OTVV67X()F%^$h>PT6R^{}rZJyI=zZ5; zti&C5xx=m<97g6{r#Z((u5br?vg;v_gK)RI?0$w9k$<`5ByyGuTqcR@_*=4D z_W0*`9eu}VCM(&oBk}q0PR4&s5kBElY{d;D#l1mPY#z9$WKdygI2lLdY6$wh8H zMAv(YQ-Uv%VUN!CnE4)^?U}>VAl&;BZ;+M@WFi|md5;e;$Gz^q_Y2HxZ#Dj;HuY#o z3-qv84}0~nw=;ck|Gnn5*PZv?;x6~`zV3B?!ZVmbf;%O=$jf+d6H=27cT8}{1b0l3 zD?zRVxf0|`$je6*LcRp^N%#yo6TYS--&2}$RG<=7_?21AWdV!v4kxT&HTEfC17?)4 zg;>lg!K@O@D#5H0%qrmse`96|W|m-P3FopcY=HGbMJlb?Y}t^ z?sM;b?!E6-+;!jEyh9GUFaW!>PnY{f;JN!A;92`TYrkjhFN-_v|D77tLjL_bka@q% z`(-}xK7}btaY|q>57>zVvzf=jAUv3yH?U_1)AJ^td$0-3Xh|F5asPw=aFWwOcu4L; z=5nYY&N^fl4~<3NhwR;<`78>;!!MAI3}nK+54Wc`eR1!@gE`Ah?Em5aaQ7oWQiW=` z^AXvOnBS3&nAs6CJ7Q)>Gn1PS$wvW(Glglm_feUSJ&V0N=FZ2`BGWM)9&3)f9&^`Y zvK`xxdmcN5T*od1;oo0Vo(fb#uD_Qf$KTP!uq_A^U%@>SWlGFSb~@s&iTyF-L^Dpj z$X)L7kS9U-kLUhVliJjyA?EteQS|@MasCa$<7Rri6hBY~xsIFZ@l~v09UFu2g!`Sy zNiOtrA`k9*q6^*Vi8-7|z_U-d^9i#(aV`i?mg6`6;7?>axe0kr>gVJk zs^XnFC-b=mv_z-p1|#RWu}okxa-N%u9XPibJ)blCbGL%(G?W^k*0&8N+y{F_SsWM@Q#ZqnGpX*tzpMIj@uRIytYC^JaPe z4)=J-;~>27JZX6wyLQ3*ap7Z%Qk<{&mQwsk8LDA^7wp@G4#;?+8+Pr2_vFG5hNGhk z@?Mblf|*{>(FJq75J?eqqR)%^ytoK6yr|QQ@?PA4ZZAf& zm!q5`iR;LD@h*>p@KQiBlB3^CsmaRw6vq2;$qrny1D8r7>m@sI$qrolg(}pf8ST;O zr9liu&P$`2guIt#Fq^q}KQ3)z8wW|`1oq&PJ-Bp*YuJNJ_TbWgL3laj1=5ijIWK4D zUGk8hg2;W@PF$Axa%tqeTnn9EZh~F7+zOpu?m~AWko~fKxI7l~zr2`btY9^|y}X53 zyw8_+5|55A>-e(Vmu0?e2d?P!N^ZHc7@L8Ixp8k};_Pjj<0&vL>~`UL?t!)D@YM>}Qhw zOwwhNE|a#ii#_baJ|xMR^f$-(mouE_681Rh|M_omm-{^8X%Jq0hUc)$S6}31Uga&` z<{fg-fj;Q*>Ojo>>P0;7>OCIvBnYqR;95D{`lS<5;$qTB1K$x1eI@*Yib z@9SOYjvc!`j1idK^>O@zJ6*rRHSGJ1k8$peZ}^TMknhGsCNqth+(5>g-r<|cux~f3 zP=|UnqzS9oLJZs38HBgqAPf4qrH@;=FrQnxxHT9#Z;d1oz1+IYRh)VIYsyoBN;vcO z6!da?G0U)nw;u%IofM?xC0@bj@6@C=GTxE#&KhL96NeqWvpWdy%5^s{`LUaK?dIKX z3}PxXn9V$#dv^(cu@cX_Yc_Y|Nnk&A>h58Vq4T>ZImKD*)ql^Djgr*IIsYxkcmBH@ zg!j^r88g0@9W%Zs-@PJy!e@MeZti`{ci5ME_T^qRey0ZRe$PGbdH%iOjAAU4aIbsr zb#E5>ySJJsHnW9YxYNDAk^kOl&S5t9%;w$=ZUy1}XVCS19p2A@+1}4h9`t!%pZE27 zzZjolzwevneR=Qyf_dKml|PXAzRdUQU|#ooF_UfB+XpY>c@Nz4L0d*+7amNJ#Ucca)OG4Y>h7iPE=-LA_nCX| zto6+NXufmyyZ1hsKQk)>7{n0fp~n}lXEp1%k*(OL3p+VX9;dh;J-;B!1z9er=fd;6 z#4G6a1-og4IVuoARjLz>To=`Qu`cSo*cN@i*pW24VE-@nAd}vx`{Fd#Qiyvm zy62Mo{bfh!g=|L}ga|M&RhNWD~N>;I!4cy39wv$T%g&gN3 zMU-+UcXJ;P@D}HSuv|Rdn1u7no!|^#@iV{iU;YfjFrW&NL=i(RYU7-+Db0~9Y(slw z3v-yrG-fh~E4hlr$QHUglqKBFehzYkqsS7TLYDAh%J_hf_>?dBn(z3LU-+Frg0Q@L z%d5A%ddo*slQ`5{z8+>#K9R=geR*}3*W2>?SY98>JFkMA71}YBF^osQDokN6=2Kw_ z%ea>7xSlO+V<)$=hrQgx6FkdvyvS?3$veEqH+;{zAgox9O2|@CmWt}BSc^K;Mb9gy zF@Ql_&M-zWnsLZgQN0zXqt1#euuBzhU^VO5$Y$(QMf+4y-4%;?0lQGiy_MWkN&ZT{ zsdSh}kfV|umE@@OGBQ;9gwOdBSt^-Lr62g4f4LNd5g`?cAer`brYo1xi$3&cAbKBR ze z{KT)=r^@!Ha$APu-pcN&{20EeB1@IVw4gQirAi07BSRG#s$?^S9Ijw8-b59ds?1?N za#h*NUJi1Ee2#O9V&tkKM-@4$JkN`~$4AIeq%Coz|5&Z+hWZzET=5BV6`s$C4i$ZD8HWGuC*PeU3bTco=qWr@r} z-y`)sQr{!>JW`fOdlV^4CHQR`#%u0~{ugqo_Ahy^-pTe3Zv|3iU=lhj$Wb z|07@J#~`e(57q5Kb@x_xPj&OG?wjheRF@+vf~uHHR1M;hAu5>^%qOZPtudph%jk(* zQGFSJY*DjW!V0cuH5<5*t;iN7OOz~8$2rM;Jd7MsPas2-3{fv4LzFiW^&J<2utq?6 zDp3WwYN)qHP2x~@4RzLNgS<65(1~=qk%78vsJliVrm_Y-t>NAp?uje9vPy|CE8q~Wr>!>-**b53%G;3ktzB?%qLo|=nt@u(Pk0-3%~Oh=eZd8uP@|? zkt3!)3A8}3V`PX)L;W%8kI6*+G2TSXB(6r!W0rFrHy~4tT4Oe`1vSSUAs<;|j&Xw1 z*#8)H$K1ue*sYj1QDeA+;hChE0{-|Tyb*6eaQFd zf1F*4yAXu_{#IC9p4#%%*8AFeUptmKl4(yL`jgENav07i%%JuJCZpck>aD$;6{xwk z+_mk0?G0?gPSw5%eXo56_0(~19rx53jc@A6Qs+UQW%*okD=~(b;qka{sTV3KE;2An&ZDlz41R_7V*FGdl1&u`?~6^tG9LavF?7HSJxia z3-K1}wV*Za=skd2+HcRAykg!fTz2D9-->X}2mUD%s?2RMxPQ12KgIn6^n z!t>apdav^q?{gM;>d8}2?e#7OVf`5FfBibtBY{RVA%#?0q3`vxP;-4X*H?4>L8!O> zFl4W97WKV{`g%}*HxJ<6`tE6vjBgsq(m;*|JMj)0>|rmt$k3pO67J+~?&W@-<_vN* zc!^h$t-;Uy8-xkvs6Ygf)SxD^CCHK>OF~;NqZe`{^heJVhH?cXn9fzKN1X{<*v3xO zo1oqV{ZG*U1pQA?XTm+$r385s^gluW6CUSD)SaO2gy;Aw2pih#hI-mi?;0+`c@6Dg z!*}^J2onR!Q;Eu$PolXbnoD9LNhBjrVsq?OVmjT(Ad}vBABod&N8%Eea~-Q$$3|}C zHtcKSG3-yG-Y3eGC{Log61|PYN73`d_xX`u_?L6FLy-GHtWZ9B? z;yomrNAd_pGl9uWL$+jDl4VK0mW}9rvfd}(j9pGvSF$Y0htTt6JCyt)ukbo=@h%_m z5uYMovbvN1;R2U}u&J7xs<~+;st`#OF~p+xO^2eMrtWR(o~HVc;+qs%QhJfi5OTPJ z@yL*3J}KstVm>KXaSeK!B3H_KHnD|mm{p3MN-5!C9_J~Z#V)11%xk=X8Kjs&iYzJT zFoP5`NRg*m1@yjIRb**apXPL@2kLFshyJL$nYx>)yV-C?F_#4_M9t0A+-xc4(Cj+w zRkPKs#U3?#ginL8xqdWnhVz*HSI{F3v#7qFqpASVk-KdDod&?sf$ok>Ke9_O96I2^&~~8HT5p; zMa`*dPSx{NJx|s1RQ0C5!~2{?y{YDqY7VL9&_dr^G(lfmxVMFSTHK9qTFBBej)pWQ znH1V0LrWQ2_MjJL)3P4}7|9sMBUekgT24ofmd?W)SIa`~;eMV*&s)C8 zE4+!CTFTM#L;OB!`BxCO@;j(i6fxAI4)sW&5wf*XbE~dghI(6Nl0{$C+iDO)$l(g~ zz10r%wUv8Yxu>-rwDwJFSz61{dJEgw$t~QD46S8oeH`;?eVSq(;t?L_DW0VaGid!A zfAS9(g0M|UMJnShw5dx2n$m%Ex{-l;+Vn++Hr_-Vd(>tYE3ofv?0cJaY-BSxAy*sq zw%Lt3+nmB4wJG5a%)ZTiJcwOt^C;?WbC!RDux%sU+txj8XlE|%VzEc<%%+_@?dsE#3B$(!i(B5(07 zA7H;a$z3PLch~Wd6H)+;{{&kHQwYMWJ^%Mbj@Ib=zfrHi_|=yjLow4@E~=|no+ z$RLy6j6%I##xs%0Ohdh0xX>IXMD*wd>4e>BB(`O8jwgb-f_1Uw8GupdLT=;AzZ;o#vn_# zDd>GSS-LG@EnC^n&DfuA-fy=f0-PP3Hx!s-H zeLeQG`xc(VoA3TPX4T!Sx|>z^pZSgd2H|BP6{$=$qKHA}%WBh?r5xpDE(T$S%o%BP zLG}!D%($Fkj9@h5kU8Tje4k<0GFGw%HD+wY@4t*4>_OIyXZQfU&roZI{%5E+!#-vF z!+9g`dV6jD)JkG6C`jXlhyM{oKukiiT^tv&RsM-lG_Vb3ba)6?vF zPG%Z2nZtap#LxFMx1Kw>6?OOAi{A7+iL-i^au4>cr@8esx1P^)HV8Azq4rF(%QUx4 zbIVM^E@i4cQ|*~;NXL86v^$x0CsQ9YXEB!rEaDpUHB(5R<1FJmJ+IDp>uR$p(m z_13%IccI4K5AZPh*83CO*~j^PoZrX!eVpIN`F-@LPixxXu0Gj}W*Re@gT3mrkgK_# z)#zIvb@bWHO>D7QO7M0DnEwFtAMhwo z@DyJK;XpYC*24S;Hlhhlapu5|bfPmkOk@^wS%9}T@ETU)9S>ZKz7EuffqF3T9^}ie zNfzdkeFf%{J%;f(Gus|z>tD7#%C<+@8*xUqEZO>(t$*42mwlAuoWfkPAL1o`G>w_eLGK3X*&w|d^bBRZfSnxl7Vq)_ zAMr^L4z5UJ%wTX2dSNdHyJzrlrZ6478?1MO^=`1<4c5ECG7pw{u*`#H9;|PJx3L2| zIrv^)oeHnn>=3LG&MlhOjn0JnT0lmrbR&%`79DT{z#ckM+9Q%=DKXURZ#Qbtj zQ-V3>*uxy}F2}y)Ji_BV#j`xei@d_?yoLGZm~YNUe99Mm&3F9BFZ|9Q{LQ~y3c_I_ zc41g$su4vDwWvcq5@ z2Y8rAd4i`o!}GkvtGvP6yvK)p%x8SbH+;`e{K`50UX^o{W%bqhH*V_yK!1 zQcp(7I!eY-O;G3gF(!&S$ULSGLs0veYgow**pD%#+{w$l!+ZR}r63$z4m&^Au8%dB zvCbLmoUzUsJD(-UFxEL^oio-sV_)KJ-sP_#9OsO2c43^^j?=$!@{H5>abp?JTJ(P0 zHcoLj_wq6JY}|K2INsijuTKKK(ZBKLH(oEsufe+*e;o50ZwBLK7;jg{e-VTeLaI?6 z{hZ+b3F(Z)`MWr8ddWSQWc3C@||oQajFK@9EbhP|9Pi$z?GcQ?@< zOf<`h`Y=%+ChEh)e}iyRMIuP01D(ibIBJ=+5P2r8ARlK;D&`r?X40$J=SgNe>9-)9 ztd7a`Xh3Ir(u=uVgV{{p&TZ`DL7wDkE(YNgwM?l(JG#;xy_n+rDP}w6X7+P{$0*}@ zzULf&1mVS8ui^>k_`S?I}BJ(;Q}Q`In44O7)HRSi?sFjWmxf8f9T8HCfkuW5QZ z&3;VFVJzcW#f@y?BxXD99zH`I(|!!X>H08TAExWWbn}?*ZBOsd46Z~S)6HeN@2BhO z^w)Tw4})+@4vnW2nf9M`d)jg;~L4d7{-=D0U0WdYf_U7&AcKAk zz^oQ{FAJ8S?ge=iaE=RH3c@SBt1Gk7-z#^pm;JnpJFb*%p*bux!-Xk0bD=XAI&+~q z7Rs_vmWAhoa8U*9%_4nYB>$ojIBQWpNBIhw7yTN9S2d&=sZ3@L^HASaMaX*9Z#efV zwO-wpE_B08uQt=G&GhQWDC2qLxu!Bz8Nw*WuoL;Oxt;g;j4y(4aRM?eZjKrjt6{Mk z79Zv~C;5o4`8Eib#M2lvUNW2sOyU6YEjh-!=-U!~Tk71U?XgcwuVpRkd4y*v!|#Y? z0U_S_vcA~eW%4b%kuBVZoXegF!sX?VZ+Rqx(BtKDF1I7gcVkDFzk%NY%hi5u91Us2 z4QygFFYz|&T~Q0YSdqv?W}z1=%)sAF4p*4-ihqOfx{5?FfMMvtb$0r?!{l<7ulObi zSIV+dmX)%s+{!Ne?8+B;i+6(X`nn{MjGelEC2G9>W&Zci&FY5c=-~}|c*8PQqmMVd z%K!fP2SK>XTvj#4Tvp9z34V6fle~betM$R(tqoV}!|K`iJ+j)JtDizmtKGRqJ!{mn zMm=k;!>rb9#P@6TYK>m4_0QL~q%|AZj{RDz*K5DVo~%ow2buVpb$(`@pIPT;*7=!r zerA0u{M`Cf9*rMtL{NyKydZY?OK9BRor45N?X1 z4l-?;z)a-cRKoo{h?(AKrZ+|~gi(y)5XU&d_nhO8Al%%YKJ-Jb&2nv)YqP#={*=#y zaEtzI(Vs1?S;0E|?3ULtt1V}PaI0BvwI5scXzOm=xAh1=;-0O41>sGXGn%m!P=wlV z^3QL&6olK#(I3Anw_QOo_whgwZjVOp?e)>S?box4H*x>=kArYWEDdPL2qrR_!yM-% z=Yw!(1BcgvSScxz|;+^xOXgdX2|3-9m=p9SHrPGsQccKMlIerA`S+4UjL*!6V~?ruvL z{Lb9HksaKOGj`j--DbN-y?gBGo-E8^&oRt>&&43T&0gGAg$c}LHuv*5PX^)MS|pH& z_q5l%_pan&p61yg+*b`hx34w>F{^$0z0dE2ee&{+U zFpNp4;m~wuaR75Zq=rM@&Y?T_lHX9nAw4;yCx`9O;mc6NVf%Qv59WM$6KXho6FYc{ zS9ycCd5;hI2*Rnp5sFR-?zcdYtS2+=Cn; z53|cHMBj4n;rWc`gQFUO*i2)T2IX%(J_Bb}FwuJ<#JkJu@Zeu_Cov+{d`kik#^X+E-9mtV?2K%1>A}^u-e7(+Bf4=(j)t|5a{2%x!2n(vB z{sQ$E*oy)^FHnDh+6&T9e}Vc7>_vh43-r5S5JRvJ1?n$Qe}Vc7W?>Em%h3CR9Taek zljwPYeix{-K)(wf;9(x+3Et*CKI9|RT%hIxH5aJ4K+Od|W0wnl=Ry!3^^T7w(gk%L zoyKyu;djW`S42DO7Xe2JCdMO@7i%;eNr zzC%r?%<$KH^jK@w6VE{sq~J+*wqO zC}OBhT^i7kW~jZWE0@s|zdwrllg(iCq-YXTnZaBZAcMb;5*DpO?xIa>VF$Oc8@o|d zzzI%M!rk1*gFKA=DDobQWGa%W=v`zglBvjU6#a;P6#bXKxDbTJcA{8Miz`x-I@H7L zi<4+dbL>HJIZge5YS$W&rxCHABwnpn)Nq%QTj9Q`j@ zhy5zi-;(_trRG|?o4vTV)P1E#Ifd+{?kaUxsk=($E_GL_yGq?v`VQ}*2c@!?p5r_hgYb@W zRKTv@5k(BOko^uby(1+EpM4$YJo_m>@hi@F_D}xdLJ*$O!!y;e`oC88MU2J+nFyhlQYgQQ&X9Fl+{2zWzH*$$F7w%qXn&KO9wiUK^A>* zSJ?oDF$=RRvtMP4uwP|Mxt5jaciCDtu!)`6x3b;A|NU15|Hte9{p+&7{@;K8|9=n5 GZu>tLe$lr8 literal 171250 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 T_n-N%{=fhH|NlF@TmJt5K_JA9 diff --git a/Sphere2Go.xcodeproj/xcuserdata/axel.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist b/Sphere2Go.xcodeproj/xcuserdata/axel.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist index 3ce2f9c..f607aeb 100644 --- a/Sphere2Go.xcodeproj/xcuserdata/axel.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist +++ b/Sphere2Go.xcodeproj/xcuserdata/axel.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist @@ -1,11 +1,13 @@ @@ -38,6 +41,7 @@ @@ -54,6 +58,7 @@ + endingLineNumber = "4" + landmarkName = "unknown" + landmarkType = "0"> diff --git a/Sphere2Go.xcodeproj/xcuserdata/axel.xcuserdatad/xcschemes/Sphere2.xcscheme b/Sphere2Go.xcodeproj/xcuserdata/axel.xcuserdatad/xcschemes/Sphere2.xcscheme index e76a3af..bff3ba1 100644 --- a/Sphere2Go.xcodeproj/xcuserdata/axel.xcuserdatad/xcschemes/Sphere2.xcscheme +++ b/Sphere2Go.xcodeproj/xcuserdata/axel.xcuserdatad/xcschemes/Sphere2.xcscheme @@ -1,6 +1,6 @@ - - - - + + + + @@ -39,17 +48,6 @@ - - - - - - - - - - - - { public mutating func pop() -> T? { if heap.isEmpty { return nil } if heap.count == 1 { return heap.removeFirst() } - swap(&heap[0], &heap[heap.count - 1]) + heap.swapAt(0, heap.count - 1) let temp = heap.removeLast() sink(0) return temp @@ -60,7 +60,7 @@ public struct PriorityQueue { 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 } - swap(&heap[index], &heap[j]) + heap.swapAt(index, j) index = j } } @@ -68,7 +68,7 @@ public struct PriorityQueue { private mutating func swim(_ index: Int) { var index = index while index > 0 && ordered(heap[(index - 1) / 2], heap[index]) { - swap(&heap[(index - 1) / 2], &heap[index]) + heap.swapAt((index - 1) / 2, index) index = (index - 1) / 2 } } diff --git a/Sphere2Go/R1Interval.swift b/Sphere2Go/R1Interval.swift index a11134b..35e834c 100644 --- a/Sphere2Go/R1Interval.swift +++ b/Sphere2Go/R1Interval.swift @@ -38,7 +38,12 @@ struct R1Interval: Equatable, CustomStringConvertible { 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) @@ -51,12 +56,7 @@ struct R1Interval: Equatable, CustomStringConvertible { func isEmpty() -> Bool { return lo > hi } - - // Equal returns true iff the interval contains the same points as other. - func equals(_ interval: R1Interval) -> Bool { - return lo == interval.lo && hi == interval.hi || isEmpty() && interval.isEmpty() - } - + // Contains returns true iff the interval contains p. func contains(_ point: Double) -> Bool { return lo <= point && point <= hi @@ -176,7 +176,3 @@ struct R1Interval: Equatable, CustomStringConvertible { } } - -func ==(lhs: R1Interval, rhs: R1Interval) -> Bool { - return lhs.lo == rhs.lo && lhs.hi == rhs.hi || (lhs.isEmpty() && rhs.isEmpty()) -} diff --git a/Sphere2Go/R2Rect.swift b/Sphere2Go/R2Rect.swift index 1e0729f..cc5d472 100644 --- a/Sphere2Go/R2Rect.swift +++ b/Sphere2Go/R2Rect.swift @@ -22,6 +22,10 @@ struct R2Point: Equatable, CustomStringConvertible { } // 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) @@ -29,16 +33,8 @@ struct R2Point: Equatable, CustomStringConvertible { } -func ==(lhs: R2Point, rhs: R2Point) -> Bool { - return lhs.x == rhs.x && lhs.y == rhs.y -} - // R2Rect represents a closed axis-aligned rectangle in the (x,y) plane. -func ==(lhs: R2Rect, rhs: R2Rect) -> Bool { - return lhs.x == rhs.x && lhs.y == rhs.y -} - struct R2Rect: Equatable, CustomStringConvertible { let x: R1Interval let y: R1Interval @@ -91,7 +87,11 @@ struct R2Rect: Equatable, CustomStringConvertible { 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)" } diff --git a/Sphere2Go/R3Vector.swift b/Sphere2Go/R3Vector.swift index 7dc5401..f749d02 100644 --- a/Sphere2Go/R3Vector.swift +++ b/Sphere2Go/R3Vector.swift @@ -8,11 +8,6 @@ import Foundation // package r3 // import fmt, math, s1 -func ==(lhs: R3Vector, rhs: R3Vector) -> Bool { - return lhs.x == rhs.x && lhs.y == rhs.y && lhs.z == rhs.z -} - - // R3Vector represents a point in ℝ³. struct R3Vector: Equatable, CustomStringConvertible, Hashable { @@ -32,14 +27,15 @@ struct R3Vector: Equatable, CustomStringConvertible, Hashable { } // 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))" } - var hashValue: Int { - return x.hashValue ^ y.hashValue ^ z.hashValue - } - + // MARK: tests // ApproxEqual reports whether v and other are equal within a small epsilon. diff --git a/Sphere2Go/S2CellId.swift b/Sphere2Go/S2CellId.swift index bbd879b..6e87781 100644 --- a/Sphere2Go/S2CellId.swift +++ b/Sphere2Go/S2CellId.swift @@ -16,10 +16,6 @@ prefix func -(id: UInt32) -> UInt32 { return UInt32(bitPattern: -Int32(bitPattern: id)) } -func ==(lhs:CellId, rhs: CellId) -> Bool { - return lhs.id == rhs.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 @@ -29,7 +25,7 @@ func ==(lhs:CellId, rhs: CellId) -> Bool { // The major differences from the C++ version is that barely anything is implemented. struct CellId: Equatable, Hashable { - // TODO(dsymonds): Some of these constants should probably be exported. + // static let faceBits = 3 static let numFaces = 6 static let maxLevel = 30 @@ -86,7 +82,11 @@ struct CellId: Equatable, Hashable { } // 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() { @@ -98,11 +98,7 @@ struct CellId: Equatable, Hashable { } return "\(face())/\(s)" } - - var hashValue: Int { - return id.hashValue - } - + // MARK: string token func toToken() -> String { @@ -426,9 +422,6 @@ struct CellId: Equatable, Hashable { // return CellIdSequence(parent: self, level: level) // } - // TODO: the methods below are not exported yet. Settle on the entire API design - // before doing this. Do we want to mirror the C++ one as closely as possible? - // rawPoint returns an unnormalized r3 vector from the origin through the center // of the s2 cell on the sphere. func rawPoint() -> R3Vector { diff --git a/Sphere2Go/S2CellUnion.swift b/Sphere2Go/S2CellUnion.swift index 68eb349..2ec9281 100644 --- a/Sphere2Go/S2CellUnion.swift +++ b/Sphere2Go/S2CellUnion.swift @@ -30,7 +30,11 @@ class CellUnion: S2Region, Equatable { } // MARK: protocols - + + static func ==(lhs: CellUnion, rhs: CellUnion) -> Bool { + return lhs.cellIds == rhs.cellIds + } + subscript(i: Int) -> CellId { get { return cellIds[i] @@ -223,7 +227,3 @@ class CellUnion: S2Region, Equatable { } } - -func ==(lhs: CellUnion, rhs: CellUnion) -> Bool { - return lhs.cellIds == rhs.cellIds -} diff --git a/Sphere2Go/S2Loop.swift b/Sphere2Go/S2Loop.swift index b355653..f33674f 100644 --- a/Sphere2Go/S2Loop.swift +++ b/Sphere2Go/S2Loop.swift @@ -63,7 +63,7 @@ struct S2Loop: Shape, S2Region { // constructs a loop from the given points init(points: [S2Point]) { - var vertices = points + let vertices = points // create preliminary loop object with empty bounds // var l = // figure out origin @@ -159,7 +159,7 @@ struct S2Loop: Shape, S2Region { // constructs a loop from the given points static func loopFromPoints(_ points: [S2Point]) -> S2Loop { - var vertices = points + 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 diff --git a/Sphere2Go/S2Metric.swift b/Sphere2Go/S2Metric.swift index 6fc9d99..d0f47c2 100644 --- a/Sphere2Go/S2Metric.swift +++ b/Sphere2Go/S2Metric.swift @@ -35,7 +35,7 @@ struct Metric { // Value returns the value of the metric at the given level. func value(_ level: Int) -> Double { - return ldexp(deriv, -dim * level) + return scalbn(deriv, -dim * level) } // MinLevel returns the minimum level such that the metric is at most diff --git a/Sphere2Go/S2Point.swift b/Sphere2Go/S2Point.swift index 549d1b1..b23dc29 100644 --- a/Sphere2Go/S2Point.swift +++ b/Sphere2Go/S2Point.swift @@ -57,11 +57,6 @@ let detErrorMultiplier = 7.1767e-16 // // Fields should be treated as read-only. Use one of the factory methods for creation. -func ==(lhs: S2Point, rhs: S2Point) -> Bool { - return lhs.x == rhs.x && lhs.y == rhs.y && lhs.z == rhs.z -} - - // S2Point represents a point in RxRxR. struct S2Point: Equatable, CustomStringConvertible, Hashable { @@ -146,10 +141,10 @@ struct S2Point: Equatable, CustomStringConvertible, Hashable { return "(\(x), \(y), \(z))" } - var hashValue: Int { - return x.hashValue ^ y.hashValue ^ z.hashValue + 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. diff --git a/Sphere2Go/S2Polyline.swift b/Sphere2Go/S2Polyline.swift index 4270702..5be6c77 100644 --- a/Sphere2Go/S2Polyline.swift +++ b/Sphere2Go/S2Polyline.swift @@ -9,7 +9,7 @@ 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 { +struct S2Polyline: Shape, Equatable { let points: [S2Point] @@ -23,6 +23,13 @@ struct S2Polyline: Shape { 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()) @@ -56,19 +63,6 @@ struct S2Polyline: Shape { return centroid } - // Equals reports whether the given Polyline is exactly the same as this one. - func equals(_ b: S2Polyline) -> Bool { - if points.count != b.points.count { - return false - } - for i in 0.. S2Cap { return rectBound().capBound() @@ -146,7 +140,6 @@ struct S2Polyline: Shape { if i == 0 { return 0 } - return numEdges() } diff --git a/Sphere2Go/S2Rect.swift b/Sphere2Go/S2Rect.swift index f6bb71d..c32c861 100644 --- a/Sphere2Go/S2Rect.swift +++ b/Sphere2Go/S2Rect.swift @@ -73,7 +73,7 @@ struct S2Rect: S2Region { // IsFull reports whether the rectangle is full. func isFull() -> Bool { - return lat.equals(S2Rect.validRectLatRange) && lng.isFull() + return lat == S2Rect.validRectLatRange && lng.isFull() } // IsPoint reports whether the rectangle is a single point. diff --git a/Sphere2Go/S2RegionCoverer.swift b/Sphere2Go/S2RegionCoverer.swift index 05e28c2..23a0711 100644 --- a/Sphere2Go/S2RegionCoverer.swift +++ b/Sphere2Go/S2RegionCoverer.swift @@ -164,6 +164,16 @@ class Candidate: Comparable { 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) { @@ -173,14 +183,6 @@ class Candidate: Comparable { } -func ==(lhs: Candidate, rhs: Candidate) -> Bool { - return lhs.priority == rhs.priority -} - -func <(lhs: Candidate, rhs: Candidate) -> Bool { - return lhs.priority < rhs.priority -} - class Coverer { diff --git a/Sphere2GoLibTests/R3VectorTests.swift b/Sphere2GoLibTests/R3VectorTests.swift index 774fc45..3b03073 100644 --- a/Sphere2GoLibTests/R3VectorTests.swift +++ b/Sphere2GoLibTests/R3VectorTests.swift @@ -22,19 +22,19 @@ class R3R3VectorTests: XCTestCase { } func testNorm() { - XCTAssertEqualWithAccuracy(v(0, 0, 0).norm(), 0.0, accuracy: 1e-14) - XCTAssertEqualWithAccuracy(v(0, 1, 0).norm(), 1.0, accuracy: 1e-14) - XCTAssertEqualWithAccuracy(v(3, -4, 12).norm(), 13.0, accuracy: 1e-14) - XCTAssertEqualWithAccuracy(v(1, 1e-16, 1e-32).norm(), 1.0, accuracy: 1e-14) + 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() { - XCTAssertEqualWithAccuracy(v(0, 0, 0).norm2(), 0.0, accuracy: 1e-14) - XCTAssertEqualWithAccuracy(v(0, 1, 0).norm2(), 1.0, accuracy: 1e-14) - XCTAssertEqualWithAccuracy(v(1, 1, 1).norm2(), 3.0, accuracy: 1e-14) - XCTAssertEqualWithAccuracy(v(1, 2, 3).norm2(), 14.0, accuracy: 1e-14) - XCTAssertEqualWithAccuracy(v(3, -4, 12).norm2(), 169.0, accuracy: 1e-14) - XCTAssertEqualWithAccuracy(v(1, 1e-16, 1e-32).norm2(), 1.0, accuracy: 1e-14) + 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() { @@ -42,9 +42,9 @@ class R3R3VectorTests: XCTestCase { for v1 in vectors { let v = R3Vector(x: v1.0, y: v1.1, z: v1.2) let nv = v.normalize() - XCTAssertEqualWithAccuracy(v.x*nv.y, v.y*nv.x, accuracy: 1e-14) - XCTAssertEqualWithAccuracy(v.x*nv.z, v.z*nv.x, accuracy: 1e-14) - XCTAssertEqualWithAccuracy(nv.norm(), 1.0, accuracy: 1e-14) + 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) } } @@ -71,8 +71,8 @@ class R3R3VectorTests: XCTestCase { for (v1, v2, want) in tests { let v1 = v(v1.x, v1.y, v1.z) let v2 = v(v2.x, v2.y, v2.z) - XCTAssertEqualWithAccuracy(v1.dot(v2), want, accuracy: 1e-15) - XCTAssertEqualWithAccuracy(v2.dot(v1), want, accuracy: 1e-15) + XCTAssertEqual(v1.dot(v2), want, accuracy: 1e-15) + XCTAssertEqual(v2.dot(v1), want, accuracy: 1e-15) } } @@ -119,8 +119,8 @@ class R3R3VectorTests: XCTestCase { for (v1, v2, want) in tests { let v1 = v(v1.x, v1.y, v1.z) let v2 = v(v2.x, v2.y, v2.z) - XCTAssertEqualWithAccuracy(v1.distance(v2), want, accuracy: 1e-13) - XCTAssertEqualWithAccuracy(v1.distance(v2), want, accuracy: 1e-13) + XCTAssertEqual(v1.distance(v2), want, accuracy: 1e-13) + XCTAssertEqual(v1.distance(v2), want, accuracy: 1e-13) } } @@ -145,8 +145,8 @@ class R3R3VectorTests: XCTestCase { (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 { - XCTAssertEqualWithAccuracy(v1.angle(v2), want, accuracy: 1e-15) - XCTAssertEqualWithAccuracy(v2.angle(v1), want, accuracy: 1e-15) + XCTAssertEqual(v1.angle(v2), want, accuracy: 1e-15) + XCTAssertEqual(v2.angle(v1), want, accuracy: 1e-15) } } @@ -159,8 +159,8 @@ class R3R3VectorTests: XCTestCase { v(0.012, 0.0053, 0.00457), v(-0.012, -1, -0.00457)] for v in vectors { - XCTAssertEqualWithAccuracy(v.dot(v.ortho()), 0, accuracy: 1e-15) - XCTAssertEqualWithAccuracy(v.ortho().norm(), 1, accuracy: 1e-15) + XCTAssertEqual(v.dot(v.ortho()), 0, accuracy: 1e-15) + XCTAssertEqual(v.ortho().norm(), 1, accuracy: 1e-15) } } @@ -180,14 +180,14 @@ class R3R3VectorTests: XCTestCase { let d1 = v1.dot(v2) let d2 = v2.dot(v1) // Angle commutes - XCTAssertEqualWithAccuracy(a1, a2, accuracy: 1e-15) + XCTAssertEqual(a1, a2, accuracy: 1e-15) // Dot commutes - XCTAssertEqualWithAccuracy(d1, d2, accuracy: 1e-15) + XCTAssertEqual(d1, d2, accuracy: 1e-15) // Cross anti-commutes XCTAssert(c1.approxEquals(c2.mul(-1.0))) // Cross is orthogonal to original vectors - XCTAssertEqualWithAccuracy(v1.dot(c1), 0.0, accuracy: 1e-15) - XCTAssertEqualWithAccuracy(v2.dot(c1), 0.0, accuracy: 1e-15) + 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 index e77d880..f870ffb 100644 --- a/Sphere2GoLibTests/S1IntervalTests.swift +++ b/Sphere2GoLibTests/S1IntervalTests.swift @@ -64,14 +64,14 @@ class S1IntervalTests: XCTestCase { } func testCenter() { - XCTAssertEqualWithAccuracy(quad12.center(), .pi / 2, accuracy: 1e-15) - XCTAssertEqualWithAccuracy(S1Interval(lo_endpoint: 3.1, hi_endpoint: 2.9).center(), 3 - .pi, accuracy: 1e-15) - XCTAssertEqualWithAccuracy(S1Interval(lo_endpoint: -2.9, hi_endpoint: -3.1).center(), .pi - 3, accuracy: 1e-15) - XCTAssertEqualWithAccuracy(S1Interval(lo_endpoint: 2.1, hi_endpoint: -2.1).center(), .pi, accuracy: 1e-15) - XCTAssertEqualWithAccuracy(pi.center(), .pi, accuracy: 1e-15) - XCTAssertEqualWithAccuracy(mipi.center(), .pi, accuracy: 1e-15) - XCTAssertEqualWithAccuracy(quad23.center(), .pi, accuracy: 1e-15) - XCTAssertEqualWithAccuracy(quad123.center(), 0.75 * .pi, accuracy: 1e-15) + 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) } } diff --git a/Sphere2GoLibTests/S2CapTests.swift b/Sphere2GoLibTests/S2CapTests.swift index c8c3117..5fa9917 100644 --- a/Sphere2GoLibTests/S2CapTests.swift +++ b/Sphere2GoLibTests/S2CapTests.swift @@ -170,7 +170,7 @@ class S2CapTests: XCTestCase { (.pi, S2Cap.fullHeight), (4.0, S2Cap.fullHeight)] for (got, want) in tests { - XCTAssertEqualWithAccuracy(S2Cap.radiusToHeight(got), want, accuracy: 1e-14) + XCTAssertEqual(S2Cap.radiusToHeight(got), want, accuracy: 1e-14) } } @@ -186,10 +186,10 @@ class S2CapTests: XCTestCase { for (_, have, latLoDeg, latHiDeg, lngLoDeg, lngHiDeg, isFull) in tests { let r = have.rectBound() - XCTAssertEqualWithAccuracy(r.lat.lo, latLoDeg * toRadians, accuracy: epsilon) - XCTAssertEqualWithAccuracy(r.lat.hi, latHiDeg * toRadians, accuracy: epsilon) - XCTAssertEqualWithAccuracy(r.lng.lo, lngLoDeg * toRadians, accuracy: epsilon) - XCTAssertEqualWithAccuracy(r.lng.hi, lngHiDeg * toRadians, accuracy: epsilon) + 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. diff --git a/Sphere2GoLibTests/S2CellIdTests.swift b/Sphere2GoLibTests/S2CellIdTests.swift index 28f0768..2372e9e 100644 --- a/Sphere2GoLibTests/S2CellIdTests.swift +++ b/Sphere2GoLibTests/S2CellIdTests.swift @@ -267,10 +267,10 @@ class S2CellIdTests: XCTestCase { for (i, j, level, want) in tests { let uv = CellId.ijLevelToBoundUV(i: i, j: j, level: level) - XCTAssertEqualWithAccuracy(uv.x.lo, want.x.lo, accuracy: 1e-14) - XCTAssertEqualWithAccuracy(uv.x.hi, want.x.hi, accuracy: 1e-14) - XCTAssertEqualWithAccuracy(uv.y.lo, want.y.lo, accuracy: 1e-14) - XCTAssertEqualWithAccuracy(uv.y.hi, want.y.hi, accuracy: 1e-14) + 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) } } diff --git a/Sphere2GoLibTests/S2CellTests.swift b/Sphere2GoLibTests/S2CellTests.swift index 766db64..44e8d02 100644 --- a/Sphere2GoLibTests/S2CellTests.swift +++ b/Sphere2GoLibTests/S2CellTests.swift @@ -46,9 +46,9 @@ class S2CellTests: XCTestCase { for k in 0..<4 { edgeCounts[edges[k]] = (edgeCounts[edges[k]] ?? 0) + 1 vertexCounts[vertices[k]] = (vertexCounts[vertices[k]] ?? 0) + 1 - XCTAssertEqualWithAccuracy(vertices[k].dot(edges[k]), 0.0, accuracy: 1e-14) - XCTAssertEqualWithAccuracy(vertices[(k + 1) & 3].dot(edges[k]), 0.0, accuracy: 1e-14) - XCTAssertEqualWithAccuracy(vertices[k].cross(vertices[(k + 1) & 3]).normalize().dot(edges[k]), 1.0, accuracy: 1e-14) + 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. @@ -68,7 +68,7 @@ class S2CellTests: XCTestCase { // Test 1. Check the area of a top level cell. let level1Cell = CellId(id: 0x1000000000000000) let wantArea = 4.0 * .pi / 6.0 - XCTAssertEqualWithAccuracy(Cell(id: level1Cell).exactArea(), wantArea, accuracy: 1e-14) + 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 @@ -82,7 +82,7 @@ class S2CellTests: XCTestCase { approxArea += Cell(id: child).approxArea() avgArea += Cell(id: child).averageArea() } - XCTAssertEqualWithAccuracy(Cell(id: cell).exactArea(), exactArea, accuracy: 1e-14) + 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 diff --git a/Sphere2GoLibTests/S2LatLngTests.swift b/Sphere2GoLibTests/S2LatLngTests.swift index f0c40cd..e2d600d 100644 --- a/Sphere2GoLibTests/S2LatLngTests.swift +++ b/Sphere2GoLibTests/S2LatLngTests.swift @@ -65,16 +65,16 @@ class S2LatLngTests: XCTestCase { let ll = LatLng(latDegrees: lat, lngDegrees: lng) let p = ll.toPoint() // TODO(mikeperrow): Port Point.ApproxEquals, then use here. - XCTAssertEqualWithAccuracy(p.x, x, accuracy: eps) - XCTAssertEqualWithAccuracy(p.y, y, accuracy: eps) - XCTAssertEqualWithAccuracy(p.z, z, accuracy: eps) + 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) - XCTAssertEqualWithAccuracy(ll2.lat, lat * toRadians, accuracy: eps) + XCTAssertEqual(ll2.lat, lat * toRadians, accuracy: eps) if !isPolar { - XCTAssertEqualWithAccuracy(ll2.lng, lng * toRadians, accuracy: eps) + XCTAssertEqual(ll2.lng, lng * toRadians, accuracy: eps) } } } @@ -90,7 +90,7 @@ class S2LatLngTests: XCTestCase { let ll1 = LatLng(latDegrees: lat1, lngDegrees: lng1) let ll2 = LatLng(latDegrees: lat2, lngDegrees: lng2) let d = ll1.distance(ll2) - XCTAssertEqualWithAccuracy(d, want * toRadians, accuracy: tolerance) + XCTAssertEqual(d, want * toRadians, accuracy: tolerance) } } diff --git a/Sphere2GoLibTests/S2LoopTests.swift b/Sphere2GoLibTests/S2LoopTests.swift index 7fe5172..5cac757 100644 --- a/Sphere2GoLibTests/S2LoopTests.swift +++ b/Sphere2GoLibTests/S2LoopTests.swift @@ -173,7 +173,7 @@ class S2LoopTests: XCTestCase { 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) - XCTAssertEqualWithAccuracy(arctic80Inv.rectBound().lat.hi, Double(LatLng(point: S2Point(raw: mid)).lat), accuracy: 10 * Cell.dblEpsilon) + XCTAssertEqual(arctic80Inv.rectBound().lat.hi, Double(LatLng(point: S2Point(raw: mid)).lat), accuracy: 10 * Cell.dblEpsilon) } func testLoopCapBound() { diff --git a/Sphere2GoLibTests/S2PointTests.swift b/Sphere2GoLibTests/S2PointTests.swift index 758e507..dd8d3ce 100644 --- a/Sphere2GoLibTests/S2PointTests.swift +++ b/Sphere2GoLibTests/S2PointTests.swift @@ -26,7 +26,7 @@ class S2PointTests: XCTestCase { } func testOriginPoint() { - XCTAssertEqualWithAccuracy(S2Point.origin.v.norm(), 1.0, accuracy: 1e-16) + XCTAssertEqual(S2Point.origin.v.norm(), 1.0, accuracy: 1e-16) } func testPointCross() { @@ -39,9 +39,9 @@ class S2PointTests: XCTestCase { let p1 = S2Point(x: p1x, y: p1y, z: p1z) let p2 = S2Point(x: p2x, y: p2y, z: p2z) let result = p1.pointCross(p2) - XCTAssertEqualWithAccuracy(result.v.norm(), 1, accuracy: 1e-15) - XCTAssertEqualWithAccuracy(result.v.dot(p1.v), 0, accuracy: 1e-15) - XCTAssertEqualWithAccuracy(result.v.dot(p2.v), 0, accuracy: 1e-15) + 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) } } @@ -188,8 +188,8 @@ class S2PointTests: XCTestCase { 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) - XCTAssertEqualWithAccuracy(p1.distance(p2), want, accuracy: 1e-15) - XCTAssertEqualWithAccuracy(p2.distance(p1), want, accuracy: 1e-15) + XCTAssertEqual(p1.distance(p2), want, accuracy: 1e-15) + XCTAssertEqual(p2.distance(p1), want, accuracy: 1e-15) } } @@ -254,7 +254,7 @@ class S2PointTests: XCTestCase { // 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 { - XCTAssertEqualWithAccuracy(S2Point.pointArea(a, b, c), want, accuracy: nearness) + XCTAssertEqual(S2Point.pointArea(a, b, c), want, accuracy: nearness) } } @@ -272,7 +272,7 @@ class S2PointTests: XCTestCase { S2Point.pointArea(a, c, d) + S2Point.pointArea(a, d, e) + S2Point.pointArea(a, e, b) - XCTAssertEqualWithAccuracy(area, want, accuracy: 1e-15) + XCTAssertEqual(area, want, accuracy: 1e-15) } } @@ -368,11 +368,11 @@ class S2PointTests: XCTestCase { let x1 = S2Cube(face: face, u: x, v: -1).vector() let x2 = S2Cube(face: face, u: x, v: 1).vector() let uNorm1 = S2Cube.uNorm(face: face, u: x, invert: false) - XCTAssertEqualWithAccuracy(x1.cross(x2).angle(uNorm1.v), 0.0, accuracy: 1e-15) + XCTAssertEqual(x1.cross(x2).angle(uNorm1.v), 0.0, accuracy: 1e-15) let y1 = S2Cube(face: face, u: -1, v: x).vector() let y2 = S2Cube(face: face, u: 1, v: x).vector() let vNorm1 = S2Cube.vNorm(face: face, v: x, invert: false) - XCTAssertEqualWithAccuracy(y1.cross(y2).angle(vNorm1.v), 0.0, accuracy: 1e-15) + XCTAssertEqual(y1.cross(y2).angle(vNorm1.v), 0.0, accuracy: 1e-15) } } } @@ -401,8 +401,8 @@ class S2PointTests: XCTestCase { } continue } - XCTAssertEqualWithAccuracy(cube.u, u, accuracy: 1e-15) - XCTAssertEqualWithAccuracy(cube.v, v, accuracy: 1e-15) + XCTAssertEqual(cube.u, u, accuracy: 1e-15) + XCTAssertEqual(cube.v, v, accuracy: 1e-15) } } diff --git a/Sphere2GoLibTests/S2Tests.swift b/Sphere2GoLibTests/S2Tests.swift index a4acbde..17a878a 100644 --- a/Sphere2GoLibTests/S2Tests.swift +++ b/Sphere2GoLibTests/S2Tests.swift @@ -71,7 +71,7 @@ func randomUInt32() -> UInt32 { // not all possible values in this range are returned. func randomFloat64() -> Double { let randomFloatBits = UInt32(53) - return ldexp(Double(randomBits(num: randomFloatBits)), -Int(randomFloatBits)) + return scalbn(Double(randomBits(num: randomFloatBits)), -Int(randomFloatBits)) } // randomUniformInt returns a uniformly distributed integer in the range [0,n).