From 1ee06f2aa172cbae76bba9e832d53df9e8fab0da Mon Sep 17 00:00:00 2001 From: zhongchao Date: Tue, 7 Jun 2022 14:32:12 +0800 Subject: [PATCH] wait to finish --- config.gradle | 3 +- .../eagle/core/function/map/TrackManager.java | 55 +- .../eagle/core/function/map/TrackObj.java | 24 +- .../CallerAutoPilotStatusListenerManager.kt | 10 +- core/mogo-core-utils/build.gradle | 1 + .../core/utilcode/geometry/BigPoint.java | 106 + .../core/utilcode/geometry/EncodedInts.java | 253 ++ .../core/utilcode/geometry/InputStreams.java | 38 + .../utilcode/geometry/LittleEndianInput.java | 124 + .../utilcode/geometry/LittleEndianOutput.java | 94 + .../core/utilcode/geometry/Matrix3x3.java | 147 + .../geometry/ParametrizedS2Point.java | 74 + .../core/utilcode/geometry/Platform.java | 159 + .../utilcode/geometry/PrimitiveArrays.java | 297 ++ .../core/utilcode/geometry/R1Interval.java | 422 +++ .../eagle/core/utilcode/geometry/R2Rect.java | 352 +++ .../core/utilcode/geometry/R2Vector.java | 199 ++ .../eagle/core/utilcode/geometry/Real.java | 345 +++ .../eagle/core/utilcode/geometry/S1Angle.java | 285 ++ .../core/utilcode/geometry/S1ChordAngle.java | 363 +++ .../core/utilcode/geometry/S1Interval.java | 670 +++++ .../mogo/eagle/core/utilcode/geometry/S2.java | 654 ++++ .../utilcode/geometry/S2AreaCentroid.java | 65 + .../eagle/core/utilcode/geometry/S2Cap.java | 466 +++ .../eagle/core/utilcode/geometry/S2Cell.java | 785 +++++ .../core/utilcode/geometry/S2CellId.java | 1323 +++++++++ .../utilcode/geometry/S2CellIdVector.java | 35 + .../geometry/S2CellIdVectorCoder.java | 218 ++ .../core/utilcode/geometry/S2CellIndex.java | 662 +++++ .../core/utilcode/geometry/S2CellUnion.java | 837 ++++++ .../geometry/S2ClosestPointQuery.java | 660 +++++ .../eagle/core/utilcode/geometry/S2Coder.java | 41 + .../geometry/S2ContainsPointQuery.java | 278 ++ .../geometry/S2ContainsVertexQuery.java | 124 + .../utilcode/geometry/S2ConvexHullQuery.java | 266 ++ .../eagle/core/utilcode/geometry/S2Edge.java | 119 + .../core/utilcode/geometry/S2EdgeIndex.java | 637 ++++ .../core/utilcode/geometry/S2EdgeQuery.java | 628 ++++ .../core/utilcode/geometry/S2EdgeUtil.java | 2576 ++++++++++++++++ .../eagle/core/utilcode/geometry/S2Error.java | 108 + .../utilcode/geometry/S2FractalBuilder.java | 238 ++ .../core/utilcode/geometry/S2Iterator.java | 288 ++ .../core/utilcode/geometry/S2LatLng.java | 282 ++ .../core/utilcode/geometry/S2LatLngRect.java | 578 ++++ .../utilcode/geometry/S2LatLngRectBase.java | 818 +++++ .../utilcode/geometry/S2LaxPolygonShape.java | 377 +++ .../utilcode/geometry/S2LaxPolylineShape.java | 238 ++ .../eagle/core/utilcode/geometry/S2Loop.java | 2066 +++++++++++++ .../core/utilcode/geometry/S2PaddedCell.java | 264 ++ .../eagle/core/utilcode/geometry/S2Point.java | 596 ++++ .../utilcode/geometry/S2PointCompression.java | 345 +++ .../core/utilcode/geometry/S2PointIndex.java | 190 ++ .../core/utilcode/geometry/S2PointRegion.java | 152 + .../utilcode/geometry/S2PointVectorCoder.java | 856 ++++++ .../core/utilcode/geometry/S2Polygon.java | 2631 +++++++++++++++++ .../utilcode/geometry/S2PolygonBuilder.java | 929 ++++++ .../core/utilcode/geometry/S2Polyline.java | 706 +++++ .../core/utilcode/geometry/S2Predicates.java | 1733 +++++++++++ .../core/utilcode/geometry/S2Projections.java | 930 ++++++ .../core/utilcode/geometry/S2Region.java | 55 + .../utilcode/geometry/S2RegionCoverer.java | 733 +++++ .../geometry/S2RegionIntersection.java | 114 + .../core/utilcode/geometry/S2RegionUnion.java | 119 + .../eagle/core/utilcode/geometry/S2Shape.java | 249 ++ .../core/utilcode/geometry/S2ShapeAspect.java | 426 +++ .../core/utilcode/geometry/S2ShapeIndex.java | 2051 +++++++++++++ .../utilcode/geometry/S2ShapeIndexCoder.java | 532 ++++ .../geometry/S2ShapeIndexMeasures.java | 117 + .../utilcode/geometry/S2ShapeIndexRegion.java | 299 ++ .../utilcode/geometry/S2ShapeMeasures.java | 504 ++++ .../core/utilcode/geometry/S2ShapeUtil.java | 925 ++++++ .../utilcode/geometry/S2TaggedShapeCoder.java | 259 ++ .../core/utilcode/geometry/S2TextFormat.java | 738 +++++ .../utilcode/geometry/UintVectorCoder.java | 111 + .../core/utilcode/geometry/VectorCoder.java | 131 + 75 files changed, 36072 insertions(+), 11 deletions(-) create mode 100644 core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/BigPoint.java create mode 100644 core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/EncodedInts.java create mode 100644 core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/InputStreams.java create mode 100644 core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/LittleEndianInput.java create mode 100644 core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/LittleEndianOutput.java create mode 100644 core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/Matrix3x3.java create mode 100644 core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/ParametrizedS2Point.java create mode 100644 core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/Platform.java create mode 100644 core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/PrimitiveArrays.java create mode 100644 core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/R1Interval.java create mode 100644 core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/R2Rect.java create mode 100644 core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/R2Vector.java create mode 100644 core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/Real.java create mode 100644 core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/S1Angle.java create mode 100644 core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/S1ChordAngle.java create mode 100644 core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/S1Interval.java create mode 100644 core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/S2.java create mode 100644 core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/S2AreaCentroid.java create mode 100644 core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/S2Cap.java create mode 100644 core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/S2Cell.java create mode 100644 core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/S2CellId.java create mode 100644 core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/S2CellIdVector.java create mode 100644 core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/S2CellIdVectorCoder.java create mode 100644 core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/S2CellIndex.java create mode 100644 core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/S2CellUnion.java create mode 100644 core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/S2ClosestPointQuery.java create mode 100644 core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/S2Coder.java create mode 100644 core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/S2ContainsPointQuery.java create mode 100644 core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/S2ContainsVertexQuery.java create mode 100644 core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/S2ConvexHullQuery.java create mode 100644 core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/S2Edge.java create mode 100644 core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/S2EdgeIndex.java create mode 100644 core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/S2EdgeQuery.java create mode 100644 core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/S2EdgeUtil.java create mode 100644 core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/S2Error.java create mode 100644 core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/S2FractalBuilder.java create mode 100644 core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/S2Iterator.java create mode 100644 core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/S2LatLng.java create mode 100644 core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/S2LatLngRect.java create mode 100644 core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/S2LatLngRectBase.java create mode 100644 core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/S2LaxPolygonShape.java create mode 100644 core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/S2LaxPolylineShape.java create mode 100644 core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/S2Loop.java create mode 100644 core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/S2PaddedCell.java create mode 100644 core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/S2Point.java create mode 100644 core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/S2PointCompression.java create mode 100644 core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/S2PointIndex.java create mode 100644 core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/S2PointRegion.java create mode 100644 core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/S2PointVectorCoder.java create mode 100644 core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/S2Polygon.java create mode 100644 core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/S2PolygonBuilder.java create mode 100644 core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/S2Polyline.java create mode 100644 core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/S2Predicates.java create mode 100644 core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/S2Projections.java create mode 100644 core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/S2Region.java create mode 100644 core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/S2RegionCoverer.java create mode 100644 core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/S2RegionIntersection.java create mode 100644 core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/S2RegionUnion.java create mode 100644 core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/S2Shape.java create mode 100644 core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/S2ShapeAspect.java create mode 100644 core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/S2ShapeIndex.java create mode 100644 core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/S2ShapeIndexCoder.java create mode 100644 core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/S2ShapeIndexMeasures.java create mode 100644 core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/S2ShapeIndexRegion.java create mode 100644 core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/S2ShapeMeasures.java create mode 100644 core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/S2ShapeUtil.java create mode 100644 core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/S2TaggedShapeCoder.java create mode 100644 core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/S2TextFormat.java create mode 100644 core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/UintVectorCoder.java create mode 100644 core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/VectorCoder.java diff --git a/config.gradle b/config.gradle index 26e75e9d50..139e1e7568 100644 --- a/config.gradle +++ b/config.gradle @@ -28,7 +28,8 @@ ext { androidxcardview : "androidx.cardview:cardview:1.0.0", localbroadcastmanager : "androidx.localbroadcastmanager:localbroadcastmanager:1.0.0", // flexbox - flexbox : 'com.google.android.flexbox:flexbox:3.0.0', + flexbox : 'com.google.android.flexbox:flexbox:3.0.0', + guava :'com.google.guava:guava:29.0-android', // 测试 junit : "junit:junit:4.12", androidxjunit : "androidx.test.ext:junit:1.1.2", diff --git a/core/function-impl/mogo-core-function-map/src/main/java/com/mogo/eagle/core/function/map/TrackManager.java b/core/function-impl/mogo-core-function-map/src/main/java/com/mogo/eagle/core/function/map/TrackManager.java index 398ad6b22b..9eaffe8c0c 100644 --- a/core/function-impl/mogo-core-function-map/src/main/java/com/mogo/eagle/core/function/map/TrackManager.java +++ b/core/function-impl/mogo-core-function-map/src/main/java/com/mogo/eagle/core/function/map/TrackManager.java @@ -1,8 +1,59 @@ package com.mogo.eagle.core.function.map; +import android.util.ArrayMap; + +import com.mogo.eagle.core.data.config.FunctionBuildConfig; +import com.mogo.eagle.core.data.enums.TrafficTypeEnum; + +import java.util.ArrayList; +import java.util.List; + +import mogo.telematics.pad.MessagePad; + public class TrackManager { - private static final class TrackOwner{ -// private static final + private static final class TrackOwner { + private static final TrackManager trackManager = new TrackManager(); } + + public static TrackManager getInstance() { + return TrackOwner.trackManager; + } + + /** + * marker缓存队列 + */ + private final ArrayMap mMarkersCaches = new ArrayMap<>(); + + /** + * 过滤后的数据集合 + */ + private final ArrayList mFilterTrafficData = new ArrayList<>(); + + + public ArrayList filterTrafficData(List trafficData) { + //清空上次返回数据,做到缓存复用 + mFilterTrafficData.clear(); + //进入过滤机制的感知物体,首先从缓存队列中进行查找 uuid + for (MessagePad.TrackedObject data : trafficData) { + // todo 过滤掉未知感知数据,后面会依据危险等级显示 + if (!FunctionBuildConfig.isDrawUnknownIdentifyData && data.getType() == TrafficTypeEnum.TYPE_TRAFFIC_ID_WEI_ZHI.getType()) { + //CallerLogger.INSTANCE.w(TAG, "未知感知类型数据,丢弃,不渲染"); + continue; + } + String uuid = "" + data.getUuid(); + TrackObj trackObj = mMarkersCaches.get(uuid); + if (trackObj != null) { + data = trackObj.updateObj(data); + mFilterTrafficData.add(data); + } else { + trackObj = new TrackObj(data); + //todo 判断是否有重合元素 google s2 + + } + mMarkersCaches.put(uuid, trackObj); + } + return mFilterTrafficData; + } + } diff --git a/core/function-impl/mogo-core-function-map/src/main/java/com/mogo/eagle/core/function/map/TrackObj.java b/core/function-impl/mogo-core-function-map/src/main/java/com/mogo/eagle/core/function/map/TrackObj.java index a34ac2afce..87007575b7 100644 --- a/core/function-impl/mogo-core-function-map/src/main/java/com/mogo/eagle/core/function/map/TrackObj.java +++ b/core/function-impl/mogo-core-function-map/src/main/java/com/mogo/eagle/core/function/map/TrackObj.java @@ -1,13 +1,28 @@ package com.mogo.eagle.core.function.map; +import com.mogo.eagle.core.utilcode.geometry.S2CellId; +import com.mogo.eagle.core.utilcode.geometry.S2LatLng; + +import mogo.telematics.pad.MessagePad; + public class TrackObj { private final CircleQueue circleQueue = new CircleQueue(10); - private final int[] observationType = new int[3]; //类型变化的观测数组 + private final KalmanFilter kalmanFilter; //卡尔曼结果 + private S2CellId s2CellId; //s2 id权重 private long recentlyTime; private double headingDelta; //航向角德尔塔 private double speedDelta; //速度德尔塔 private double typeWeight; //类型权重 + private final int[] observationType = new int[3]; //类型变化的观测数组 + + public TrackObj(MessagePad.TrackedObject data) { + kalmanFilter = new KalmanFilter(data.getLongitude(), data.getLatitude(), 0.0000005); + circleQueue.addQueue(new ObjQueue(data.getHeading(), data.getSpeed(), data.getType())); + recentlyTime = Double.valueOf(data.getSatelliteTime()).longValue(); + S2LatLng s2LatLng = S2LatLng.fromDegrees(data.getLatitude(), data.getLongitude()); + s2CellId = S2CellId.fromLatLng(s2LatLng).parent(21); //需要验证21前后 + } public long getRecentlyTime() { return recentlyTime; @@ -41,7 +56,10 @@ public class TrackObj { this.typeWeight = typeWeight; } - public void updateObj() { - + //先处理kalman数据,将经纬度校准后,放入缓存队列,然后基于后序策略将各个项进行校准 + public MessagePad.TrackedObject updateObj(MessagePad.TrackedObject data) { +// assert kf != null; +// double[] lonLat = kf.filter(data.getLongitude(), data.getLatitude()); + return data; } } diff --git a/core/mogo-core-function-call/src/main/java/com/mogo/eagle/core/function/call/autopilot/CallerAutoPilotStatusListenerManager.kt b/core/mogo-core-function-call/src/main/java/com/mogo/eagle/core/function/call/autopilot/CallerAutoPilotStatusListenerManager.kt index 090160f922..34f7ad0caa 100644 --- a/core/mogo-core-function-call/src/main/java/com/mogo/eagle/core/function/call/autopilot/CallerAutoPilotStatusListenerManager.kt +++ b/core/mogo-core-function-call/src/main/java/com/mogo/eagle/core/function/call/autopilot/CallerAutoPilotStatusListenerManager.kt @@ -167,11 +167,11 @@ object CallerAutoPilotStatusListenerManager : CallerBase() { */ @Synchronized fun invokeAutopilotGuardian(guardianInfo: MogoReportMsg.MogoReportMessage?) { - M_AUTOPILOT_STATUS_LISTENERS.forEach { - val listener = it.value - autoPilotMessageCode = guardianInfo?.code ?: "" - listener.onAutopilotGuardian(guardianInfo) - } +// M_AUTOPILOT_STATUS_LISTENERS.forEach { +// val listener = it.value +// autoPilotMessageCode = guardianInfo?.code ?: "" +// listener.onAutopilotGuardian(guardianInfo) +// } } /** diff --git a/core/mogo-core-utils/build.gradle b/core/mogo-core-utils/build.gradle index 97bbfa5d86..45d9d9af7e 100644 --- a/core/mogo-core-utils/build.gradle +++ b/core/mogo-core-utils/build.gradle @@ -57,6 +57,7 @@ dependencies { implementation rootProject.ext.dependencies.kotlinstdlibjdk7 implementation rootProject.ext.dependencies.androidxannotation implementation rootProject.ext.dependencies.material + implementation rootProject.ext.dependencies.guava implementation rootProject.ext.dependencies.gson implementation rootProject.ext.dependencies.glideanno diff --git a/core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/BigPoint.java b/core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/BigPoint.java new file mode 100644 index 0000000000..53eae5d857 --- /dev/null +++ b/core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/BigPoint.java @@ -0,0 +1,106 @@ +/* + * Copyright 2018 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.mogo.eagle.core.utilcode.geometry; + +import com.google.common.annotations.GwtCompatible; +import com.google.common.base.Objects; +import java.math.BigDecimal; + +/** A point consisting of BigDecimal coordinates. */ +@GwtCompatible +final strictfp class BigPoint implements Comparable { + final BigDecimal x; + final BigDecimal y; + final BigDecimal z; + + /** Creates a point of BigDecimal coordinates from a point of double coordinates. */ + BigPoint(S2Point p) { + this(Platform.newBigDecimal(p.x), Platform.newBigDecimal(p.y), Platform.newBigDecimal(p.z)); + } + + /** Creates a point from the given BigDecimal coordinates. */ + BigPoint(BigDecimal x, BigDecimal y, BigDecimal z) { + this.x = x; + this.y = y; + this.z = z; + } + + /** Returns an S2Point by rounding 'this' to double precision. */ + S2Point toS2Point() { + return new S2Point(x.doubleValue(), y.doubleValue(), z.doubleValue()); + } + + /** Returns the vector cross product of 'this' with 'that'. */ + BigPoint crossProd(BigPoint that) { + return new BigPoint( + y.multiply(that.z).subtract(z.multiply(that.y)), + z.multiply(that.x).subtract(x.multiply(that.z)), + x.multiply(that.y).subtract(y.multiply(that.x))); + } + + /** Returns the vector dot product of 'this' with 'that'. */ + BigDecimal dotProd(BigPoint that) { + return x.multiply(that.x).add(y.multiply(that.y)).add(z.multiply(that.z)); + } + + /** Returns the vector dot product of 'this' with 'that'. */ + BigDecimal dotProd(S2Point that) { + return dotProd(new BigPoint(that)); + } + + /** Returns true iff this and 'p' are exactly parallel or anti-parallel. */ + boolean isLinearlyDependent(BigPoint p) { + BigPoint n = crossProd(p); + return n.x.signum() == 0 && n.y.signum() == 0 && n.z.signum() == 0; + } + + /** Returns true iff this and 'p' are exactly anti-parallel, antipodal points. */ + boolean isAntipodal(BigPoint p) { + return isLinearlyDependent(p) && dotProd(p).signum() < 0; + } + + /** Returns the square of the magnitude of this vector. */ + BigDecimal norm2() { + return this.dotProd(this); + } + + @Override + public int compareTo(BigPoint p) { + int result = x.compareTo(p.x); + if (result != 0) { + return result; + } + result = y.compareTo(p.y); + if (result != 0) { + return result; + } + return z.compareTo(p.z); + } + + @Override + public boolean equals(Object that) { + if (!(that instanceof BigPoint)) { + return false; + } + BigPoint thatPoint = (BigPoint) that; + return x.equals(thatPoint.x) && y.equals(thatPoint.y) && z.equals(thatPoint.z); + } + + @Override + public int hashCode() { + return Objects.hashCode(x, y, z); + } +} diff --git a/core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/EncodedInts.java b/core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/EncodedInts.java new file mode 100644 index 0000000000..8cea064ba8 --- /dev/null +++ b/core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/EncodedInts.java @@ -0,0 +1,253 @@ +/* + * Copyright 2018 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.mogo.eagle.core.utilcode.geometry; + +import com.google.common.annotations.GwtCompatible; +import com.google.common.primitives.UnsignedInts; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +/** Utilities for encoding and decoding integers. */ +@GwtCompatible +public class EncodedInts { + + /** + * Reads a variable-encoded signed long. + * + *

Note that if you frequently read/write negative numbers, you should consider zigzag-encoding + * your values before storing them as varints. See {@link EncodedInts#encodeZigZag32} and {@link + * #decodeZigZag32(int)}. + * + * @throws IOException if {@code input.read()} throws an {@code IOException} or returns -1 (EOF), + * or if the variable-encoded signed long is malformed. + */ + public static long readVarint64(InputStream input) throws IOException { + long result = 0; + for (int shift = 0; shift < 64; shift += 7) { + final byte b = InputStreams.readByte(input); + result |= (long) (b & 0x7F) << shift; + if ((b & 0x80) == 0) { + return result; + } + } + throw new IOException("Malformed varint."); + } + + /** + * Writes a signed long using variable encoding. + * + *

Note that if you frequently read/write negative numbers, you should consider zigzag-encoding + * your values before storing them as varints. See {@link EncodedInts#encodeZigZag32} and {@link + * #decodeZigZag32(int)}. + * + * @throws IOException if {@code output.write(int)} throws an {@link IOException}. + */ + public static void writeVarint64(OutputStream output, long value) throws IOException { + while (true) { + if ((value & ~0x7FL) == 0) { + output.write((byte) value); + return; + } else { + output.write((byte) (((int) value & 0x7F) | 0x80)); + value >>>= 7; + } + } + } + + /** + * Decodes a unsigned integer consisting of {@code bytesPerWord} bytes from {@code supplier} in + * little-endian format as an unsigned 64-bit integer. + * + *

This method is not compatible with {@link #readVarint64(InputStream)} or {@link + * #writeVarint64(OutputStream, long)}. + * + * @throws IOException if {@code input.read()} throws an {@code IOException} or returns -1 (EOF). + */ + public static long decodeUintWithLength(InputStream input, int bytesPerWord) throws IOException { + long x = 0; + for (int i = 0; i < bytesPerWord; i++) { + x += (InputStreams.readByte(input) & 0xffL) << (8 * i); + } + return x; + } + + /** + * Encodes an unsigned integer to {@code consumer} in little-endian format using {@code + * bytesPerWord} bytes. (The client must ensure that the encoder's buffer is large enough). + * + *

This method is not compatible with {@link #readVarint64(InputStream)} or {@link + * #writeVarint64(OutputStream, long)}. + * + * @throws IOException if {@code output.write(int)} throws an {@link IOException}. + */ + public static void encodeUintWithLength(OutputStream output, long value, int bytesPerWord) + throws IOException { + while (--bytesPerWord >= 0) { + output.write((byte) value); + value >>>= 8; + } + assert value == 0; + } + + /** + * Encode a ZigZag-encoded 32-bit value. ZigZag encodes signed integers into values that can be + * efficiently encoded with varint. (Otherwise, negative values must be sign-extended to 64 bits + * to be varint encoded, thus always taking 10 bytes on the wire.) + * + * @param n A signed 32-bit integer. + * @return An unsigned 32-bit integer, stored in a signed int because Java has no explicit + * unsigned support. + */ + public static int encodeZigZag32(final int n) { + // Note: the right-shift must be arithmetic + return (n << 1) ^ (n >> 31); + } + + /** + * Encode a ZigZag-encoded 64-bit value. ZigZag encodes signed integers into values that can be + * efficiently encoded with varint. (Otherwise, negative values must be sign-extended to 64 bits + * to be varint encoded, thus always taking 10 bytes on the wire.) + * + * @param n A signed 64-bit integer. + * @return An unsigned 64-bit integer, stored in a signed int because Java has no explicit + * unsigned support. + */ + public static long encodeZigZag64(final long n) { + // Note: the right-shift must be arithmetic + return (n << 1) ^ (n >> 63); + } + + /** + * Decode a ZigZag-encoded 32-bit signed value. ZigZag encodes signed integers into values that + * can be efficiently encoded with varint. (Otherwise, negative values must be sign-extended to 64 + * bits to be varint encoded, thus always taking 10 bytes on the wire.) + * + * @param n A 32-bit integer, stored in a signed int because Java has no explicit unsigned + * support. + * @return A signed 32-bit integer. + */ + public static int decodeZigZag32(final int n) { + return (n >>> 1) ^ -(n & 1); + } + + /** + * Decode a ZigZag-encoded 64-bit signed value. ZigZag encodes signed integers into values that + * can be efficiently encoded with varint. (Otherwise, negative values must be sign-extended to 64 + * bits to be varint encoded, thus always taking 10 bytes on the wire.) + * + * @param n A 64-bit integer, stored in a signed long because Java has no explicit unsigned + * support. + * @return A signed 64-bit integer. + */ + public static long decodeZigZag64(final long n) { + return (n >>> 1) ^ -(n & 1); + } + + /** + * Returns the interleaving of bits of val1 and val2, where the LSB of val1 is the LSB of the + * result, and the MSB of val2 is the MSB of the result. + */ + public static long interleaveBits(int val1, int val2) { + return insertBlankBits(val1) | (insertBlankBits(val2) << 1); + } + + /** Returns the first int de-interleaved from the result of {@link #interleaveBits}. */ + public static int deinterleaveBits1(long bits) { + return removeBlankBits(bits); + } + + /** Returns the second int de-interleaved from the result of {@link #interleaveBits}. */ + public static int deinterleaveBits2(long bits) { + return removeBlankBits(bits >>> 1); + } + + /** + * Inserts blank bits between the bits of 'value' such that the MSB is blank and the LSB is + * unchanged. + */ + private static final long insertBlankBits(int value) { + long bits = UnsignedInts.toLong(value); + bits = (bits | (bits << 16)) & 0x0000ffff0000ffffL; + bits = (bits | (bits << 8)) & 0x00ff00ff00ff00ffL; + bits = (bits | (bits << 4)) & 0x0f0f0f0f0f0f0f0fL; + bits = (bits | (bits << 2)) & 0x3333333333333333L; + bits = (bits | (bits << 1)) & 0x5555555555555555L; + return bits; + } + + /** Reverses {@link #insertBlankBits} by extracting the even bits (bit 0, 2, ...). */ + private static int removeBlankBits(long bits) { + bits &= 0x5555555555555555L; + bits |= bits >>> 1; + bits &= 0x3333333333333333L; + bits |= bits >>> 2; + bits &= 0x0f0f0f0f0f0f0f0fL; + bits |= bits >>> 4; + bits &= 0x00ff00ff00ff00ffL; + bits |= bits >>> 8; + bits &= 0x0000ffff0000ffffL; + bits |= bits >>> 16; + return (int) bits; + } + + /** + * Like {@link #interleaveBits} but interleaves bit pairs rather than individual bits. This format + * is faster to decode than the fully interleaved format, and produces the same results for our + * use case. + * + *

This code is about 10% faster than {@link #interleaveBits}. + */ + public static long interleaveBitPairs(int val1, int val2) { + return insertBlankPairs(val1) | (insertBlankPairs(val2) << 2); + } + + /** Returns the first int de-interleaved from the result of {@link #interleaveBitPairs}. */ + public static int deinterleaveBitPairs1(long pairs) { + return removeBlankPairs(pairs); + } + + /** Returns the second int de-interleaved from the result of {@link #interleaveBitPairs}. */ + public static int deinterleaveBitPairs2(long pairs) { + return removeBlankPairs(pairs >>> 2); + } + + /** Inserts 00 pairs in between the pairs from 'value'. */ + private static final long insertBlankPairs(int value) { + long bits = UnsignedInts.toLong(value); + bits = (bits | (bits << 16)) & 0x0000ffff0000ffffL; + bits = (bits | (bits << 8)) & 0x00ff00ff00ff00ffL; + bits = (bits | (bits << 4)) & 0x0f0f0f0f0f0f0f0fL; + bits = (bits | (bits << 2)) & 0x3333333333333333L; + return bits; + } + + /** + * Reverses {#link #insertBitPairs} by selecting the two LSB bits, dropping the next two, + * selecting the next two, etc. + */ + private static int removeBlankPairs(long pairs) { + pairs &= 0x3333333333333333L; + pairs |= pairs >>> 2; + pairs &= 0x0f0f0f0f0f0f0f0fL; + pairs |= pairs >>> 4; + pairs &= 0x00ff00ff00ff00ffL; + pairs |= pairs >>> 8; + pairs &= 0x0000ffff0000ffffL; + pairs |= pairs >>> 16; + return (int) pairs; + } +} diff --git a/core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/InputStreams.java b/core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/InputStreams.java new file mode 100644 index 0000000000..2e2360ac7c --- /dev/null +++ b/core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/InputStreams.java @@ -0,0 +1,38 @@ +/* + * Copyright 2018 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.mogo.eagle.core.utilcode.geometry; + +import com.google.common.annotations.GwtCompatible; +import java.io.IOException; +import java.io.InputStream; + +/** Utilities for handling {@link InputStream}s. */ +@GwtCompatible +final class InputStreams { + + /** + * Reads a byte from {@code input}. + * + * @throws IOException if {@code input.read()} throws an {@code IOException} or returns -1 (EOF). + */ + static byte readByte(InputStream input) throws IOException { + int result = input.read(); + if (result < 0) { + throw new IOException("EOF"); + } + return (byte) (result & 0xFF); + } +} diff --git a/core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/LittleEndianInput.java b/core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/LittleEndianInput.java new file mode 100644 index 0000000000..0dccec103c --- /dev/null +++ b/core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/LittleEndianInput.java @@ -0,0 +1,124 @@ +/* + * Copyright 2016 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.mogo.eagle.core.utilcode.geometry; + +import com.google.common.annotations.GwtCompatible; +import java.io.IOException; +import java.io.InputStream; + +/** Simple utility for reading little endian primitives from a stream. */ +@GwtCompatible +public final class LittleEndianInput { + private final InputStream input; + + /** Constructs a little-endian input that reads from the given stream. */ + public LittleEndianInput(InputStream input) { + this.input = input; + } + + /** + * Reads a byte. + * + * @throws IOException if {@code input.read()} throws an {@code IOException} or returns -1 (EOF). + */ + public byte readByte() throws IOException { + return InputStreams.readByte(input); + } + + /** + * Reads a fixed size of bytes from the input. + * + * @param size the number of bytes to read. + * @throws IOException if past end of input or error in underlying stream + */ + public byte[] readBytes(final int size) throws IOException { + byte[] result = new byte[size]; + int numRead = input.read(result); + if (numRead < size) { + throw new IOException("EOF"); + } + return result; + } + + /** + * Reads a little-endian signed integer. + * + * @throws IOException if past end of input or error in underlying stream + */ + public int readInt() throws IOException { + return (readByte() & 0xFF) + | ((readByte() & 0xFF) << 8) + | ((readByte() & 0xFF) << 16) + | ((readByte() & 0xFF) << 24); + } + + /** + * Reads a little-endian signed long. + * + * @throws IOException if past end of input or error in underlying stream + */ + public long readLong() throws IOException { + return (readByte() & 0xFFL) + | ((readByte() & 0xFFL) << 8) + | ((readByte() & 0xFFL) << 16) + | ((readByte() & 0xFFL) << 24) + | ((readByte() & 0xFFL) << 32) + | ((readByte() & 0xFFL) << 40) + | ((readByte() & 0xFFL) << 48) + | ((readByte() & 0xFFL) << 56); + } + + /** + * Reads a little-endian IEEE754 32-bit float. + * + * @throws IOException if past end of input or error in underlying stream + */ + public float readFloat() throws IOException { + return Float.intBitsToFloat(readInt()); + } + + /** + * Reads a little-endian IEEE754 64-bit double. + * + * @throws IOException if past end of input or error in underlying stream + */ + public double readDouble() throws IOException { + return Double.longBitsToDouble(readLong()); + } + + /** + * Reads a variable-encoded signed integer with {@link #readVarint64()}. + * + * @throws IOException if past end of input or error in underlying stream + */ + public int readVarint32() throws IOException { + return (int) readVarint64(); + } + + /** + * Reads a variable-encoded signed long with {@link EncodedInts#readVarint64(InputStream)} + * + * @throws IOException if past end of input or error in underlying stream + */ + public long readVarint64() throws IOException { + return EncodedInts.readVarint64(input); + } + + /** Closes the underlying stream. */ + public void close() throws IOException { + input.close(); + } +} diff --git a/core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/LittleEndianOutput.java b/core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/LittleEndianOutput.java new file mode 100644 index 0000000000..87a5865dc1 --- /dev/null +++ b/core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/LittleEndianOutput.java @@ -0,0 +1,94 @@ +/* + * Copyright 2016 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.mogo.eagle.core.utilcode.geometry; + +import com.google.common.annotations.GwtCompatible; +import java.io.IOException; +import java.io.OutputStream; + +/** Simple utility for writing little endian primitives to a stream. */ +@GwtCompatible +public final class LittleEndianOutput { + private final OutputStream output; + + /** Constructs a little-endian output that writes to the given stream. */ + public LittleEndianOutput(OutputStream output) { + this.output = output; + } + + /** Writes a byte. */ + public void writeByte(byte value) throws IOException { + output.write((int) value); + } + + public void writeBytes(byte[] bytes) throws IOException { + output.write(bytes); + } + + /** Writes a little-endian signed integer. */ + public void writeInt(int value) throws IOException { + output.write(value & 0xFF); + output.write((value >> 8) & 0xFF); + output.write((value >> 16) & 0xFF); + output.write((value >> 24) & 0xFF); + } + + /** Writes a little-endian signed long. */ + public void writeLong(long value) throws IOException { + output.write((int) (value & 0xFF)); + output.write((int) (value >> 8) & 0xFF); + output.write((int) (value >> 16) & 0xFF); + output.write((int) (value >> 24) & 0xFF); + output.write((int) (value >> 32) & 0xFF); + output.write((int) (value >> 40) & 0xFF); + output.write((int) (value >> 48) & 0xFF); + output.write((int) (value >> 56) & 0xFF); + } + + /** Writes a little-endian IEEE754 32-bit float. */ + public void writeFloat(float value) throws IOException { + writeInt(Float.floatToIntBits(value)); + } + + /** Writes a little-endian IEEE754 64-bit double. */ + public void writeDouble(double value) throws IOException { + writeLong(Double.doubleToLongBits(value)); + } + + /** + * Writes a signed integer using variable encoding with {@link #writeVarint64(long)}. + * + * @throws IOException if past end of input or error in underlying stream + */ + public void writeVarint32(int value) throws IOException { + writeVarint64(value); + } + + /** + * Writes a signed long using variable encoding with {@link + * EncodedInts#writeVarint64(OutputStream, long)}. + * + * @throws IOException if past end of input or error in underlying stream + */ + public void writeVarint64(long value) throws IOException { + EncodedInts.writeVarint64(output, value); + } + + /** Closes the underlying output stream. */ + public void close() throws IOException { + output.close(); + } +} diff --git a/core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/Matrix3x3.java b/core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/Matrix3x3.java new file mode 100644 index 0000000000..e4a51e684a --- /dev/null +++ b/core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/Matrix3x3.java @@ -0,0 +1,147 @@ +/* + * Copyright 2013 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.mogo.eagle.core.utilcode.geometry; + +import com.google.common.annotations.GwtCompatible; +import com.google.common.base.Preconditions; +import java.util.List; +import javax.annotation.CheckReturnValue; + +/** A simple 3x3 matrix. */ +// TODO(eengle): Rename this to Matrix as it is not necessarily 3x3, and make Matrix3x3 a subclass. +@GwtCompatible +final class Matrix3x3 { + private final double[] values; + private final int rows; + private final int cols; + + /** Constructs a matrix from a series of column vectors. */ + public static Matrix3x3 fromCols(S2Point... columns) { + Matrix3x3 result = new Matrix3x3(3, columns.length); + for (int row = 0; row < result.rows; row++) { + for (int col = 0; col < result.cols; col++) { + result.set(row, col, columns[col].get(row)); + } + } + return result; + } + + /** Constructs a matrix from a series of column vectors. */ + public static Matrix3x3 fromCols(List frame) { + return fromCols(frame.toArray(new S2Point[frame.size()])); + } + + /** Constructs a 2D matrix of the given width and values. */ + public Matrix3x3(int cols, double... values) { + Preconditions.checkArgument(cols >= 0, "Negative rows not allowed."); + rows = values.length / cols; + this.cols = cols; + Preconditions.checkArgument( + rows * cols == values.length, "Values not an even multiple of 'cols'"); + this.values = values; + } + + /** Constructs a 2D matrix of a fixed size. */ + public Matrix3x3(int rows, int cols) { + Preconditions.checkArgument(rows >= 0, "Negative rows not allowed."); + Preconditions.checkArgument(cols >= 0, "Negative cols not allowed."); + this.rows = rows; + this.cols = cols; + this.values = new double[rows * cols]; + } + + /** Returns the number of rows in this matrix. */ + public int rows() { + return rows; + } + + /** Returns the number of columns in this matrix. */ + public int cols() { + return cols; + } + + /** Sets a value. */ + public void set(int row, int col, double value) { + values[row * cols + col] = value; + } + + /** Gets a value. */ + public double get(int row, int col) { + return values[row * cols + col]; + } + + /** Returns the transpose of this. */ + @CheckReturnValue + public Matrix3x3 transpose() { + Matrix3x3 result = new Matrix3x3(cols, rows); + for (int row = 0; row < result.rows; row++) { + for (int col = 0; col < result.cols; col++) { + result.set(row, col, get(col, row)); + } + } + return result; + } + + /** Returns the result of multiplying this x m. */ + @CheckReturnValue + public Matrix3x3 mult(Matrix3x3 m) { + Preconditions.checkArgument(cols == m.rows); + Matrix3x3 result = new Matrix3x3(rows, m.cols); + for (int row = 0; row < result.rows; row++) { + for (int col = 0; col < result.cols; col++) { + double sum = 0; + for (int i = 0; i < cols; i++) { + sum += get(row, i) * m.get(i, col); + } + result.set(row, col, sum); + } + } + return result; + } + + /** Return the vector of the given column. */ + public S2Point getCol(int col) { + Preconditions.checkState(rows == 3); + Preconditions.checkArgument(0 <= col && col < cols); + return new S2Point(values[col], values[cols + col], values[2 * cols + col]); + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof Matrix3x3)) { + return false; + } + Matrix3x3 m = (Matrix3x3) o; + if (rows != m.rows || cols != m.cols) { + return false; + } + for (int i = 0; i < values.length; i++) { + if (values[i] != m.values[i]) { + return false; + } + } + return true; + } + + @Override + public int hashCode() { + long hash = 37L * cols; + for (int i = 0; i < values.length; i++) { + hash = 37L * hash + Platform.doubleHash(values[i]); + } + return (int) hash; + } +} diff --git a/core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/ParametrizedS2Point.java b/core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/ParametrizedS2Point.java new file mode 100644 index 0000000000..dcf1d6ce75 --- /dev/null +++ b/core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/ParametrizedS2Point.java @@ -0,0 +1,74 @@ +/* + * Copyright 2006 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.mogo.eagle.core.utilcode.geometry; + +import com.google.common.annotations.GwtCompatible; +import java.io.Serializable; + +/** + * An S2Point that also has a parameter associated with it, which corresponds to a time-like order + * on the points. + * + * @author kirilll@google.com (Kirill Levin) + */ +@GwtCompatible(serializable = true) +public final class ParametrizedS2Point implements Comparable, Serializable { + + private final double time; + private final S2Point point; + + @SuppressWarnings("GoodTime") // should accept a java.time.Duration (?) + public ParametrizedS2Point(double time, S2Point point) { + this.time = time; + this.point = point; + } + + @SuppressWarnings("GoodTime") // should return a java.time.Duration (?) + public double getTime() { + return time; + } + + public S2Point getPoint() { + return point; + } + + @Override + public int compareTo(ParametrizedS2Point o) { + int compareTime = Double.compare(time, o.time); + if (compareTime != 0) { + return compareTime; + } + return point.compareTo(o.point); + } + + @Override + public boolean equals(Object other) { + if (other instanceof ParametrizedS2Point) { + ParametrizedS2Point x = (ParametrizedS2Point) other; + return time == x.time && point.equalsPoint(x.point); + } else { + return false; + } + } + + @Override + public int hashCode() { + // TODO(jrosenstock): Use Objects.hash when API 19 (2014-06) is allowed. Current min is 14 + // (2011-10). Double.hashCode requires an even higher API level, so just hash the point. + return point.hashCode(); + } +} diff --git a/core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/Platform.java b/core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/Platform.java new file mode 100644 index 0000000000..e0914b521b --- /dev/null +++ b/core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/Platform.java @@ -0,0 +1,159 @@ +/* + * Copyright 2013 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.mogo.eagle.core.utilcode.geometry; + +import com.google.common.annotations.GwtCompatible; +import java.io.PrintStream; +import java.math.BigDecimal; +import java.util.Locale; +import java.util.logging.Logger; + +/** + * Contains utility methods which require different GWT client and server implementations. This + * contains the server side implementations. + */ +@GwtCompatible(emulated = true) +final class Platform { + + private Platform() {} + + /** @see Math#IEEEremainder(double, double) */ + static double IEEEremainder(double f1, double f2) { + return Math.IEEEremainder(f1, f2); + } + + /** @see Math#getExponent(double) */ + static int getExponent(double d) { + return Math.getExponent(d); + } + + /** + * Returns the {@link Logger} for the class. + * + * @see Logger#getLogger(String) + */ + static Logger getLoggerForClass(Class clazz) { + return Logger.getLogger(clazz.getCanonicalName()); + } + + /** + * Invokes {@code stream.printf} with the arguments. The GWT client just prints the format string + * and the arguments separately. Using this method is not recommended; you should instead + * construct strings with normal string concatenation whenever possible, so it will work the same + * way in normal Java and GWT client versions. + */ + static void printf(PrintStream stream, String format, Object... params) { + stream.printf(format, params); + } + + /** + * Returns {@code String.format} with the arguments. The GWT client just returns a string + * consisting of the format string with the parameters concatenated to the end of it. Using this + * method is not recommended; you should instead construct strings with normal string + * concatenation whenever possible, so it will work the same way in normal Java and GWT client + * versions. + */ + static String formatString(String format, Object... params) { + return String.format(format, params); + } + + /** + * Formats the double as a string and removes unneeded trailing zeros, to behave the same as + * printf("%.15g",d) in C++. The Javascript implementation does NOT have identical behavior. + */ + static String formatDouble(double d) { + StringBuilder out = new StringBuilder(); + if (d == 0d) { + return "0"; + } + // Style 'g' uses either 'e' or 'f', depending on the magnitude of the number. + out.append(String.format(Locale.US, "%.15g", d)); + + // If formatted with style 'e', the 'e' is always in the same place relative to the length, + // and the string will be at least five chars long, like "1e-20". + if ((out.length() >= 5) && (out.charAt(out.length() - 4) == 'e')) { + // Remove trailing zeros before the 'e'. + while ((out.length() >= 5) && out.charAt(out.length() - 5) == '0') { + out.deleteCharAt(out.length() - 5); + } + // Remove trailing decimal point. + if (out.charAt(out.length() - 5) == '.') { + out.deleteCharAt(out.length() - 5); + } + } else { + // Otherwise, it was formatted with style 'f'. Remove trailing zeros. + while (out.length() > 0 && out.charAt(out.length() - 1) == '0') { + out.setLength(out.length() - 1); + } + // Remove trailing decimal point. + if (out.length() > 0 && out.charAt(out.length() - 1) == '.') { + out.setLength(out.length() - 1); + } + } + return out.toString(); + } + + /** A portable way to hash a double value. */ + public static long doubleHash(double value) { + return Double.doubleToLongBits(value); + } + + /** + * Returns the sign of the determinant of the matrix constructed from the three column vectors + * {@code a}, {@code b}, and {@code c}. This operation is very robust for small determinants, but + * is extremely slow and should only be used if performance is not a concern or all faster + * techniques have been exhausted. + */ + public static int sign(S2Point a, S2Point b, S2Point c) { + Real bycz = Real.mul(b.y, c.z); + Real bzcy = Real.mul(b.z, c.y); + Real bzcx = Real.mul(b.z, c.x); + Real bxcz = Real.mul(b.x, c.z); + Real bxcy = Real.mul(b.x, c.y); + Real bycx = Real.mul(b.y, c.x); + Real bcx = bycz.sub(bzcy); + Real bcy = bzcx.sub(bxcz); + Real bcz = bxcy.sub(bycx); + Real x = bcx.mul(a.x); + Real y = bcy.mul(a.y); + Real z = bcz.mul(a.z); + return x.add(y).add(z).signum(); + } + + /** + * Returns the size of an ulp of the argument. An ulp of a double value is the positive distance + * between this floating-point value and the double next larger in magnitude. + */ + public static double ulp(double x) { + return Math.ulp(x); + } + + /** + * Returns the next representable value in the direction of 'dir' starting from 'x', emulating the + * behavior of {@link Math#nextAfter}. + */ + public static double nextAfter(double x, double dir) { + return Math.nextAfter(x, dir); + } + + /** + * Returns a new {@code BigDecimal} instance whose value is the exact decimal representation of + * {@code x}, emulating the behavior of {@link BigDecimal#BigDecimal(double)}. + */ + static BigDecimal newBigDecimal(double x) { + return new BigDecimal(x); + } +} diff --git a/core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/PrimitiveArrays.java b/core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/PrimitiveArrays.java new file mode 100644 index 0000000000..8cab367286 --- /dev/null +++ b/core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/PrimitiveArrays.java @@ -0,0 +1,297 @@ +/* + * Copyright 2019 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.mogo.eagle.core.utilcode.geometry; + +import com.google.common.annotations.GwtCompatible; +import com.google.common.annotations.GwtIncompatible; +import com.google.common.base.Preconditions; +import com.google.common.primitives.Doubles; +import com.google.common.primitives.ImmutableLongArray; +import com.google.common.primitives.Ints; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.ByteBuffer; + +/** A set of interfaces for describing primitive arrays. */ +@GwtCompatible +public final class PrimitiveArrays { + + /** + * An array of {@code byte}s. + * + *

Implementations will be thread-safe if the underlying data is not mutated. Users should + * ensure the underlying data is not mutated in order to get predictable behaviour. Any buffering + * should be done internally. + * + *

Implementations may support arrays > 2GB in size like so: + * + *

{@code
+   * new Bytes() {
+   *   byte get(long position) {
+   *     if (position < b1.length) {
+   *       return b1[Ints.checkedCast(position)];
+   *     }
+   *     return b2[Ints.checkedCast(position - b2.length)];
+   *   }
+   *   long length() { return b1.length + b2.length; }
+   * }
+   * }
+ */ + public interface Bytes { + /** + * Returns the {@code byte} at position {@code position}. + * + *

Throws an {@link IndexOutOfBoundsException} if the absolute get on the underlying + * implementation fails. + */ + byte get(long position); + + /** Returns the length of this array. */ + long length(); + + /** Returns a {@link Cursor} with the given {@code position} and {@code limit}. */ + default Cursor cursor(long position, long limit) { + Preconditions.checkArgument(position <= limit && position <= length()); + return new Cursor(position, limit); + } + + /** + * Returns a {@link Cursor} with the given {@code position}. + * + *

The {@code limit} of the returned cursor is the {@link #length()} of this array. + */ + default Cursor cursor(long position) { + return cursor(position, length()); + } + + /** + * Returns a {@link Cursor}. + * + *

The {@code position} of the returned cursor is 0, and the {@code limit} is the {@link + * #length()} of this array. + */ + default Cursor cursor() { + return cursor(0); + } + + /** + * Returns a {@link Bytes} wrapping {@code buffer}. + * + *

The returned array starts from index 0 of buffer, and its length is {@code + * buffer.limit()}. + */ + @GwtIncompatible("ByteBuffer") + static Bytes fromByteBuffer(ByteBuffer buffer) { + // TODO(eengle): Buffer positions > 0 trip bugs in the various methods. Exclude this case. + Preconditions.checkState(buffer.position() == 0); + return new Bytes() { + @Override + public byte get(long position) { + return buffer.get(Ints.checkedCast(position)); + } + + @Override + public long length() { + return (long) buffer.limit(); + } + }; + } + + /** Returns a {@link Bytes} wrapping {@code bytes}. */ + static Bytes fromByteArray(byte[] bytes) { + return new Bytes() { + @Override + public byte get(long position) { + return bytes[Ints.checkedCast(position)]; + } + + @Override + public long length() { + return bytes.length; + } + }; + } + + /** Returns an {@link InputStream} wrapping this array starting at {@code offset}. */ + default InputStream toInputStream(long offset) { + Preconditions.checkArgument(offset >= 0 && offset <= length()); + return new InputStream() { + long position = offset; + + @Override + public int read() { + if (position == length()) { + return -1; + } + return get(position++) & 0xFF; + } + }; + } + + /** + * Returns an {@link InputStream} wrapping this array starting at {@code cursor.position}. + * + *

{@code cursor.position} is incremented for each byte read from the returned {@link + * InputStream}. + */ + default InputStream toInputStream(Cursor cursor) { + Preconditions.checkArgument(cursor.position >= 0 && cursor.position <= length()); + return new InputStream() { + @Override + public int read() { + if (cursor.position == length()) { + return -1; + } + return get(cursor.position++) & 0xFF; + } + }; + } + + /** Returns an {@link InputStream} wrapping this array starting at the 0th byte. */ + default InputStream toInputStream() { + return toInputStream(0); + } + + /** Writes this array to {@code output}. */ + default void writeTo(OutputStream output) throws IOException { + for (long i = 0; i < length(); i++) { + output.write(get(i)); + } + } + + /** + * Returns a unsigned integer consisting of {@code numBytes} bytes read from this array at + * {@code cursor.position} in little-endian format as an unsigned 64-bit integer. + * + *

{@code cursor.position} is updated to the index of the first byte following the varint64. + */ + default long readVarint64(Cursor cursor) { + long result = 0; + for (int shift = 0; shift < 64; shift += 7) { + byte b = get(cursor.position++); + result |= (long) (b & 0x7F) << shift; + if ((b & 0x80) == 0) { + return result; + } + } + throw new IllegalArgumentException("Malformed varint."); + } + + /** + * Same as {@link #readVarint64(Cursor)}, but throws an {@link IllegalArgumentException} if the + * read varint64 is greater than {@link Integer#MAX_VALUE}. + */ + default int readVarint32(Cursor cursor) { + return Ints.checkedCast(readVarint64(cursor)); + } + + /** + * Returns a unsigned integer consisting of {@code numBytes} bytes read from this array at + * {@code cursor.position} in little-endian format as an unsigned 64-bit integer. + * + *

{@code cursor.position} is updated to the index of the first byte following the uint. + * + *

This method is not compatible with {@link #readVarint64(Cursor)}. + */ + default long readUintWithLength(Cursor cursor, int numBytes) { + long result = readUintWithLength(cursor.position, numBytes); + cursor.position += numBytes; + return result; + } + + /** Same as {@link #readUintWithLength(Cursor, int)}, but does not require a {@link Cursor}. */ + default long readUintWithLength(long position, int numBytes) { + long x = 0; + for (int i = 0; i < numBytes; i++) { + x += (get(position++) & 0xffL) << (8 * i); + } + return x; + } + + /** Returns a little-endian double read from this array at {@code position}. */ + default double readLittleEndianDouble(long position) { + return Double.longBitsToDouble(readUintWithLength(position, Doubles.BYTES)); + } + } + + /** + * An array of {@code long}s. + * + *

Implementations will be thread-safe if the underlying data is not mutated. Users should + * ensure the underlying data is not mutated in order to get predictable behaviour. Any buffering + * should be done internally. + */ + interface Longs { + /** + * Returns the {@code long} at position {@code position}. + * + *

Throws an {@link IndexOutOfBoundsException} if the absolute get on the underlying + * implementation fails. + */ + long get(int position); + + /** Returns the length of this array. */ + int length(); + + /** Returns a {@link Longs} wrapping {@code immutableLongArray}. */ + static Longs fromImmutableLongArray(ImmutableLongArray immutableLongArray) { + return new Longs() { + @Override + public long get(int position) { + return immutableLongArray.get(position); + } + + @Override + public int length() { + return immutableLongArray.length(); + } + }; + } + + /** + * Decodes and returns this array as an {@code int[]}. + * + *

Throws an {@link IllegalArgumentException} if any value in this array is < {@link + * Integer#MIN_VALUE} or > {@link Integer#MAX_VALUE}. + */ + default int[] toIntArray() { + int[] result = new int[length()]; + for (int i = 0; i < result.length; i++) { + result[i] = Ints.checkedCast(get(i)); + } + return result; + } + } + + /** A cursor storing a position and a limit. */ + public static class Cursor { + public long position; + public long limit; + + Cursor(long position, long limit) { + Preconditions.checkArgument(position >= 0); + Preconditions.checkArgument(position <= limit); + this.position = position; + this.limit = limit; + } + + /** Returns the number of remaining elements ({@code limit - position}). */ + public long remaining() { + return limit - position; + } + } +} diff --git a/core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/R1Interval.java b/core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/R1Interval.java new file mode 100644 index 0000000000..1d9be4783a --- /dev/null +++ b/core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/R1Interval.java @@ -0,0 +1,422 @@ +/* + * Copyright 2005 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.mogo.eagle.core.utilcode.geometry; + +import com.google.common.annotations.GwtCompatible; +import java.io.Serializable; +import javax.annotation.CheckReturnValue; + +/** + * An R1Interval represents a closed, bounded interval on the real line. It is capable of + * representing the empty interval (containing no points) and zero-length intervals (containing a + * single point). + * + */ +@GwtCompatible(serializable = true) +public final strictfp class R1Interval implements Serializable { + private double lo; + private double hi; + + /** + * Default constructor, contains the empty interval. + * + *

Package private since only the S2 library needs to mutate R1Intervals. External code that + * needs an empty interval should call {@link #empty()}. + */ + R1Interval() { + lo = 1; + hi = 0; + } + + /** Interval constructor. If lo > hi, the interval is empty. */ + public R1Interval(double lo, double hi) { + this.lo = lo; + this.hi = hi; + } + + /** Copy constructor. */ + public R1Interval(R1Interval interval) { + this.lo = interval.lo; + this.hi = interval.hi; + } + + /** Returns an empty interval. (Any interval where lo > hi is considered empty.) */ + public static R1Interval empty() { + return new R1Interval(1, 0); + } + + /** Convenience method to construct an interval containing a single point. */ + public static R1Interval fromPoint(double p) { + return new R1Interval(p, p); + } + + /** + * Convenience method to construct the minimal interval containing the two given points. This is + * equivalent to starting with an empty interval and calling AddPoint() twice, but it is more + * efficient. + */ + public static R1Interval fromPointPair(double p1, double p2) { + R1Interval result = new R1Interval(); + result.initFromPointPair(p1, p2); + return result; + } + + void initFromPointPair(double p1, double p2) { + if (p1 <= p2) { + lo = p1; + hi = p2; + } else { + lo = p2; + hi = p1; + } + } + + public double lo() { + return lo; + } + + public double hi() { + return hi; + } + + /** Designates which end of the interval to work with. */ + enum Endpoint { + /** The low end of the interval. */ + LO { + @Override + public double getValue(R1Interval interval) { + return interval.lo; + } + + @Override + public void setValue(R1Interval interval, double value) { + interval.lo = value; + } + + @Override + public Endpoint opposite() { + return HI; + } + }, + /** The high end of the interval. */ + HI { + @Override + public double getValue(R1Interval interval) { + return interval.hi; + } + + @Override + public void setValue(R1Interval interval, double value) { + interval.hi = value; + } + + @Override + public Endpoint opposite() { + return LO; + } + }; + + public abstract double getValue(R1Interval interval); + + public abstract void setValue(R1Interval interval, double value); + + public abstract Endpoint opposite(); + } + + /** Returns the value at the given Endpoint, which must not be null. */ + double getValue(Endpoint endpoint) { + return endpoint.getValue(this); + } + + /** Sets the value of the given Endpoint, which must not be null. */ + void setValue(Endpoint endpoint, double value) { + endpoint.setValue(this, value); + } + + /** Returns true if the interval is empty, i.e. it contains no points. */ + public boolean isEmpty() { + return lo > hi; + } + + /** Returns the center of the interval. For empty intervals, the result is arbitrary. */ + public double getCenter() { + return 0.5 * (lo + hi); + } + + /** Returns the length of the interval. The length of an empty interval is negative. */ + public double getLength() { + return hi - lo; + } + + public boolean contains(double p) { + return p >= lo && p <= hi; + } + + public boolean interiorContains(double p) { + return p > lo && p < hi; + } + + /** Returns true if this interval contains the interval {@code y}. */ + public boolean contains(R1Interval y) { + if (y.isEmpty()) { + return true; + } + return y.lo >= lo && y.hi <= hi; + } + + /** + * Returns true if the interior of this interval contains the entire interval {@code y} (including + * its boundary). + */ + public boolean interiorContains(R1Interval y) { + if (y.isEmpty()) { + return true; + } + return y.lo > lo && y.hi < hi; + } + + /** Returns true if this interval intersects {@code y}, i.e. if they have any points in common. */ + public boolean intersects(R1Interval y) { + if (lo <= y.lo) { + return y.lo <= hi && y.lo <= y.hi; + } else { + return lo <= y.hi && lo <= hi; + } + } + + /** + * Returns true if the interior of this interval intersects any point of {@code y} (including its + * boundary). + */ + public boolean interiorIntersects(R1Interval y) { + return y.lo < hi && lo < y.hi && lo < hi && y.lo <= y.hi; + } + + /** + * Return the Hausdorff distance to the given interval {@code y}. For two R1Intervals x and y, + * this distance is defined as h(x, y) = max_{p in x} min_{q in y} d(p, q). + */ + public double getDirectedHausdorffDistance(R1Interval y) { + if (isEmpty()) { + return 0.0; + } + if (y.isEmpty()) { + return Double.MAX_VALUE; + } + return Math.max(0.0, Math.max(hi() - y.hi(), y.lo() - lo())); + } + + /** + * Sets the minimum and maximum value of this interval. If {@code lo} is greater than {@code hi} + * this interval will become empty. + * + *

Package private since only the S2 libraries have a current need to mutate R1Intervals. + */ + void set(double lo, double hi) { + this.lo = lo; + this.hi = hi; + } + + /** + * Sets the minimum value of this interval. If {@code lo} is greater than {@code hi()} this + * interval will become empty. + * + *

Package private since only the S2 libraries have a current need to mutate R1Intervals. + */ + void setLo(double lo) { + this.lo = lo; + } + + /** + * Sets the maximum value of this interval. If {@code hi} is less than {@code lo()} this interval + * will become empty. + * + *

Package private since only the S2 libraries have a current need to mutate R1Intervals. + */ + void setHi(double hi) { + this.hi = hi; + } + + /** + * Sets the current interval to the empty interval. + * + *

Package private since only the S2 libraries have a current need to mutate R1Intervals. + */ + void setEmpty() { + this.lo = 1; + this.hi = 0; + } + + /** + * Expands this interval so that it contains the point {@code p}. + * + *

Package private since only the S2 library needs to mutate R1Intervals. + */ + void unionInternal(double p) { + if (isEmpty()) { + lo = p; + hi = p; + } else if (p < lo) { + lo = p; + } else if (p > hi) { + hi = p; + } + } + + /** + * Returns the closest point in the interval to the point {@code p}. The interval must be + * non-empty. + */ + public double clampPoint(double p) { + // assert (!isEmpty()); + return Math.max(lo, Math.min(hi, p)); + } + + /** + * Return an interval that contains all points with a distance "radius" of a point in this + * interval. Note that the expansion of an empty interval is always empty. + */ + @CheckReturnValue + public R1Interval expanded(double radius) { + // assert (radius >= 0); + if (isEmpty()) { + return this; + } + return new R1Interval(lo - radius, hi + radius); + } + + /** + * Expands this interval to contain all points within a distance "radius" of a point in this + * interval. + * + *

Package private since only S2 classes are intended to mutate R1Intervals for now. + */ + void expandedInternal(double radius) { + lo -= radius; + hi += radius; + } + + /** Returns the smallest interval that contains this interval and {@code y}. */ + @CheckReturnValue + public R1Interval union(R1Interval y) { + if (isEmpty()) { + return y; + } + if (y.isEmpty()) { + return this; + } + return new R1Interval(Math.min(lo, y.lo), Math.max(hi, y.hi)); + } + + /** + * Sets this interval to the union of this interval and {@code y}. + * + *

Package private since only S2 classes are intended to mutate R11Intervals for now. + */ + void unionInternal(R1Interval y) { + if (isEmpty()) { + lo = y.lo; + hi = y.hi; + } else if (!y.isEmpty()) { + lo = Math.min(lo, y.lo); + hi = Math.max(hi, y.hi); + } + } + + /** + * Returns the intersection of this interval with {@code y}. Empty intervals do not need to be + * special-cased. + */ + @CheckReturnValue + public R1Interval intersection(R1Interval y) { + return new R1Interval(Math.max(lo, y.lo), Math.min(hi, y.hi)); + } + + /** + * Sets this interval to the intersection of the current interval and {@code y}. + * + *

Package private since only S2 classes are intended to mutate R1 intervals for now. + */ + void intersectionInternal(R1Interval y) { + lo = Math.max(lo, y.lo); + hi = Math.min(hi, y.hi); + } + + /** Returns the smallest interval that contains this interval and the point {@code p}. */ + @CheckReturnValue + public R1Interval addPoint(double p) { + if (isEmpty()) { + return R1Interval.fromPoint(p); + } else if (p < lo) { + return new R1Interval(p, hi); + } else if (p > hi) { + return new R1Interval(lo, p); + } else { + return new R1Interval(lo, hi); + } + } + + @Override + public boolean equals(Object that) { + if (that instanceof R1Interval) { + R1Interval y = (R1Interval) that; + // Return true if two intervals contain the same set of points. + return (lo == y.lo && hi == y.hi) || (isEmpty() && y.isEmpty()); + } + return false; + } + + @Override + public int hashCode() { + if (isEmpty()) { + return 17; + } + + long value = 17; + value = 37 * value + Double.doubleToLongBits(lo); + value = 37 * value + Double.doubleToLongBits(hi); + return (int) (value ^ (value >>> 32)); + } + + /** + * As {@link #approxEquals(R1Interval, double)}, with a default value for maxError just larger + * than typical rounding errors in computing intervals. + */ + public boolean approxEquals(R1Interval y) { + return approxEquals(y, 1e-15); + } + + /** + * Returns true if this interval can be transformed into {@code y} by moving each endpoint by at + * most {@code maxError}. The empty interval is considered to be positioned arbitrarily on the + * real line, thus any interval for which {@code length <= 2*maxError} is true matches the empty + * interval. + */ + public boolean approxEquals(R1Interval y, double maxError) { + if (isEmpty()) { + return y.getLength() <= maxError; + } + if (y.isEmpty()) { + return getLength() <= maxError; + } + return Math.abs(y.lo - lo) <= maxError && Math.abs(y.hi - hi) <= maxError; + } + + @Override + public String toString() { + return "[" + lo + ", " + hi + "]"; + } +} diff --git a/core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/R2Rect.java b/core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/R2Rect.java new file mode 100644 index 0000000000..58781122c3 --- /dev/null +++ b/core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/R2Rect.java @@ -0,0 +1,352 @@ +/* + * Copyright 2013 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.mogo.eagle.core.utilcode.geometry; + +import com.google.common.annotations.GwtCompatible; +import java.io.Serializable; +import javax.annotation.CheckReturnValue; + +/** + * An R2Rect represents a closed axis-aligned rectangle in the (x,y) plane. This class is mutable to + * allow iteratively constructing bounds via e.g. {@link #addPoint(R2Vector)}. + */ +@GwtCompatible(serializable = true) +public final strictfp class R2Rect implements Serializable { + private final R1Interval x; + private final R1Interval y; + + /** Creates an empty R2Rect. */ + public R2Rect() { + // The default R1Interval constructor creates an empty interval. + this.x = new R1Interval(); + this.y = new R1Interval(); + // assert (isValid()); + } + + /** Constructs a rectangle from the given lower-left and upper-right points. */ + public R2Rect(R2Vector lo, R2Vector hi) { + x = new R1Interval(lo.x(), hi.x()); + y = new R1Interval(lo.y(), hi.y()); + // assert (isValid()); + } + + /** + * Constructs a rectangle from the given intervals in x and y. The two intervals must either be + * both empty or both non-empty. + */ + public R2Rect(R1Interval x, R1Interval y) { + this.x = x; + this.y = y; + // assert (isValid()); + } + + /** Copy constructor. */ + public R2Rect(R2Rect rect) { + this.x = new R1Interval(rect.x); + this.y = new R1Interval(rect.y); + } + + /** + * Returns a new instance of the canonical empty rectangle. Use isEmpty() to test for empty + * rectangles, since they have more than one representation. + */ + public static R2Rect empty() { + return new R2Rect(R1Interval.empty(), R1Interval.empty()); + } + + /** + * Returns a new rectangle from a center point and size in each dimension. Both components of size + * should be non-negative, i.e. this method cannot be used to create an empty rectangle. + */ + public static R2Rect fromCenterSize(R2Vector center, R2Vector size) { + return new R2Rect( + new R1Interval(center.x() - 0.5 * size.x(), center.x() + 0.5 * size.x()), + new R1Interval(center.y() - 0.5 * size.y(), center.y() + 0.5 * size.y())); + } + + /** Returns a rectangle containing a single point. */ + public static R2Rect fromPoint(R2Vector p) { + return new R2Rect(p, p); + } + + /** + * Returns the minimal bounding rectangle containing the two given points. This is equivalent to + * starting with an empty rectangle and calling addPoint() twice. Note that it is different than + * the R2Rect(lo, hi) constructor, where the first point is always used as the lower-left corner + * of the resulting rectangle. + */ + public static R2Rect fromPointPair(R2Vector p1, R2Vector p2) { + return new R2Rect( + R1Interval.fromPointPair(p1.x(), p2.x()), R1Interval.fromPointPair(p1.y(), p2.y())); + } + + /** Returns the interval along the x-axis. */ + public R1Interval x() { + return x; + } + + /** Returns the interval along the y-axis. */ + public R1Interval y() { + return y; + } + + /** Returns the point in this rectangle with the minimum x and y values. */ + public R2Vector lo() { + return new R2Vector(x().lo(), y().lo()); + } + + /** Returns the point in this rectangle with the maximum x and y values. */ + public R2Vector hi() { + return new R2Vector(x().hi(), y().hi()); + } + + /** + * Returns true if this rectangle is valid, which essentially just means that if the bound for + * either axis is empty then both must be. + */ + public boolean isValid() { + // The x/y ranges must either be both empty or both non-empty. + return x().isEmpty() == y().isEmpty(); + } + + /** Return true if this rectangle is empty, i.e. it contains no points at all. */ + public boolean isEmpty() { + return x().isEmpty(); + } + + /** + * Returns the kth vertex of this rectangle (k = 0,1,2,3) in CCW order. Vertex 0 is + * in the lower-left corner. + */ + public R2Vector getVertex(int k) { + // Twiddle bits to return the points in CCW order (lower left, lower right, + // upper right, upper left). + // assert (k >= 0 && k <= 3); + return getVertex((k >> 1) ^ (k & 1), k >> 1); + } + + /** + * Returns the vertex in direction "i" along the x-axis (0=left, 1=right) and direction "j" along + * the y-axis (0=down, 1=up). Equivalently, returns the vertex constructed by selecting endpoint + * "i" of the x-interval (0=lo, 1=hi) and vertex "j" of the y-interval. + */ + public R2Vector getVertex(int i, int j) { + return new R2Vector(i == 0 ? x.lo() : x.hi(), j == 0 ? y.lo() : y.hi()); + } + + /** Returns the center of this rectangle in (x,y)-space. */ + public R2Vector getCenter() { + return new R2Vector(x().getCenter(), y().getCenter()); + } + + /** + * Return the width and height of this rectangle in (x,y)-space. Empty rectangles have a negative + * width and height. + */ + public R2Vector getSize() { + return new R2Vector(x().getLength(), y().getLength()); + } + + /** Valid axes. */ + public enum Axis { + X { + @Override + public R1Interval getInterval(R2Rect rect) { + return rect.x; + } + }, + Y { + @Override + public R1Interval getInterval(R2Rect rect) { + return rect.y; + } + }; + + public abstract R1Interval getInterval(R2Rect rect); + } + + /** Returns the interval for the given axis, which must not be null. */ + public R1Interval getInterval(Axis axis) { + return axis.getInterval(this); + } + + /** + * Returns true if this rectangle contains the given point. Note that rectangles are closed + * regions, i.e. they contain their boundary. + */ + public boolean contains(R2Vector p) { + return x().contains(p.x()) && y().contains(p.y()); + } + + /** + * Returns true if and only if the given point is contained in the interior of the region (i.e. + * the region excluding its boundary). + */ + public boolean interiorContains(R2Vector p) { + return x().interiorContains(p.x()) && y().interiorContains(p.y()); + } + + /** Returns true if and only if this rectangle contains the given other rectangle. */ + public boolean contains(R2Rect other) { + return x().contains(other.x()) && y().contains(other.y()); + } + + /** + * Returns true if and only if the interior of this rectangle contains all points of the given + * other rectangle (including its boundary). + */ + public boolean interiorContains(R2Rect other) { + return x().interiorContains(other.x()) && y().interiorContains(other.y()); + } + + /** Returns true if this rectangle and the given other rectangle have any points in common. */ + public boolean intersects(R2Rect other) { + return x().intersects(other.x()) && y().intersects(other.y()); + } + + /** + * Return true if and only if the interior of this rectangle intersects any point (including the + * boundary) of the given other rectangle. + */ + public boolean interiorIntersects(R2Rect other) { + return x().interiorIntersects(other.x()) && y().interiorIntersects(other.y()); + } + + /** + * Increase the size of the bounding rectangle to include the given point. This rectangle is + * expanded by the minimum amount possible. + */ + public void addPoint(R2Vector p) { + x.unionInternal(p.x()); + y.unionInternal(p.y()); + } + + /** + * Expand the rectangle to include the given other rectangle. This is the same as replacing the + * rectangle by the union of the two rectangles, but is somewhat more efficient. + */ + public void addRect(R2Rect other) { + x.unionInternal(other.x); + y.unionInternal(other.y); + } + + /** + * Return the closest point in this rectangle to the given point "p". This rectangle must be + * non-empty. + */ + public R2Vector clampPoint(R2Vector p) { + return new R2Vector(x().clampPoint(p.x()), y().clampPoint(p.y())); + } + + /** + * Return a rectangle that has been expanded on each side in the x-direction by margin.x(), and on + * each side in the 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. + */ + @CheckReturnValue + public R2Rect expanded(R2Vector margin) { + R1Interval xx = x().expanded(margin.x()); + R1Interval yy = y().expanded(margin.y()); + if (xx.isEmpty() || yy.isEmpty()) { + return empty(); + } else { + return new R2Rect(xx, yy); + } + } + + /** + * Returns a rectangle that has been expanded on both sides by the given margin. Any expansion of + * an empty rectangle remains empty. + */ + @CheckReturnValue + public R2Rect expanded(double margin) { + return expanded(new R2Vector(margin, margin)); + } + + /** + * Expands this rectangle on both axes by the given margin. + * + *

Package private since only S2 classes are intended to mutate R2Rects for now. + */ + void expand(double margin) { + if (!isEmpty()) { + x.expandedInternal(margin); + y.expandedInternal(margin); + } + } + + /** + * Returns the smallest rectangle containing the union of this rectangle and the given rectangle. + */ + @CheckReturnValue + public R2Rect union(R2Rect other) { + return new R2Rect(x().union(other.x()), y().union(other.y())); + } + + /** + * Returns the smallest rectangle containing the intersection of this rectangle and the given + * rectangle. + */ + @CheckReturnValue + public R2Rect intersection(R2Rect other) { + R1Interval xx = x().intersection(other.x()); + R1Interval yy = y().intersection(other.y()); + if (xx.isEmpty() || yy.isEmpty()) { + return empty(); + } + return new R2Rect(xx, yy); + } + + /** Returns a simple convolution hashcodes from the x and y internals. */ + @Override + public int hashCode() { + return x.hashCode() * 701 + y.hashCode(); + } + + /** Returns true if two rectangles contains the same set of points. */ + @Override + public boolean equals(Object other) { + if (other instanceof R2Rect) { + R2Rect r2 = (R2Rect) other; + return x().equals(r2.x()) && y().equals(r2.y()); + } else { + return false; + } + } + + /** + * Returns true if the x- and y-intervals of the two rectangles are the same up to the given + * tolerance. See {@link R1Interval} for details on approximate interval equality. + */ + public boolean approxEquals(R2Rect other) { + return approxEquals(other, 1e-15); + } + + /** + * Returns true if the given rectangles are equal to within {@code maxError}. See {@link + * R1Interval} for details on approximate interval equality. + */ + public boolean approxEquals(R2Rect other, double maxError) { + return x().approxEquals(other.x(), maxError) && y().approxEquals(other.y(), maxError); + } + + /** Returns a simple string representation of this rectangle's lower and upper corners. */ + @Override + public String toString() { + return "[Lo" + lo() + ", Hi" + hi() + "]"; + } +} diff --git a/core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/R2Vector.java b/core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/R2Vector.java new file mode 100644 index 0000000000..8ecb41f634 --- /dev/null +++ b/core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/R2Vector.java @@ -0,0 +1,199 @@ +/* + * Copyright 2005 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.mogo.eagle.core.utilcode.geometry; + +import com.google.common.annotations.GwtCompatible; +import java.io.Serializable; + +/** + * R2Vector represents a vector in the two-dimensional space. It defines the basic geometrical + * operations for 2D vectors, e.g. cross product, addition, norm, comparison, etc. + * + */ +@GwtCompatible(serializable = true) +public final strictfp class R2Vector implements Serializable { + double x; + double y; + + /** Constructs a new R2Vector at the origin [0,0] of the R2 coordinate system. */ + public R2Vector() { + this(0, 0); + } + + /** Constructs a new R2 vector from the given x and y coordinates. */ + public R2Vector(double x, double y) { + this.x = x; + this.y = y; + } + + /** Constructs a new R2 vector from the given coordinates array, which must have length 2. */ + public R2Vector(double[] coord) { + if (coord.length != 2) { + throw new IllegalStateException("Points must have exactly 2 coordinates"); + } + x = coord[0]; + y = coord[1]; + } + + /** Returns the x coordinate of this R2 vector. */ + public double x() { + return x; + } + + /** Returns the y coordinate of this R2 vector. */ + public double y() { + return y; + } + + /** + * Returns the coordinate of the given axis, which will be the x axis if index is 0, and the y + * axis if index is 1. + * + * @throws ArrayIndexOutOfBoundsException Thrown if the given index is not 0 or 1. + */ + public double get(int index) { + if (index < 0 || index > 1) { + throw new ArrayIndexOutOfBoundsException(index); + } + return index == 0 ? this.x : this.y; + } + + /** + * Sets the position of this vector from the given other vector. Package private since this is + * only mutable for S2. + */ + void set(R2Vector v) { + this.x = v.x(); + this.y = v.y(); + } + + /** + * Sets the position of this vector from the given values. Package private since this is only + * mutable for S2. + */ + void set(double x, double y) { + this.x = x; + this.y = y; + } + + /** Returns the vector result of {@code p1 - p2}. */ + public static R2Vector add(final R2Vector p1, final R2Vector p2) { + return new R2Vector(p1.x + p2.x, p1.y + p2.y); + } + + /** Returns the vector result of {@code p1 - p2}. */ + public static R2Vector sub(final R2Vector p1, final R2Vector p2) { + return new R2Vector(p1.x - p2.x, p1.y - p2.y); + } + + /** + * Returns the element-wise multiplication of p1 and p2, e.g. {@code vector [p1.x*p2.x, + * p1.y*p2.y]}. + */ + public static R2Vector mul(final R2Vector p, double m) { + return new R2Vector(m * p.x, m * p.y); + } + + /** Returns the vector magnitude. */ + public double norm() { + return Math.sqrt(norm2()); + } + + /** Returns the square of the vector magnitude. */ + public double norm2() { + return (x * x) + (y * y); + } + + /** + * Returns a new vector scaled to magnitude 1, or a copy of the original vector if magnitude was + * 0. + */ + public static R2Vector normalize(R2Vector vector) { + double n = vector.norm(); + if (n != 0) { + return mul(vector, 1.0 / n); + } else { + return new R2Vector(vector.x, vector.y); + } + } + + /** + * Returns a new R2 vector orthogonal to the current one with the same norm and counterclockwise + * to it. + */ + public R2Vector ortho() { + return new R2Vector(-y, x); + } + + /** Returns the dot product of the given vectors. */ + public static double dotProd(final R2Vector p1, final R2Vector p2) { + return (p1.x * p2.x) + (p1.y * p2.y); + } + + /** Returns the dot product of this vector with that vector. */ + public double dotProd(R2Vector that) { + return dotProd(this, that); + } + + /** Returns the cross product of this vector with that vector. */ + public double crossProd(final R2Vector that) { + return this.x * that.y - this.y * that.x; + } + + /** + * Returns true if this vector is less than that vector, with the x-axis as the primary sort key + * and the y-axis as the secondary sort key. + */ + public boolean lessThan(R2Vector that) { + if (x < that.x) { + return true; + } + if (that.x < x) { + return false; + } + if (y < that.y) { + return true; + } + return false; + } + + /** Returns true if that object is an R2Vector with exactly the same x and y coordinates. */ + @Override + public boolean equals(Object that) { + if (!(that instanceof R2Vector)) { + return false; + } + R2Vector thatPoint = (R2Vector) that; + return this.x == thatPoint.x && this.y == thatPoint.y; + } + + /** + * Calcualates hashcode based on stored coordinates. Since we want +0.0 and -0.0 to be treated the + * same, we ignore the sign of the coordinates. + */ + @Override + public int hashCode() { + long value = 17; + value += 37 * value + Double.doubleToLongBits(Math.abs(x)); + value += 37 * value + Double.doubleToLongBits(Math.abs(y)); + return (int) (value ^ (value >>> 32)); + } + + @Override + public String toString() { + return "(" + x + ", " + y + ")"; + } +} diff --git a/core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/Real.java b/core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/Real.java new file mode 100644 index 0000000000..fe1fec67e9 --- /dev/null +++ b/core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/Real.java @@ -0,0 +1,345 @@ +/* + * Copyright 2014 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.mogo.eagle.core.utilcode.geometry; + +import com.google.common.annotations.GwtIncompatible; +import java.math.BigDecimal; + +/** + * This class provides portable support for several exact arithmetic operations on double values, + * without loss of precision. It stores an array of double values, and operations that require + * additional bits of precision return Reals with larger arrays. + * + *

Converting a sequence of a dozen strictfp arithmetic operations to use Real can take up to 20 + * times longer than the natural but imprecise approach of using built in double operators like + + * and *. Compared to other approaches like BigDecimal that consume more memory and typically slow + * operations down by a factor of 100, that's great, but use of this class should still be avoided + * when imprecise results will suffice. + * + *

This class exists as a package private element of the geometry library for the predicates in + * {@link S2Predicates}, that require arbitrary precision arithmetic. It could be made suitable for + * general usage by adding robust implementations of multiplication and division between two Reals, + * and a toString() implementation that prints the exact summation of all the components. + * + *

Many of the algorithms in this class were adapted from the multiple components technique for + * extended 64-bit IEEE 754 floating point precision, as described in: + * + *

+ * Robust Adaptive Floating-Point Geometric Predicates
+ * Jonathan Richard Shewchuk
+ * School of Computer Science
+ * Carnegie Mellon University
+ * 
+ * + *

Faster adaptive techniques are also presented in that paper, but are not implemented here. + */ +@GwtIncompatible(value = "No javascript support for strictfp.") +strictfp class Real extends Number { + private static final long serialVersionUID = 1L; + + /** + * Used to split doubles into two half-length values, for exact multiplication. The value should + * be Math.pow(2, Math.ceil(mantissaBits / 2)) + 1. + */ + private static final double SPLITTER; + + static { + // Find half ulp(1). We could use Math.ulp but it's not supported on GWT. + double epsilon = 1.0; + do { + epsilon *= 0.5; + } while (1.0 + epsilon != 1.0); + int mantissaBits = (int) Math.round(-Math.log(epsilon) / Math.log(2)); + SPLITTER = (1 << ((mantissaBits + 1) / 2)) + 1; + } + + /** Returns the result of a + b, without loss of precision. */ + public static Real add(double a, double b) { + double x = a + b; + double error = twoSumError(a, b, x); + return new Real(error, x); + } + + /** Returns the result of a - b, without loss of precision. */ + public static Real sub(double a, double b) { + double x = a - b; + double error = twoDiffError(a, b, x); + return new Real(error, x); + } + + /** Returns the result of a * b, without loss of precision. */ + public static Real mul(double a, double b) { + double x = a * b; + double bhi = splitHigh(b); + double blo = splitLow(b, bhi); + double error = twoProductError(a, bhi, blo, x); + return new Real(error, x); + } + + /** + * A sequence of ordinary double values, ordered by magnitude in ascending order, containing no + * zeroes and with no overlapping base 2 digits. + */ + private final double[] values; + + /** Creates a Real based on the given double value. */ + public Real(double value) { + values = new double[] {value}; + } + + private Real(double... values) { + this.values = values; + } + + /** Returns the result of a + b, without loss of precision. */ + public Real add(Real that) { + return add(this, that, false); + } + + /** Returns the result of a - b, without loss of precision. */ + public Real sub(Real that) { + return add(this, that, true); + } + + /** + * Returns the result of adding together the components of a and b, inverting each element of b if + * negateB is true. + */ + private static Real add(Real a, Real b, boolean negateB) { + double bSign = negateB ? -1 : 1; + double[] result = new double[a.values.length + b.values.length]; + int aIndex = 0; + int bIndex = 0; + + double sum; + double newSum; + double error; + if (smallerMagnitude(a.values[aIndex], b.values[bIndex])) { + sum = a.values[aIndex++]; + } else { + sum = bSign * b.values[bIndex++]; + } + + int resultIndex = 0; + double smaller; + if ((aIndex < a.values.length) && (bIndex < b.values.length)) { + if (smallerMagnitude(a.values[aIndex], b.values[bIndex])) { + smaller = a.values[aIndex++]; + } else { + smaller = bSign * b.values[bIndex++]; + } + newSum = smaller + sum; + error = fastTwoSumError(smaller, sum, newSum); + sum = newSum; + if (error != 0.0) { + result[resultIndex++] = error; + } + while ((aIndex < a.values.length) && (bIndex < b.values.length)) { + if (smallerMagnitude(a.values[aIndex], b.values[bIndex])) { + smaller = a.values[aIndex++]; + } else { + smaller = bSign * b.values[bIndex++]; + } + newSum = sum + smaller; + error = twoSumError(sum, smaller, newSum); + sum = newSum; + if (error != 0.0) { + result[resultIndex++] = error; + } + } + } + while (aIndex < a.values.length) { + smaller = a.values[aIndex++]; + newSum = sum + smaller; + error = twoSumError(sum, smaller, newSum); + sum = newSum; + if (error != 0.0) { + result[resultIndex++] = error; + } + } + while (bIndex < b.values.length) { + smaller = bSign * b.values[bIndex++]; + newSum = sum + smaller; + error = twoSumError(sum, smaller, newSum); + sum = newSum; + if (error != 0.0) { + result[resultIndex++] = error; + } + } + if ((sum != 0.0) || (resultIndex == 0)) { + result[resultIndex++] = sum; + } + + if (result.length > resultIndex) { + result = copyOf(result, resultIndex); + } + return new Real(result); + } + + /** Returns true if the magnitude of a is less than the magnitude of b. */ + private static boolean smallerMagnitude(double a, double b) { + return (b > a) == (b > -a); + } + + /** Returns the result of this * scale, without loss of precision. */ + public Real mul(double scale) { + double[] result = new double[values.length * 2]; + double scaleHigh = splitHigh(scale); + double scaleLow = splitLow(scale, scaleHigh); + double quotient = values[0] * scale; + double error = twoProductError(values[0], scaleHigh, scaleLow, quotient); + int resultIndex = 0; + if (error != 0) { + result[resultIndex++] = error; + } + for (int i = 1; i < values.length; i++) { + double term = values[i] * scale; + double termError = twoProductError(values[i], scaleHigh, scaleLow, term); + double sum = quotient + termError; + error = twoSumError(quotient, termError, sum); + if (error != 0) { + result[resultIndex++] = error; + } + quotient = term + sum; + error = fastTwoSumError(term, sum, quotient); + if (error != 0) { + result[resultIndex++] = error; + } + } + if ((quotient != 0.0) || (resultIndex == 0)) { + result[resultIndex++] = quotient; + } + if (result.length > resultIndex) { + result = copyOf(result, resultIndex); + } + return new Real(result); + } + + /** Returns the negative of this number. */ + public Real negate() { + double[] copy = new double[values.length]; + for (int i = values.length - 1; i >= 0; i--) { + copy[i] = -values[i]; + } + return new Real(copy); + } + + /** Returns the signum of this number more quickly than via Math.signum(doubleValue()). */ + public int signum() { + double msb = values[values.length - 1]; + if (msb > 0) { + return 1; + } else if (msb < 0) { + return -1; + } else { + return 0; + } + } + + /** Returns the string representation of the double value nearest this Real. */ + @Override + public String toString() { + return Double.toString(doubleValue()); + } + + @Override + public int intValue() { + return (int) longValue(); + } + + @Override + public long longValue() { + return Math.round(doubleValue()); + } + + @Override + public float floatValue() { + return (float) doubleValue(); + } + + @Override + public double doubleValue() { + // Since the components are guaranteed to have no overlapping digits, we + // could simply sum them without loss of precision... but to return a double + // we truncate to the 53 bits of the largest exponent. + double sum = 0; + for (double value : values) { + sum += value; + } + return sum; + } + + /** Returns a BigDecimal representation of this extended precision real value. */ + public BigDecimal bigValue() { + BigDecimal sum = new BigDecimal(values[0]); + for (int i = 1; i < values.length; i++) { + sum = sum.add(new BigDecimal(values[i])); + } + return sum.stripTrailingZeros(); + } + + private static double[] copyOf(double[] array, int newLength) { + double[] result = new double[newLength]; + for (int i = 0; i < newLength; i++) { + result[i] = array[i]; + } + return result; + } + + /** Returns the error in the sum x=a+b, when |a|>=|b|. */ + private static double fastTwoSumError(double a, double b, double x) { + return b - (x - a); + } + + /** + * Returns the error in the sum x=a+b, when the relative magnitudes of a and b are not known in + * advance. + */ + private static double twoSumError(double a, double b, double x) { + double error = x - a; + return (a - (x - error)) + (b - error); + } + + /** Returns the error in the difference x=a-b. */ + private static double twoDiffError(double a, double b, double x) { + double error = a - x; + return (a - (x + error)) + (error - b); + } + + /** Returns the high split for the given value. */ + private static double splitHigh(double a) { + double c = SPLITTER * a; + return c - (c - a); + } + + /** + * Returns the low split for the given value and previously-computed high split as returned by + * {@link #splitHigh(double)}. + */ + private static double splitLow(double a, double ahi) { + return a - ahi; + } + + /** Returns the error in the product x=a*b, with precomputed splits for b. */ + private static double twoProductError(double a, double bhi, double blo, double x) { + double ahi = splitHigh(a); + double alo = splitLow(a, ahi); + double err1 = x - (ahi * bhi); + double err2 = err1 - (alo * bhi); + double err3 = err2 - (ahi * blo); + return (alo * blo) - err3; + } +} diff --git a/core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/S1Angle.java b/core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/S1Angle.java new file mode 100644 index 0000000000..ed9e6a9903 --- /dev/null +++ b/core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/S1Angle.java @@ -0,0 +1,285 @@ +/* + * Copyright 2005 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.mogo.eagle.core.utilcode.geometry; + +import com.google.common.annotations.GwtCompatible; +import com.google.common.primitives.Ints; +import java.io.Serializable; +import javax.annotation.CheckReturnValue; + +@GwtCompatible(serializable = true) +public final strictfp class S1Angle implements Comparable, Serializable { + /** An angle larger than any finite angle. */ + public static final S1Angle INFINITY = new S1Angle(Double.POSITIVE_INFINITY); + + /** An explicit shorthand for the default constructor. */ + public static final S1Angle ZERO = new S1Angle(); + + private final double radians; + + /** Returns the angle in radians. */ + public double radians() { + return radians; + } + + /** Returns the angle in degrees. */ + public double degrees() { + return radians * (180 / Math.PI); + } + + /** + * Returns angle in tens of microdegrees, rounded to the nearest ten microdegrees. + * + *

Normalized angles will never overflow an int. + * + * @throws IllegalArgumentException if the result overflows an int + */ + public int e5() { + return Ints.checkedCast(Math.round(degrees() * 1e5)); + } + + /** + * Returns angle in microdegrees, rounded to the nearest microdegree. + * + *

Normalized angles will never overflow an int. + * + * @throws IllegalArgumentException if the result overflows an int + */ + public int e6() { + return Ints.checkedCast(Math.round(degrees() * 1e6)); + } + + /** + * Returns angle in tenths of a microdegree, rounded to the nearest tenth of a microdegree. + * + *

Normalized angles will never overflow an int. + * + * @throws IllegalArgumentException if the result overflows an int + */ + public int e7() { + return Ints.checkedCast(Math.round(degrees() * 1e7)); + } + + /** The default constructor yields a zero angle. */ + public S1Angle() { + this.radians = 0; + } + + private S1Angle(double radians) { + this.radians = radians; + } + + /** + * Return the angle between two points, which is also equal to the distance between these points + * on the unit sphere. The points do not need to be normalized. + */ + public S1Angle(S2Point x, S2Point y) { + this.radians = x.angle(y); + } + + @Override + public boolean equals(Object that) { + if (that instanceof S1Angle) { + return this.radians == ((S1Angle) that).radians; + } + return false; + } + + @Override + public int hashCode() { + long value = Double.doubleToLongBits(radians); + return (int) (value ^ (value >>> 32)); + } + + public boolean lessThan(S1Angle that) { + return this.radians < that.radians; + } + + public boolean greaterThan(S1Angle that) { + return this.radians > that.radians; + } + + public boolean lessOrEquals(S1Angle that) { + return this.radians <= that.radians; + } + + public boolean greaterOrEquals(S1Angle that) { + return this.radians >= that.radians; + } + + public static S1Angle max(S1Angle left, S1Angle right) { + return right.greaterThan(left) ? right : left; + } + + public static S1Angle min(S1Angle left, S1Angle right) { + return right.greaterThan(left) ? left : right; + } + + /** Returns a new S1Angle specified in radians. */ + public static S1Angle radians(double radians) { + return new S1Angle(radians); + } + + /** + * Returns a new S1Angle converted from degrees. Note that degrees(x).degrees() == x + * may not hold due to inexact arithmetic. + */ + public static S1Angle degrees(double degrees) { + return new S1Angle(degrees * (Math.PI / 180)); + } + + /** Returns a new S1Angle converted from tens of microdegrees. */ + public static S1Angle e5(int e5) { + return degrees(e5 * 1e-5); + } + + /** Returns a new S1Angle converted from microdegrees. */ + public static S1Angle e6(int e6) { + // Multiplying by 1e-6 isn't quite as accurate as dividing by 1e6, + // but it's about 10 times faster and more than accurate enough. + return degrees(e6 * 1e-6); + } + + /** Returns a new S1Angle converted from tenths of a microdegree. */ + public static S1Angle e7(int e7) { + return degrees(e7 * 1e-7); + } + + /** Returns the distance along the surface of a sphere of the given radius. */ + public double distance(double radius) { + return radians * radius; + } + + public S1Angle neg() { + return new S1Angle(-radians); + } + + /** + * Retuns an {@link S1Angle} whose angle is (this + a). + */ + @CheckReturnValue + public S1Angle add(S1Angle a) { + return new S1Angle(radians + a.radians); + } + + /** + * Retuns an {@link S1Angle} whose angle is (this - a). + */ + @CheckReturnValue + public S1Angle sub(S1Angle a) { + return new S1Angle(radians - a.radians); + } + + /** + * Retuns an {@link S1Angle} whose angle is (this * m). + */ + @CheckReturnValue + public S1Angle mul(double m) { + return new S1Angle(radians * m); + } + + /** + * Retuns an {@link S1Angle} whose angle is (this / d). + */ + @CheckReturnValue + public S1Angle div(double d) { + return new S1Angle(radians / d); + } + + /** + * Returns the trigonometric cosine of the angle. + */ + public double cos() { + return Math.cos(radians); + } + + /** + * Returns the trigonometric sine of the angle. + */ + public double sin() { + return Math.sin(radians); + } + + /** + * Returns the trigonometric tangent of the angle. + */ + public double tan() { + return Math.tan(radians); + } + + /** + * Returns the angle normalized to the range (-180, 180] degrees. + */ + @CheckReturnValue + public S1Angle normalize() { + final boolean isNormalized = radians > -Math.PI && radians <= Math.PI; + if (isNormalized) { + return this; + } + double normalized = Platform.IEEEremainder(radians, 2.0 * Math.PI); + if (normalized <= -Math.PI) { + normalized = Math.PI; + } + assert normalized > -Math.PI; + assert normalized <= Math.PI; + return new S1Angle(normalized); + } + + /** + * Writes the angle in degrees with a "d" suffix, e.g. "17.3745d". By default 6 digits are + * printed; this can be changed using setprecision(). Up to 17 digits are required to distinguish + * one angle from another. + */ + @Override + public String toString() { + return degrees() + "d"; + } + + @Override + public int compareTo(S1Angle that) { + return this.radians < that.radians ? -1 : this.radians > that.radians ? 1 : 0; + } + + /** Creates a new Builder initialized to a copy of this angle. */ + public Builder toBuilder() { + return new Builder().add(this); + } + + /** A builder of {@link S1Angle} instances. */ + public static final class Builder { + private double radians; + + /** Constructs a new builder initialized to {@link #ZERO}. */ + public Builder() {} + + /** Adds angle. */ + public Builder add(S1Angle angle) { + radians += angle.radians; + return this; + } + + /** Adds radians. */ + public Builder add(double radians) { + this.radians += radians; + return this; + } + + /** Returns a new {@link S1Angle} copied from the current state of this builder. */ + public S1Angle build() { + return new S1Angle(radians); + } + } +} diff --git a/core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/S1ChordAngle.java b/core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/S1ChordAngle.java new file mode 100644 index 0000000000..b4455cd483 --- /dev/null +++ b/core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/S1ChordAngle.java @@ -0,0 +1,363 @@ +/* + * Copyright 2014 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.mogo.eagle.core.utilcode.geometry; + +import static com.google.common.base.Preconditions.checkArgument; +import static java.lang.Math.asin; +import static java.lang.Math.sqrt; + +import com.google.common.annotations.GwtCompatible; +import com.google.common.primitives.Doubles; +import java.io.Serializable; + +/** + * S1ChordAngle represents the angle subtended by a chord (i.e., the straight 3D Cartesian line + * segment connecting two points on the unit sphere). Its representation makes it very efficient for + * computing and comparing distances, but unlike S1Angle it is only capable of representing angles + * between 0 and Pi radians. Generally, S1ChordAngle should only be used in loops where many angles + * need to be calculated and compared. Otherwise it is simpler to use S1Angle. + * + *

S1ChordAngle also loses some accuracy as the angle approaches Pi radians. Specifically, the + * representation of (Pi - x) radians can be expected to have an error of about (1e-15 / x), with a + * maximum error of about 1e-7. + */ +@GwtCompatible(serializable = true) +public final strictfp class S1ChordAngle implements Comparable, Serializable { + + /** Max value that can be returned from {@link #getLength2()}. */ + public static final double MAX_LENGTH2 = 4.0; + + /** The zero chord angle. */ + public static final S1ChordAngle ZERO = new S1ChordAngle(0); + + /** The chord angle of 90 degrees (a "right angle"). */ + public static final S1ChordAngle RIGHT = new S1ChordAngle(2); + + /** The chord angle of 180 degrees (a "straight angle"). This is the max finite chord angle. */ + public static final S1ChordAngle STRAIGHT = new S1ChordAngle(MAX_LENGTH2); + + /** + * A chord angle larger than any finite chord angle. The only valid operations on {@code INFINITY} + * are comparisons and {@link S1Angle} conversions. + */ + public static final S1ChordAngle INFINITY = new S1ChordAngle(Double.POSITIVE_INFINITY); + + /** + * A chord angle smaller than {@link #ZERO}. The only valid operations on {@code NEGATIVE} are + * comparisons and {@link S1Angle} conversions. + */ + public static final S1ChordAngle NEGATIVE = new S1ChordAngle(-1); + + private final double length2; + + /** + * Constructs the S1ChordAngle corresponding to the distance between the two given points. The + * points must be unit length. + */ + public S1ChordAngle(S2Point x, S2Point y) { + checkArgument(S2.isUnitLength(x)); + checkArgument(S2.isUnitLength(y)); + // The distance may slightly exceed 4.0 due to roundoff errors. + length2 = Math.min(MAX_LENGTH2, x.getDistance2(y)); + checkArgument(isValid()); + } + + /** + * Returns a new chord angle approximated from {@code angle} (see {@link + * #getS1AngleConstructorMaxError()} for the max magnitude of the error). + * + *

Angles outside the range [0, Pi] are handled as follows: + * + *

    + *
  • {@link S1Angle#INFINITY} is mapped to {@link #INFINITY} + *
  • negative angles are mapped to {@link #NEGATIVE} + *
  • finite angles larger than Pi are mapped to {@link #STRAIGHT} + *
+ * + *

Note that this operation is relatively expensive and should be avoided. To use {@link + * S1ChordAngle} effectively, you should structure your code so that input arguments are converted + * to S1ChordAngles at the beginning of your algorithm, and results are converted back to {@link + * S1Angle}s only at the end. + */ + public static S1ChordAngle fromS1Angle(S1Angle angle) { + if (angle.radians() < 0) { + return NEGATIVE; + } else if (angle.equals(S1Angle.INFINITY)) { + return INFINITY; + } else { + // The chord length is 2 * sin(angle / 2). + double length = 2 * Math.sin(0.5 * Math.min(Math.PI, angle.radians())); + return new S1ChordAngle(length * length); + } + } + + /** + * S1ChordAngles are represented by the squared chord length, which can range from 0 to {@code + * MAX_LENGTH2}. {@link #INFINITY} uses an infinite squared length. + */ + private S1ChordAngle(double length2) { + this.length2 = length2; + checkArgument(isValid()); + } + + /** + * Construct an S1ChordAngle from the squared chord length. Note that the argument is + * automatically clamped to a maximum of {@code MAX_LENGTH2} to handle possible roundoff errors. + * The argument must be non-negative. + */ + public static S1ChordAngle fromLength2(double length2) { + return new S1ChordAngle(Math.min(MAX_LENGTH2, length2)); + } + + /** Returns whether the chord distance is exactly 0. */ + public boolean isZero() { + return length2 == 0; + } + + /** Returns whether the chord distance is negative. */ + public boolean isNegative() { + return length2 < 0; + } + + /** Returns whether the chord distance is exactly (positive) infinity. */ + public boolean isInfinity() { + return length2 == Double.POSITIVE_INFINITY; + } + + /** Returns true if the angle is negative or infinity. */ + public boolean isSpecial() { + return isNegative() || isInfinity(); + } + + /** + * Returns true if getLength2() is within the normal range of 0 to 4 (inclusive) or the angle is + * special. + */ + public boolean isValid() { + return (length2 >= 0 && length2 <= MAX_LENGTH2) || isNegative() || isInfinity(); + } + + /** + * Convert the chord angle to an {@link S1Angle}. {@link #INFINITY} is converted to {@link + * S1Angle#INFINITY}, and {@link #NEGATIVE} is converted to a negative {@link S1Angle}. This + * operation is relatively expensive. + */ + public S1Angle toAngle() { + if (isNegative()) { + return S1Angle.radians(-1); + } else if (isInfinity()) { + return S1Angle.INFINITY; + } else { + return S1Angle.radians(2 * asin(0.5 * sqrt(length2))); + } + } + + /** The squared length of the chord. (Most clients will not need this.) */ + public double getLength2() { + return length2; + } + + /** + * Returns the smallest representable S1ChordAngle larger than this object. This can be used to + * convert a "<" comparison to a "<=" comparison. + * + *

Note the following special cases: + * + *

    + *
  • NEGATIVE.successor() == ZERO + *
  • STRAIGHT.successor() == INFINITY + *
  • INFINITY.Successor() == INFINITY + *
+ */ + public S1ChordAngle successor() { + if (length2 >= MAX_LENGTH2) { + return INFINITY; + } + if (length2 < 0.0) { + return ZERO; + } + return new S1ChordAngle(Platform.nextAfter(length2, 10.0)); + } + + /** + * As {@link #successor}, but returns the largest representable S1ChordAngle less than this + * object. + * + *

Note the following special cases: + * + *

    + *
  • INFINITY.predecessor() == STRAIGHT + *
  • ZERO.predecessor() == NEGATIVE + *
  • NEGATIVE.predecessor() == NEGATIVE + *
+ */ + public S1ChordAngle predecessor() { + if (length2 <= 0.0) { + return NEGATIVE; + } + if (length2 > MAX_LENGTH2) { + return STRAIGHT; + } + return new S1ChordAngle(Platform.nextAfter(length2, -10.0)); + } + + /** + * Returns a new S1ChordAngle whose chord distance represents the sum of the angular distances + * represented by the 'a' and 'b' chord angles. + * + *

Note that this method is much more efficient than converting the chord angles to S1Angles + * and adding those. It requires only one square root plus a few additions and multiplications. + */ + public static S1ChordAngle add(S1ChordAngle a, S1ChordAngle b) { + checkArgument(!a.isSpecial()); + checkArgument(!b.isSpecial()); + + // Optimization for the common case where "b" is an error tolerance parameter that happens to be + // set to zero. + double a2 = a.length2; + double b2 = b.length2; + if (b2 == 0) { + return a; + } + + // Clamp the angle sum to at most 180 degrees. + if (a2 + b2 >= MAX_LENGTH2) { + return S1ChordAngle.STRAIGHT; + } + + // Let "a" and "b" be the (non-squared) chord lengths, and let c = a+b. + // Let A, B, and C be the corresponding half-angles (a = 2*sin(A), etc). + // Then the formula below can be derived from c = 2 * sin(A+B) and the relationships + // sin(A+B) = sin(A)*cos(B) + sin(B)*cos(A) + // cos(X) = sqrt(1 - sin^2(X)) . + double x = a2 * (1 - 0.25 * b2); // isValid() => non-negative + double y = b2 * (1 - 0.25 * a2); // isValid() => non-negative + return new S1ChordAngle(Math.min(MAX_LENGTH2, x + y + 2 * sqrt(x * y))); + } + + /** + * Subtract one S1ChordAngle from another. + * + *

Note that this method is much more efficient than converting the chord angles to S1Angles + * and adding those. It requires only one square root plus a few additions and multiplications. + */ + public static S1ChordAngle sub(S1ChordAngle a, S1ChordAngle b) { + // See comments in add(S1ChordAngle, S1ChordAngle). + checkArgument(!a.isSpecial()); + checkArgument(!b.isSpecial()); + double a2 = a.length2; + double b2 = b.length2; + if (b2 == 0) { + return a; + } + if (a2 <= b2) { + return S1ChordAngle.ZERO; + } + double x = a2 * (1 - 0.25 * b2); + double y = b2 * (1 - 0.25 * a2); + return new S1ChordAngle(Math.max(0.0, x + y - 2 * sqrt(x * y))); + } + + /** Returns the smaller of the given instances. */ + public static S1ChordAngle min(S1ChordAngle a, S1ChordAngle b) { + return a.length2 <= b.length2 ? a : b; + } + + /** Returns the larger of the given instances. */ + public static S1ChordAngle max(S1ChordAngle a, S1ChordAngle b) { + return a.length2 > b.length2 ? a : b; + } + + /** Returns the square of Math.sin(toAngle().radians()), but computed more efficiently. */ + public static double sin2(S1ChordAngle a) { + checkArgument(!a.isSpecial()); + // Let "a" be the (non-squared) chord length, and let A be the corresponding half-angle + // (a = 2*sin(A)). The formula below can be derived from: + // sin(2*A) = 2 * sin(A) * cos(A) + // cos^2(A) = 1 - sin^2(A) + // This is much faster than converting to an angle and computing its sine. + return a.length2 * (1 - 0.25 * a.length2); + } + + /** Returns Math.sin(toAngle().radians()), but computed more efficiently. */ + public static double sin(S1ChordAngle a) { + return sqrt(sin2(a)); + } + + /** Returns Math.cos(toAngle().radians()), but computed more efficiently. */ + public static double cos(S1ChordAngle a) { + // cos(2*A) = cos^2(A) - sin^2(A) = 1 - 2*sin^2(A) + checkArgument(!a.isSpecial()); + return 1 - 0.5 * a.length2; + } + + /** Returns Math.tan(toAngle().radians()), but computed more efficiently. */ + public static double tan(S1ChordAngle a) { + return sin(a) / cos(a); + } + + /** + * Returns a new S1ChordAngle that has been adjusted by the given error bound (which can be + * positive or negative). {@code error} should be the value returned by one of the error bound + * methods below. For example: + * + *

+   *    {@code S1ChordAngle a = new S1ChordAngle(x, y);}
+   *    {@code S1ChordAngle a1 = a.plusError(a.getS2PointConstructorMaxError());}
+   * 
+ * + *

If this {@link #isSpecial}, we return {@code this}. + */ + public S1ChordAngle plusError(double error) { + return isSpecial() ? this : fromLength2(Math.max(0.0, Math.min(MAX_LENGTH2, length2 + error))); + } + + /** Returns the error in {@link #fromS1Angle}. */ + public double getS1AngleConstructorMaxError() { + return S2.DBL_EPSILON * length2; + } + + /** + * There is a relative error of {@code 2.5 * DBL_EPSILON} when computing the squared distance, + * plus a relative error of {@code 2 * DBL_EPSILON} and an absolute error of {@code 16 * + * DBL_EPSILON^2} because the lengths of the input points may differ from 1 by up to {@code 2 * + * DBL_EPSILON} each. (This is the maximum length error in {@link S2Point#normalize}). + */ + public double getS2PointConstructorMaxError() { + return (4.5 * S2.DBL_EPSILON * length2) + (16 * S2.DBL_EPSILON * S2.DBL_EPSILON); + } + + /** Returns the string of the closest {@link S1Angle} to this chord distance. */ + @Override + public String toString() { + return toAngle().toString(); + } + + @Override + public int compareTo(S1ChordAngle that) { + return Double.compare(this.length2, that.length2); + } + + @Override + public boolean equals(Object other) { + return (other instanceof S1ChordAngle) && length2 == ((S1ChordAngle) other).length2; + } + + @Override + public int hashCode() { + return length2 == 0.0 ? 0 : Doubles.hashCode(length2); + } +} diff --git a/core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/S1Interval.java b/core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/S1Interval.java new file mode 100644 index 0000000000..6e8d884e49 --- /dev/null +++ b/core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/S1Interval.java @@ -0,0 +1,670 @@ +/* + * Copyright 2005 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.mogo.eagle.core.utilcode.geometry; + +import com.google.common.annotations.GwtCompatible; +import java.io.Serializable; +import javax.annotation.CheckReturnValue; + +/** + * An S1Interval represents a closed interval on a unit circle (also known as a 1-dimensional + * sphere). It is capable of representing the empty interval (containing no points), the full + * interval (containing all points), and zero-length intervals (containing a single point). + * + *

Points are represented by the angle they make with the positive x-axis in the range [-Pi, Pi]. + * An interval is represented by its lower and upper bounds (both inclusive, since the interval is + * closed). The lower bound may be greater than the upper bound, in which case the interval is + * "inverted" (i.e. it passes through the point (-1, 0)). + * + *

Note that the point (-1, 0) has two valid representations, Pi and -Pi. The normalized + * representation of this point internally is Pi, so that endpoints of normal intervals are in the + * range (-Pi, Pi]. However, we take advantage of the point -Pi to construct two special intervals: + * the full() interval is [-Pi, Pi], and the Empty() interval is [Pi, -Pi]. + * + */ +@GwtCompatible(serializable = true) +public final strictfp class S1Interval implements Cloneable, Serializable { + private double lo; + private double hi; + + public S1Interval() { + setEmpty(); + } + + /** + * Both endpoints must be in the range -Pi to Pi inclusive. The value -Pi is converted internally + * to Pi except for the full() and empty() intervals. + */ + public S1Interval(double lo, double hi) { + this(lo, hi, false); + } + + /** Copy constructor. Assumes that the {@code interval} is valid. */ + public S1Interval(S1Interval interval) { + this.lo = interval.lo; + this.hi = interval.hi; + } + + /** + * Internal constructor that just passes the arguments down to {@link #set(double, double, + * boolean)}. + */ + private S1Interval(double lo, double hi, boolean checked) { + set(lo, hi, checked); + } + + /** + * Assigns the range of this interval, assuming both arguments are in the correct range, i.e. + * normalization from -Pi to Pi is already done. If {@code checked} is false, endpoints at -Pi + * will be moved to +Pi unless the other endpoint is already there. + * + *

Note that because S1Interval has invariants to maintain after each update, values cannot be + * set singly, both endpoints must be set together. + */ + void set(double newLo, double newHi, boolean checked) { + this.lo = newLo; + this.hi = newHi; + if (!checked) { + if (newLo == -S2.M_PI && newHi != S2.M_PI) { + lo = S2.M_PI; + } + if (newHi == -S2.M_PI && newLo != S2.M_PI) { + hi = S2.M_PI; + } + } + } + + /** + * Sets the range of this interval to the empty interval. + * + *

Package private since only S2 code needs to mutate S1Intervals for now. + */ + void setEmpty() { + lo = S2.M_PI; + hi = -S2.M_PI; + } + + /** + * Sets the range of this interval to the full interval. + * + *

Package private since only S2 code needs to mutate S1Intervals for now. + */ + void setFull() { + lo = -S2.M_PI; + hi = S2.M_PI; + } + + public static S1Interval empty() { + S1Interval result = new S1Interval(); + result.setEmpty(); + return result; + } + + public static S1Interval full() { + S1Interval result = new S1Interval(); + result.setFull(); + return result; + } + + /** Convenience method to construct an interval containing a single point. */ + public static S1Interval fromPoint(double radians) { + if (radians == -S2.M_PI) { + radians = S2.M_PI; + } + return new S1Interval(radians, radians, true); + } + + /** + * Convenience method to construct the minimal interval containing the two given points. This is + * equivalent to starting with an empty interval and calling addPoint() twice, but it is more + * efficient. + */ + public static S1Interval fromPointPair(double p1, double p2) { + // assert (Math.abs(p1) <= S2.M_PI && Math.abs(p2) <= S2.M_PI); + S1Interval result = new S1Interval(); + result.initFromPointPair(p1, p2); + return result; + } + + void initFromPointPair(double p1, double p2) { + if (p1 == -S2.M_PI) { + p1 = S2.M_PI; + } + if (p2 == -S2.M_PI) { + p2 = S2.M_PI; + } + if (positiveDistance(p1, p2) <= S2.M_PI) { + this.lo = p1; + this.hi = p2; + } else { + this.lo = p2; + this.hi = p1; + } + } + + public double lo() { + return lo; + } + + public double hi() { + return hi; + } + + /** + * An interval is valid if neither bound exceeds Pi in absolute value, and the value -Pi appears + * only in the Empty() and full() intervals. + */ + public boolean isValid() { + return (Math.abs(lo) <= S2.M_PI + && Math.abs(hi) <= S2.M_PI + && !(lo == -S2.M_PI && hi != S2.M_PI) + && !(hi == -S2.M_PI && lo != S2.M_PI)); + } + + /** Returns true if the interval contains all points on the unit circle. */ + public boolean isFull() { + return hi - lo == 2 * S2.M_PI; + } + + /** Returns true if the interval is empty, i.e. it contains no points. */ + public boolean isEmpty() { + return lo - hi == 2 * S2.M_PI; + } + + /** Returns true if lo() > hi(). (This is true for empty intervals.) */ + public boolean isInverted() { + return lo > hi; + } + + /** + * Returns the midpoint of the interval. For full and empty intervals, the result is arbitrary. + */ + public double getCenter() { + double center = 0.5 * (lo + hi); + if (!isInverted()) { + return center; + } + // Return the center in the range (-Pi, Pi]. + return (center <= 0) ? (center + S2.M_PI) : (center - S2.M_PI); + } + + /** Returns the length of the interval. The length of an empty interval is negative. */ + public double getLength() { + double length = hi - lo; + if (length >= 0) { + return length; + } + length += 2 * S2.M_PI; + // Empty intervals have a negative length. + return (length > 0) ? length : -1; + } + + /** + * Return the complement of the interior of the interval. An interval and its complement have the + * same boundary but do not share any interior values. The complement operator is not a bijection, + * since the complement of a singleton interval (containing a single value) is the same as the + * complement of an empty interval. + */ + public S1Interval complement() { + if (lo == hi) { + return full(); // Singleton. + } + return new S1Interval(hi, lo, true); // Handles + // empty and + // full. + } + + /** + * Return the midpoint of the complement of the interval. For full and empty intervals, the result + * is arbitrary. For a singleton interval (containing a single point), the result is its antipodal + * point on S1. + */ + public double getComplementCenter() { + if (lo() != hi()) { + return complement().getCenter(); + } else { // Singleton. + return (hi() <= 0) ? (hi() + S2.M_PI) : (hi() - S2.M_PI); + } + } + + /** Returns true if the interval (which is closed) contains the point 'p'. */ + public boolean contains(double p) { + // Works for empty, full, and singleton intervals. + // assert (Math.abs(p) <= S2.M_PI); + if (p == -S2.M_PI) { + p = S2.M_PI; + } + return fastContains(p); + } + + /** + * Returns true if the interval (which is closed) contains the point 'p'. Skips the normalization + * of 'p' from -Pi to Pi. + */ + public boolean fastContains(double p) { + if (isInverted()) { + return (p >= lo || p <= hi) && !isEmpty(); + } else { + return p >= lo && p <= hi; + } + } + + /** Returns true if the interior of the interval contains the point 'p'. */ + public boolean interiorContains(double p) { + // Works for empty, full, and singleton intervals. + // assert (Math.abs(p) <= S2.M_PI); + if (p == -S2.M_PI) { + p = S2.M_PI; + } + + if (isInverted()) { + return p > lo || p < hi; + } else { + return (p > lo && p < hi) || isFull(); + } + } + + /** + * Returns true if the interval contains the interval {@code y}. Works for empty, full, and + * singleton intervals. + */ + public boolean contains(final S1Interval y) { + // It might be helpful to compare the structure of these tests to + // the simpler Contains(double) method above. + + if (isInverted()) { + if (y.isInverted()) { + return y.lo >= lo && y.hi <= hi; + } + return (y.lo >= lo || y.hi <= hi) && !isEmpty(); + } else { + if (y.isInverted()) { + return isFull() || y.isEmpty(); + } + return y.lo >= lo && y.hi <= hi; + } + } + + /** + * Returns true if the interior of this interval contains the entire interval 'y'. Note that + * x.interiorContains(x) is true only when x is the empty or full interval, and + * x.interiorContains(S1Interval(p,p)) is equivalent to x.InteriorContains(p). + */ + public boolean interiorContains(final S1Interval y) { + if (isInverted()) { + if (!y.isInverted()) { + return y.lo > lo || y.hi < hi; + } + return (y.lo > lo && y.hi < hi) || y.isEmpty(); + } else { + if (y.isInverted()) { + return isFull() || y.isEmpty(); + } + return (y.lo > lo && y.hi < hi) || isFull(); + } + } + + /** + * Returns true if the two intervals contain any points in common. Note that the point +/-Pi has + * two representations, so the intervals [-Pi,-3] and [2,Pi] intersect, for example. + */ + public boolean intersects(final S1Interval y) { + if (isEmpty() || y.isEmpty()) { + return false; + } + if (isInverted()) { + // Every non-empty inverted interval contains Pi. + return y.isInverted() || y.lo <= hi || y.hi >= lo; + } else { + if (y.isInverted()) { + return y.lo <= hi || y.hi >= lo; + } + return y.lo <= hi && y.hi >= lo; + } + } + + /** + * Returns true if the interior of this interval contains any point of the interval {@code y} + * (including its boundary). Works for empty, full, and singleton intervals. + */ + public boolean interiorIntersects(final S1Interval y) { + if (isEmpty() || y.isEmpty() || lo == hi) { + return false; + } + if (isInverted()) { + return y.isInverted() || y.lo < hi || y.hi > lo; + } else { + if (y.isInverted()) { + return y.lo < hi || y.hi > lo; + } + return (y.lo < hi && y.hi > lo) || isFull(); + } + } + + /** + * Return the Hausdorff distance to the given interval {@code y}. For two S1Intervals x and y, + * this distance is defined by {@code h(x, y) = max_{p in x} min_{q in y} d(p, q)}, where {@code + * d(.,.)} is measured along S1. + */ + public double getDirectedHausdorffDistance(final S1Interval y) { + if (y.contains(this)) { + return 0.0; // this includes the case *this is empty + } + if (y.isEmpty()) { + return S2.M_PI; // maximum possible distance on S1 + } + + double yComplementCenter = y.getComplementCenter(); + if (contains(yComplementCenter)) { + return positiveDistance(y.hi(), yComplementCenter); + } else { + // The Hausdorff distance is realized by either two hi() endpoints or two + // lo() endpoints, whichever is farther apart. + double hiHi = + new S1Interval(y.hi(), yComplementCenter).contains(hi()) + ? positiveDistance(y.hi(), hi()) + : 0; + double loLo = + new S1Interval(yComplementCenter, y.lo()).contains(lo()) + ? positiveDistance(lo(), y.lo()) + : 0; + // assert (hiHi > 0 || loLo > 0); + return Math.max(hiHi, loLo); + } + } + + /** + * Expands the interval by the minimum amount necessary so that it contains the point {@code p} + * (an angle in the range [-Pi, Pi]). + */ + @CheckReturnValue + public S1Interval addPoint(double p) { + // assert (Math.abs(p) <= S2.M_PI); + if (p == -S2.M_PI) { + p = S2.M_PI; + } + + if (fastContains(p)) { + return new S1Interval(this); + } + + if (isEmpty()) { + return S1Interval.fromPoint(p); + } else { + // Compute distance from p to each endpoint. + double dlo = positiveDistance(p, lo); + double dhi = positiveDistance(hi, p); + if (dlo < dhi) { + return new S1Interval(p, hi); + } else { + return new S1Interval(lo, p); + } + // Adding a point can never turn a non-full interval into a full one. + } + } + + /** + * Returns the closest point in the interval to the point {@code p}. The interval must be + * non-empty. + */ + public double clampPoint(double p) { + // assert (!isEmpty()); + // assert (Math.abs(p) <= S2.M_PI); + if (p == -S2.M_PI) { + p = S2.M_PI; + } + + if (fastContains(p)) { + return p; + } + + // Compute distance from p to each endpoint. + double dlo = positiveDistance(p, lo); + double dhi = positiveDistance(hi, p); + return (dlo < dhi) ? lo : hi; + } + + /** + * Returns a new interval that has been expanded on each side by the distance {@code margin}. If + * "margin" is negative, then shrink 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. + */ + @CheckReturnValue + public S1Interval expanded(double margin) { + S1Interval copy = new S1Interval(this); + copy.expandedInternal(margin); + return copy; + } + + /** + * Expands this interval on each side by the distance {@code margin}. If "margin" is negative, + * then shrink 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. + * + *

Package private since only S2 code should be mutating S1Intervals for now. + */ + void expandedInternal(double margin) { + if (margin >= 0) { + if (isEmpty()) { + return; + } + // Check whether this interval will be full after expansion, allowing + // for a 1-bit rounding error when computing each endpoint. + if (getLength() + 2 * margin + 2 * S2.DBL_EPSILON >= 2 * S2.M_PI) { + setFull(); + return; + } + } else { + if (isFull()) { + return; + } + // Check whether this interval will be empty after expansion, allowing + // for a 1-bit rounding error when computing each endpoint. + if (getLength() + 2 * margin - 2 * S2.DBL_EPSILON <= 0) { + setEmpty(); + return; + } + } + + set( + Platform.IEEEremainder(lo - margin, 2 * S2.M_PI), + Platform.IEEEremainder(hi + margin, 2 * S2.M_PI), + false); + if (lo <= -S2.M_PI) { + lo = S2.M_PI; + } + } + + /** Returns the smallest interval that contains this interval and the interval {@code y}. */ + @CheckReturnValue + public S1Interval union(S1Interval y) { + S1Interval result = new S1Interval(this); + result.unionInternal(y); + return result; + } + + /** + * Sets this interval to the union of the current interval and {@code y}. + * + *

Package private since only S2 classes are intended to mutate S1Intervals for now. + */ + void unionInternal(S1Interval y) { + // The y.isFull() case is handled correctly in all cases by the code + // below, but can follow three separate code paths depending on whether + // this interval is inverted, is non-inverted but contains Pi, or neither. + if (!y.isEmpty()) { + if (fastContains(y.lo)) { + if (fastContains(y.hi)) { + // Either this interval contains y, or the union of the two + // intervals is the full interval. + if (!contains(y)) { + setFull(); + } + } else { + hi = y.hi; + } + } else if (fastContains(y.hi)) { + lo = y.lo; + } else if (isEmpty() || y.fastContains(lo)) { + // This interval contains neither endpoint of y. This means that either y + // contains all of this interval, or the two intervals are disjoint. + lo = y.lo; + hi = y.hi; + } else { + // Check which pair of endpoints are closer together. + double dlo = positiveDistance(y.hi, lo); + double dhi = positiveDistance(hi, y.lo); + if (dlo < dhi) { + lo = y.lo; + } else { + hi = y.hi; + } + } + } + } + + /** + * Returns the value of the given endpoint in this interval, which must be 0 for the low end, or 1 + * for the high end. + */ + public double get(int endpoint) { + if (endpoint < 0 || endpoint > 1) { + throw new ArrayIndexOutOfBoundsException(); + } + return endpoint == 0 ? lo : hi; + } + + /** + * Returns the smallest interval that contains the intersection of this interval with {@code y}. + * Note that the region of intersection may consist of two disjoint intervals. + */ + @CheckReturnValue + public S1Interval intersection(final S1Interval y) { + S1Interval result = new S1Interval(this); + result.intersectionInternal(y); + return result; + } + + /** + * Sets this interval to the intersection of the current interval and {@code y}. + * + *

Package private since only S2 classes are intended to mutate S1Intervals for now. + */ + void intersectionInternal(final S1Interval y) { + // The y.isFull() case is handled correctly in all cases by the code below, but can follow three + // separate code paths depending on whether this interval is inverted, is non-inverted but + // contains Pi, or neither. + + if (y.isEmpty()) { + this.setEmpty(); + } else if (fastContains(y.lo)) { + if (fastContains(y.hi)) { + // Either this interval contains y, or the region of intersection consists of two disjoint + // subintervals. In either case, we want to set the interval to the shorter of the two + // original intervals. + if (y.getLength() < getLength()) { + this.set(y.lo, y.hi, true); // isFull() code path + } + } else { + this.set(y.lo, hi, true); + } + } else if (fastContains(y.hi)) { + this.set(lo, y.hi, true); + } else { + // This interval contains neither endpoint of y. This means that either y + // contains all of this interval, or the two intervals are disjoint. + if (!y.fastContains(lo)) { + // assert (!intersects(y)); + this.setEmpty(); + } + } + } + + /** + * Returns true if this interval can be transformed into the interval {@code y} by moving each + * endpoint by at most "maxError" (and without the endpoints crossing, which would invert the + * interval). Empty and full intervals are considered to start at an arbitrary point on the unit + * circle, thus any interval with (length <= 2*maxError) matches the empty interval, and any + * interval with (length >= 2*Pi - 2*maxError) matches the full interval. + */ + public boolean approxEquals(S1Interval y, double maxError) { + // Full and empty intervals require special cases because the "endpoints" + // are considered to be positioned arbitrarily. + if (isEmpty()) { + return y.getLength() <= 2 * maxError; + } + if (y.isEmpty()) { + return getLength() <= 2 * maxError; + } + if (isFull()) { + return y.getLength() >= 2 * (S2.M_PI - maxError); + } + if (y.isFull()) { + return getLength() >= 2 * (S2.M_PI - maxError); + } + + // The purpose of the last test below is to verify that moving the endpoints + // does not invert the interval, e.g. [-1e20, 1e20] vs. [1e20, -1e20]. + return (Math.abs(Platform.IEEEremainder(y.lo - lo, 2 * S2.M_PI)) <= maxError + && Math.abs(Platform.IEEEremainder(y.hi - hi, 2 * S2.M_PI)) <= maxError + && Math.abs(getLength() - y.getLength()) <= 2 * maxError); + } + + /** As {@link #approxEquals(S1Interval, double)}, with a default maxError of 1e-15. */ + public boolean approxEquals(final S1Interval y) { + return approxEquals(y, 1e-15); + } + + /** Returns true if two intervals contains the same set of points. */ + @Override + public boolean equals(Object that) { + if (that instanceof S1Interval) { + S1Interval thatInterval = (S1Interval) that; + return lo == thatInterval.lo && hi == thatInterval.hi; + } + return false; + } + + @Override + public int hashCode() { + long value = 17; + value = 37 * value + Double.doubleToLongBits(lo); + value = 37 * value + Double.doubleToLongBits(hi); + return (int) ((value >>> 32) ^ value); + } + + @Override + public String toString() { + return "[" + this.lo + ", " + this.hi + "]"; + } + + /** + * Computes the distance from {@code a} to {@code b} in the range [0, 2*Pi). This is equivalent to + * {@code drem(b - a - S2.M_PI, 2 * S2.M_PI) + S2.M_PI}, except that it is more numerically stable + * (it does not lose precision for very small positive distances). + */ + public static double positiveDistance(double a, double b) { + double d = b - a; + if (d >= 0) { + return d; + } + // We want to ensure that if b == Pi and a == (-Pi + eps), + // the return result is approximately 2*Pi and not zero. + return (b + S2.M_PI) - (a - S2.M_PI); + } +} diff --git a/core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/S2.java b/core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/S2.java new file mode 100644 index 0000000000..96aa3350a7 --- /dev/null +++ b/core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/S2.java @@ -0,0 +1,654 @@ +/* + * Copyright 2005 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.mogo.eagle.core.utilcode.geometry; + + +import androidx.core.util.Preconditions; + +public final strictfp class S2 { + // Declare some frequently used constants + public static final double M_PI = Math.PI; + public static final double M_1_PI = 1.0 / Math.PI; + public static final double M_PI_2 = Math.PI / 2.0; + public static final double M_PI_4 = Math.PI / 4.0; + /** Inverse of the root of 2. */ + public static final double M_SQRT1_2 = 1 / Math.sqrt(2); + + public static final double M_SQRT2 = Math.sqrt(2); + public static final double M_E = Math.E; + + /** The smallest floating-point value {@code x} such that {@code (1 + x != 1)}. */ + public static final double DBL_EPSILON; + + static { + double machEps = 1.0d; + do { + machEps /= 2.0f; + } while ((1.0 + (machEps / 2.0)) != 1.0); + DBL_EPSILON = machEps; + } + + // This point is about 66km from the north pole towards the East Siberian Sea. See the unit test + // for more details. It is written here using constant components to avoid computational errors + // from producting a different value than other implementations of S2. + private static final S2Point ORIGIN = + new S2Point(-0.0099994664350250197, 0.0025924542609324121, 0.99994664350250195); + + // Together these flags define a cell orientation. If SWAP_MASK + // is true, then canonical traversal order is flipped around the + // diagonal (i.e. i and j are swapped with each other). If + // INVERT_MASK is true, then the traversal order is rotated by 180 + // degrees (i.e. the bits of i and j are inverted, or equivalently, + // the axis directions are reversed). + public static final int SWAP_MASK = 0x01; + public static final int INVERT_MASK = 0x02; + + /** Mapping Hilbert traversal order to orientation adjustment mask. */ + private static final int[] posToOrientation = {SWAP_MASK, 0, 0, INVERT_MASK + SWAP_MASK}; + + /** + * Returns an XOR bit mask indicating how the orientation of a child subcell is related to the + * orientation of its parent cell. The returned value can be XOR'd with the parent cell's + * orientation to give the orientation of the child cell. + * + * @param position the position of the subcell in the Hilbert traversal, in the range [0,3]. + * @return a bit mask containing some combination of {@link #SWAP_MASK} and {@link #INVERT_MASK}. + * @throws IllegalArgumentException if position is out of bounds. + */ + public static int posToOrientation(int position) { + Preconditions.checkArgument(0 <= position && position < 4); + return posToOrientation[position]; + } + + /** Mapping from cell orientation + Hilbert traversal to IJ-index. */ + private static final int[][] posToIj = { + // 0 1 2 3 + {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) + }; + + /** + * Return the IJ-index of the subcell at the given position in the Hilbert curve traversal with + * the given orientation. This is the inverse of {@link #ijToPos}. + * + * @param orientation the subcell orientation, in the range [0,3]. + * @param position the position of the subcell in the Hilbert traversal, in the range [0,3]. + * @return the IJ-index where {@code 0->(0,0), 1->(0,1), 2->(1,0), 3->(1,1)}. + * @throws IllegalArgumentException if either parameter is out of bounds. + */ + public static int posToIJ(int orientation, int position) { + return posToIj[orientation][position]; + } + + /** Mapping from Hilbert traversal order + cell orientation to IJ-index. */ + private static final int[][] IJ_TO_POS = { + // (0,0) (0,1) (1,0) (1,1) + {0, 1, 3, 2}, // canonical order + {0, 3, 1, 2}, // axes swapped + {2, 3, 1, 0}, // bits inverted + {2, 1, 3, 0}, // swapped & inverted + }; + + /** + * Returns the order in which a specified subcell is visited by the Hilbert curve. This is the + * inverse of {@link #posToIJ}. + * + * @param orientation the subcell orientation, in the range [0,3]. + * @param ijIndex the subcell index where {@code 0->(0,0), 1->(0,1), 2->(1,0), 3->(1,1)}. + * @return the position of the subcell in the Hilbert traversal, in the range [0,3]. + * @throws IllegalArgumentException if either parameter is out of bounds. + */ + public static final int ijToPos(int orientation, int ijIndex) { + return IJ_TO_POS[orientation][ijIndex]; + } + + /** Defines an area or a length cell metric. */ + public static final class Metric { + // NOTE: This isn't GWT serializable because writing custom field serializers for inner classes + // is hard. + + private final double deriv; + private final int dim; + + /** Defines a cell metric of the given dimension (1 == length, 2 == area). */ + public Metric(int dim, double deriv) { + this.deriv = deriv; + this.dim = dim; + } + + /** + * The "deriv" value of a metric is a derivative, and must be multiplied by a length or area in + * (s,t)-space to get a useful value. + */ + public double deriv() { + return deriv; + } + + /** Return the value of a metric for cells at the given level. */ + public double getValue(int level) { + return Math.scalb(deriv, -dim * level); + } + + /** + * Return the level at which the metric has approximately the given value. For example, + * S2::kAvgEdge.GetClosestLevel(0.1) returns the level at which the average cell edge length is + * approximately 0.1. The return value is always a valid level. + */ + public int getClosestLevel(double value) { + return getMinLevel((dim == 1 ? S2.M_SQRT2 : 2) * value); + } + + /** + * Return the minimum level such that the metric is at most the given value, or + * S2CellId::kMaxLevel if there is no such level. For example, S2::kMaxDiag.GetMinLevel(0.1) + * returns the minimum level such that all cell diagonal lengths are 0.1 or smaller. The return + * value is always a valid level. + */ + public int getMinLevel(double value) { + if (value <= 0) { + return S2CellId.MAX_LEVEL; + } + + // This code is equivalent to computing a floating-point "level" + // value and rounding up. The getExponent() method returns the + // exponent corresponding to a fraction in the range [1,2). + int exponent = Platform.getExponent(value / deriv); + int level = Math.max(0, Math.min(S2CellId.MAX_LEVEL, -(exponent >> (dim - 1)))); + // assert (level == S2CellId.MAX_LEVEL || getValue(level) <= value); + // assert (level == 0 || getValue(level - 1) > value); + return level; + } + + /** + * Return the maximum level such that the metric is at least the given value, or zero if there + * is no such level. For example, S2.kMinWidth.GetMaxLevel(0.1) returns the maximum level such + * that all cells have a minimum width of 0.1 or larger. The return value is always a valid + * level. + */ + public int getMaxLevel(double value) { + if (value <= 0) { + return S2CellId.MAX_LEVEL; + } + + // This code is equivalent to computing a floating-point "level" + // value and rounding down. + int exponent = Platform.getExponent(deriv / value); + int level = Math.max(0, Math.min(S2CellId.MAX_LEVEL, exponent >> (dim - 1))); + // assert (level == 0 || getValue(level) >= value); + // assert (level == S2CellId.MAX_LEVEL || getValue(level + 1) < value); + return level; + } + } + + /** + * Return a unique "origin" on the sphere for operations that need a fixed reference point. 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.) + */ + public static S2Point origin() { + return ORIGIN; + } + + /** + * Return true if the given point is approximately unit length (this is mainly useful for + * assertions). + */ + public static boolean isUnitLength(S2Point p) { + // Normalize() is guaranteed to return a vector whose L2-norm differs from 1 + // by less than 2 * DBL_EPSILON. Thus the squared L2-norm differs by less + // than 4 * DBL_EPSILON. The actual calculated Norm2() can have up to 1.5 * + // DBL_EPSILON of additional error. The total error of 5.5 * DBL_EPSILON + // can then be rounded down since the result must be a representable + // double-precision value. + return Math.abs(p.norm2() - 1) <= 5 * DBL_EPSILON; // About 1.11e-15 + } + + /** + * Return true if 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) + */ + public static boolean simpleCrossing(S2Point a, S2Point b, S2Point c, S2Point d) { + // We compute SimpleCCW() 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. Note that this is slightly more + // restrictive than the corresponding definition for planar edges, + // since we need to exclude pairs of line segments that would + // otherwise "intersect" by crossing two antipodal points. + + S2Point ab = S2Point.crossProd(a, b); + S2Point cd = S2Point.crossProd(c, d); + double acb = -ab.dotProd(c); + double cbd = -cd.dotProd(b); + double bda = ab.dotProd(d); + double dac = cd.dotProd(a); + + return (acb * cbd > 0) && (cbd * bda > 0) && (bda * dac > 0); + } + + /** + * Return a vector "c" that is orthogonal to the given unit-length vectors "a" and "b". This + * function is similar to a.CrossProd(b) except that it does a better job of ensuring + * orthogonality when "a" is nearly parallel to "b", and it returns a non-zero result even when a + * == b or a == -b. + * + *

It satisfies the following properties (RCP == robustCrossProd): + * + *

(1) RCP(a,b) != 0 for all a, b (2) RCP(b,a) == -RCP(a,b) unless a == b or a == -b (3) + * RCP(-a,b) == -RCP(a,b) unless a == b or a == -b (4) RCP(a,-b) == -RCP(a,b) unless a == b or a + * == -b + */ + public static S2Point robustCrossProd(S2Point a, S2Point b) { + // The direction of a.CrossProd(b) becomes unstable as (a + b) or (a - b) + // approaches zero. This leads to situations where a.CrossProd(b) is not + // very orthogonal to "a" and/or "b". We could fix this using Gram-Schmidt, + // but we also want b.robustCrossProd(a) == -a.robustCrossProd(b). + // + // The easiest fix is to just compute the cross product of (b+a) and (b-a). + // Mathematically, this cross product is exactly twice the cross product of + // "a" and "b", but it has the numerical advantage that (b+a) and (b-a) + // are always perpendicular (since "a" and "b" are unit length). This + // yields a result that is nearly orthogonal to both "a" and "b" even if + // these two values differ only in the lowest bit of one component. + // assert (isUnitLength(a) && isUnitLength(b)); + S2Point x = S2Point.crossProd(S2Point.add(b, a), S2Point.sub(b, a)); + if (!x.equalsPoint(S2Point.ORIGIN)) { + return x; + } + + // The only result that makes sense mathematically is to return zero, but + // we find it more convenient to return an arbitrary orthogonal vector. + return ortho(a); + } + + private static final S2Point[] ORTHO_BASES = { + new S2Point(1, 0.0053, 0.00457), new S2Point(0.012, 1, 0.00457), new S2Point(0.012, 0.0053, 1) + }; + + /** + * Returns a unit-length vector that is orthogonal to {@code a}. Satisfies {@code ortho(-a) = + * -ortho(a)} for all {@code a}. + */ + public static S2Point ortho(S2Point a) { + int k = a.largestAbsComponent() - 1; + if (k < 0) { + k = 2; + } + return S2Point.normalize(S2Point.crossProd(a, ORTHO_BASES[k])); + } + + /** + * Returns the area of triangle ABC. This method combines two different algorithms to get accurate + * results for both large and small triangles. The maximum error is about 5e-15 (about 0.25 square + * meters on the Earth's surface), the same as girardArea() below, but unlike that method it is + * also accurate for small triangles. Example: when the true area is 100 square meters, area() + * yields an error about 1 trillion times smaller than girardArea(). + * + *

All points should be unit length, and no two points should be antipodal. The area is always + * positive. + */ + public static double area(S2Point a, S2Point b, S2Point c) { + // assert isUnitLength(a) && isUnitLength(b) && isUnitLength(c); + + // 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. + + // We use volatile doubles to force the compiler to truncate all of these + // quantities to 64 bits. Otherwise it may compute a value of dmin > 0 + // simply because it chose to spill one of the intermediate values to + // memory but not one of the others. + final double sa = b.angle(c); + final double sb = c.angle(a); + final double sc = a.angle(b); + final double s = 0.5 * (sa + sb + sc); + if (s >= 3e-4) { + // Consider whether Girard's formula might be more accurate. + double s2 = s * s; + double dmin = s - Math.max(sa, Math.max(sb, sc)); + if (dmin < 1e-2 * s * s2 * s2) { + // This triangle is skinny enough to consider using Girard's formula. We increase the area + // by the approximate maximum error in the Girard calculation in order to ensure that this + // test is conservative. + double area = girardArea(a, b, c); + if (dmin < s * (0.1 * (area + 5e-15))) { + return area; + } + } + } + // Use l'Huilier's formula. + return 4 + * Math.atan( + Math.sqrt( + Math.max( + 0.0, + Math.tan(0.5 * s) + * Math.tan(0.5 * (s - sa)) + * Math.tan(0.5 * (s - sb)) + * Math.tan(0.5 * (s - sc))))); + } + + /** + * Returns the area of the triangle computed using Girard's formula. All points should be unit + * length, and no two points should be antipodal. + * + *

This method is about twice as fast as area() but has poor relative accuracy for small + * triangles. The maximum error is about 5e-15 (about 0.25 square meters on the Earth's surface) + * and the average error is about 1e-15. These bounds apply to triangles of any size, even as the + * maximum edge length of the triangle approaches 180 degrees. But note that for such triangles, + * tiny perturbations of the input points can change the true mathematical area dramatically. + */ + public static double girardArea(S2Point a, S2Point b, S2Point c) { + // This is equivalent to the usual Girard's formula but is slightly more + // accurate, faster to compute, and handles a == b == c without a special + // case. RobustCrossProd() is necessary to get good accuracy when two of the + // input points are very close together. + S2Point ab = S2.robustCrossProd(a, b); + S2Point bc = S2.robustCrossProd(b, c); + S2Point ac = S2.robustCrossProd(a, c); + return Math.max(0.0, ab.angle(ac) - ab.angle(bc) + bc.angle(ac)); + } + + /** + * Like area(), but returns a positive value for counterclockwise triangles and a negative value + * otherwise. + */ + public static double signedArea(S2Point a, S2Point b, S2Point c) { + return S2Predicates.sign(a, b, c) * area(a, b, c); + } + + // About centroids: + // ---------------- + // + // There are several notions of the "centroid" of a triangle. First, there + // is the planar centroid, which is simply the centroid of the ordinary + // (non-spherical) triangle defined by the three vertices. Second, there is + // the surface centroid, which is defined as the intersection of the three + // medians of the spherical triangle. It is possible to show that this + // point is simply the planar centroid projected to the surface of the + // sphere. Finally, there is the true centroid (mass centroid), which 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. + // + // Also 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. + + /** + * Return the centroid of the planar triangle ABC. This 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 above). + */ + public static S2Point planarCentroid(S2Point a, S2Point b, S2Point c) { + return new S2Point((a.x + b.x + c.x) / 3.0, (a.y + b.y + c.y) / 3.0, (a.z + b.z + c.z) / 3.0); + } + + /** + * Returns the true centroid of the spherical geodesic edge AB multiplied by the length of the + * edge AB. As with triangles, the true centroid of a collection of edges may be computed simply + * by summing the result of this method for each edge. + * + *

Note that the planar centroid of a geodesic edge is simply 0.5 * (a + b), while the surface + * centroid is (a + b).normalize(). However neither of these values is appropriate for computing + * the centroid of a collection of edges (such as a polyline). + * + *

Also note that the result of this function is defined to be {@link S2Point#ORIGIN} if the + * edge is degenerate (and that this is intended behavior). + */ + public static S2Point trueCentroid(S2Point a, S2Point b) { + // The centroid (multiplied by length) is a vector toward the midpoint of the edge, whose length + // is twice the sine of half the angle between the two vertices. + // Defining theta to be this angle, we have: + S2Point vDiff = a.sub(b); // Length == 2*sin(theta) + S2Point vSum = a.add(b); // Length == 2*cos(theta) + double sin2 = vDiff.norm2(); + double cos2 = vSum.norm2(); + if (cos2 == 0) { + return S2Point.ORIGIN; // Ignore antipodal edges. + } + return vSum.mul(Math.sqrt(sin2 / cos2)); // Length == 2*sin(theta) + } + + /** + * Returns the true centroid of the spherical triangle ABC multiplied by the signed area of + * spherical triangle ABC. 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. + */ + public static S2Point trueCentroid(S2Point a, S2Point b, S2Point c) { + // I couldn't find any references for computing the true centroid of a + // spherical triangle... I have a truly marvellous demonstration of this + // formula which this margin is too narrow to contain :) + + // assert (isUnitLength(a) && isUnitLength(b) && isUnitLength(c)); + + // Use angle() in order to get accurate results for small triangles. + double aAngle = b.angle(c); + double bAngle = c.angle(a); + double cAngle = a.angle(b); + double ra = (aAngle == 0) ? 1 : (aAngle / Math.sin(aAngle)); + double rb = (bAngle == 0) ? 1 : (bAngle / Math.sin(bAngle)); + double rc = (cAngle == 0) ? 1 : (cAngle / Math.sin(cAngle)); + + // 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. + // + // TODO(user): 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. + S2Point x = new S2Point(a.x, b.x - a.x, c.x - a.x); + S2Point y = new S2Point(a.y, b.y - a.y, c.y - a.y); + S2Point z = new S2Point(a.z, b.z - a.z, c.z - a.z); + S2Point r = new S2Point(ra, rb - ra, rc - ra); + return new S2Point( + 0.5 * S2Point.scalarTripleProduct(r, y, z), + 0.5 * S2Point.scalarTripleProduct(r, z, x), + 0.5 * S2Point.scalarTripleProduct(r, x, y)); + } + + /** + * Returns +1 if the edge AB is CCW around the origin, -1 if its clockwise, and 0 if the result is + * indeterminate. + */ + public static int planarCCW(R2Vector a, R2Vector b) { + double sab = (a.dotProd(b) > 0) ? -1 : 1; + R2Vector vab = R2Vector.add(a, R2Vector.mul(b, sab)); + double da = a.norm2(); + double db = b.norm2(); + double sign; + if (da < db || (da == db && a.lessThan(b))) { + sign = a.crossProd(vab) * sab; + } else { + sign = vab.crossProd(b); + } + if (sign > 0) { + return 1; + } + if (sign < 0) { + return -1; + } + return 0; + } + + public static int planarOrderedCCW(R2Vector a, R2Vector b, R2Vector c) { + int sum = 0; + sum += planarCCW(a, b); + sum += planarCCW(b, c); + sum += planarCCW(c, a); + if (sum > 0) { + return 1; + } + if (sum < 0) { + return -1; + } + return 0; + } + + /** + * Return the angle at the vertex B in the triangle ABC. The return value is always in the range + * [0, Pi]. The points do not need to be normalized. Ensures that Angle(a,b,c) == Angle(c,b,a) for + * all a,b,c. + * + *

The angle is undefined if A or C is diametrically opposite from B, and becomes numerically + * unstable as the length of edge AB or BC approaches 180 degrees. + */ + public static double angle(S2Point a, S2Point b, S2Point c) { + // robustCrossProd() is necessary to get good accuracy when two of the input + // points are very close together. + return robustCrossProd(a, b).angle(robustCrossProd(c, b)); + } + + /** + * Returns the exterior angle at the vertex B in the triangle ABC. The return value is positive if + * ABC is counterclockwise and negative otherwise. If you imagine an ant walking from A to B to C, + * this is the angle that the ant turns at vertex B (positive = left, negative = right). + * + *

Ensures that turnAngle(a,b,c) == -turnAngle(c,b,a) for all distinct a,b,c. The result is + * undefined if (a == b || b == c), but is either -Pi or Pi if (a == c). All points should be + * normalized. + */ + public static double turnAngle(S2Point a, S2Point b, S2Point c) { + // We use robustCrossProd() to get good accuracy when two points are very close together, and + // S2Predicates.sign() to ensure that the sign is correct for turns that are close to 180 + // degrees. + // + // Unfortunately we can't save robustCrossProd(a, b) and pass it as the optional 4th argument to + // S2Predicates.sign(), because it requires a.crossProd(b) exactly (the robust version differs + // in magnitude). + double angle = robustCrossProd(a, b).angle(robustCrossProd(b, c)); + + // Don't return S2Predicates.sign() * angle because it is legal to have (a == c). + return (S2Predicates.sign(a, b, c) > 0) ? angle : -angle; + } + + /** + * Returns the maximum error in {@link #turnAngle}. The returned value is proportional to the + * number of vertices and the machine epsilon. + */ + public static double getTurningAngleMaxError(int numVertices) { + // The maximum error can be bounded as follows: + // 2.24 * DBL_EPSILON for robustCrossProd(b, a) + // 2.24 * DBL_EPSILON for robustCrossProd(c, b) + // 3.25 * DBL_EPSILON for angle() + // 2.00 * DBL_EPSILON for each addition in the Kahan summation + // ------------------ + // 9.73 * DBL_EPSILON + final double maxErrorPerVertex = 9.73 * S2.DBL_EPSILON; + return maxErrorPerVertex * numVertices; + } + + /** + * Returns a right-handed coordinate frame (three orthonormal vectors) based on a single point, + * which will become the third axis. + */ + public static Matrix3x3 getFrame(S2Point p0) { + S2Point p1 = ortho(p0); + S2Point p2 = S2Point.normalize(S2Point.crossProd(p1, p0)); + return Matrix3x3.fromCols(p2, p1, p0); + } + + /** Returns a normalized copy {@code p} after rotating it by the rotation matrix {@code r}. */ + static S2Point rotate(S2Point p, Matrix3x3 r) { + Matrix3x3 rotated = r.mult(new Matrix3x3(1, p.x, p.y, p.z)); + return S2Point.normalize(new S2Point(rotated.get(0, 0), rotated.get(1, 0), rotated.get(2, 0))); + } + + /** Converts 'p' to the basis given in 'frame'. */ + static S2Point toFrame(Matrix3x3 frame, S2Point p) { + // The inverse of an orthonormal matrix is its transpose. + return frame.transpose().mult(Matrix3x3.fromCols(p)).getCol(0); + } + + /** Converts 'p' from the basis given in 'frame'. */ + static S2Point fromFrame(Matrix3x3 frame, S2Point q) { + return frame.mult(Matrix3x3.fromCols(q)).getCol(0); + } + + /** + * Return true if two points are within the given distance of each other (mainly useful for + * testing). + */ + public static boolean approxEquals(S2Point a, S2Point b, double maxError) { + return a.angle(b) <= maxError; + } + + public static boolean approxEquals(S2Point a, S2Point b) { + return approxEquals(a, b, 1e-15); + } + + public static boolean approxEquals(double a, double b, double maxError) { + return Math.abs(a - b) <= maxError; + } + + public static boolean approxEquals(double a, double b) { + return approxEquals(a, b, 1e-15); + } + + // Don't instantiate + private S2() {} +} diff --git a/core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/S2AreaCentroid.java b/core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/S2AreaCentroid.java new file mode 100644 index 0000000000..dbc21e53a7 --- /dev/null +++ b/core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/S2AreaCentroid.java @@ -0,0 +1,65 @@ +/* + * Copyright 2011 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.mogo.eagle.core.utilcode.geometry; + +import com.google.common.annotations.GwtCompatible; +import com.google.common.base.Objects; +import java.io.Serializable; +import javax.annotation.Nullable; + +/** + * The area of an interior, i.e. the region on the left side of an odd number of loops and + * optionally a centroid. The area is between 0 and 4*Pi. If it has a centroid, it is the true + * centroid of the interior multiplied by the area of the shape. Note that the centroid may not be + * contained by the shape. + * + * @author dbentley@google.com (Daniel Bentley) + */ +@GwtCompatible(serializable = true) +public final class S2AreaCentroid implements Serializable { + + private final double area; + private final S2Point centroid; + + public S2AreaCentroid(double area, @Nullable S2Point centroid) { + this.area = area; + this.centroid = centroid; + } + + public double getArea() { + return area; + } + + @Nullable + public S2Point getCentroid() { + return centroid; + } + + @Override + public boolean equals(Object obj) { + if (obj instanceof S2AreaCentroid) { + S2AreaCentroid that = (S2AreaCentroid) obj; + return this.area == that.area && Objects.equal(this.centroid, that.centroid); + } + return false; + } + + @Override + public int hashCode() { + return Objects.hashCode(area, centroid); + } +} diff --git a/core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/S2Cap.java b/core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/S2Cap.java new file mode 100644 index 0000000000..5203ffb164 --- /dev/null +++ b/core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/S2Cap.java @@ -0,0 +1,466 @@ +/* + * Copyright 2005 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.mogo.eagle.core.utilcode.geometry; + +import com.google.common.annotations.GwtCompatible; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.Serializable; +import javax.annotation.CheckReturnValue; + +/** + * S2Cap 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 Pi/2 is a hemisphere, and a + * cap of radius Pi covers the entire sphere. + * + *

A cap can also be defined by its center point and height. The height is simply the distance + * from the center point to the cutoff plane. There is also support for "empty" and "full" caps, + * which contain no points and all points respectively. + * + */ +@GwtCompatible(serializable = true) +public final strictfp class S2Cap implements S2Region, Serializable { + private final S2Point axis; + private final S1ChordAngle radius; + + // Caps may be constructed from either an axis and a height, or an axis and + // an angle. To avoid ambiguity, there are no public constructors + + private S2Cap(S2Point axis, S1ChordAngle radius) { + this.axis = axis; + this.radius = radius; + // assert (isValid()); + } + + /** + * Creates a cap where the radius is expressed as an S1ChordAngle. This constructor is more + * efficient than {@link #fromAxisAngle(S2Point, S1Angle)}. + */ + public static S2Cap fromAxisChord(S2Point center, S1ChordAngle radius) { + return new S2Cap(center, radius); + } + + /** + * Create a cap given its axis and the cap height, i.e. the maximum projected distance along the + * cap axis from the cap center. 'axis' should be a unit-length vector. + */ + public static S2Cap fromAxisHeight(S2Point axis, double height) { + // assert (S2.isUnitLength(axis)); + return new S2Cap(axis, S1ChordAngle.fromLength2(2 * height)); + } + + /** + * Create a cap given its axis and the cap opening angle, i.e. maximum angle between the axis and + * a point on the cap. 'axis' should be a unit-length vector, and 'angle' should be between 0 and + * 180 degrees. + */ + public static S2Cap fromAxisAngle(S2Point axis, S1Angle angle) { + // The "min" calculation below is necessary to handle S1Angle.INFINITY. + // assert (S2.isUnitLength(axis)); + return fromAxisChord( + axis, S1ChordAngle.fromS1Angle(S1Angle.radians(Math.min(angle.radians(), S2.M_PI)))); + } + + /** + * Create a cap given its axis and its area in steradians. 'axis' should be a unit-length vector, + * and 'area' should be between 0 and 4 * M_PI. + */ + public static S2Cap fromAxisArea(S2Point axis, double area) { + // assert (S2.isUnitLength(axis)); + return new S2Cap(axis, S1ChordAngle.fromLength2(area / S2.M_PI)); + } + + /** Return an empty cap, i.e. a cap that contains no points. */ + public static S2Cap empty() { + return new S2Cap(S2Point.X_POS, S1ChordAngle.NEGATIVE); + } + + /** Return a full cap, i.e. a cap that contains all points. */ + public static S2Cap full() { + return new S2Cap(S2Point.X_POS, S1ChordAngle.STRAIGHT); + } + + // Accessor methods. + public S2Point axis() { + return axis; + } + + public S1ChordAngle radius() { + return radius; + } + + /** Returns the height of the cap, i.e. the distance from the center point to the cutoff plane. */ + public double height() { + return 0.5 * radius.getLength2(); + } + + public double area() { + return 2 * S2.M_PI * Math.max(0.0, height()); + } + + /** + * Returns the cap radius as an S1Angle. Since the cap angle is stored internally as an + * S1ChordAngle, this method requires a trigonometric operation and may yield a slightly different + * result than the value passed to {@link #fromAxisAngle(S2Point, S1Angle)}. + */ + public S1Angle angle() { + return radius.toAngle(); + } + + /** + * Returns true if the axis is {@link S2#isUnitLength unit length}, and the angle is less than Pi. + * + *

Negative angles or heights are valid, and represent empty caps. + */ + public boolean isValid() { + return S2.isUnitLength(axis) && radius.getLength2() <= 4; + } + + /** Return true if the cap is empty, i.e. it contains no points. */ + public boolean isEmpty() { + return radius.isNegative(); + } + + /** Return true if the cap is full, i.e. it contains all points. */ + public boolean isFull() { + return S1ChordAngle.STRAIGHT.equals(radius); + } + + /** + * Return 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, + * since the complement of a singleton cap (containing a single point) is the same as the + * complement of an empty cap. + */ + @CheckReturnValue + public S2Cap complement() { + // The complement of a full cap is an empty cap, not a singleton. + // Also make sure that the complement of an empty cap is full. + if (isFull()) { + return empty(); + } + if (isEmpty()) { + return full(); + } + return fromAxisChord(S2Point.neg(axis), S1ChordAngle.fromLength2(4 - radius.getLength2())); + } + + /** + * Return true if and only if this cap contains the given other cap (in a set containment sense, + * e.g. every cap contains the empty cap). + */ + public boolean contains(S2Cap other) { + if (isFull() || other.isEmpty()) { + return true; + } else { + S1ChordAngle axialDistance = new S1ChordAngle(axis, other.axis); + return radius.compareTo(S1ChordAngle.add(axialDistance, other.radius)) >= 0; + } + } + + /** + * Return true if and only if the interior of this cap intersects the given other cap. (This + * relationship is not symmetric, since only the interior of this cap is used.) + */ + public boolean interiorIntersects(S2Cap other) { + // Interior(X) intersects Y if and only if Complement(Interior(X)) + // does not contain Y. + return !complement().contains(other); + } + + /** + * Return true if and only if the given point is contained in the interior of the region (i.e. the + * region excluding its boundary). 'p' should be a unit-length vector. + */ + public boolean interiorContains(S2Point p) { + // assert (S2.isUnitLength(p)); + return isFull() || new S1ChordAngle(axis, p).compareTo(radius) < 0; + } + + /** + * Increase the cap radius if necessary to include the given point. If the cap is empty the axis + * is set to the given point, but otherwise it is left unchanged. + * + * @param p must be {@link S2#isUnitLength unit length} + */ + @CheckReturnValue + public S2Cap addPoint(S2Point p) { + // assert (S2.isUnitLength(p)); + if (isEmpty()) { + return new S2Cap(p, S1ChordAngle.ZERO); + } else { + // After adding p to this cap, we require that the result contains p. However we don't need to + // do anything special to achieve this because contains() does exactly the same distance + // calculation that we do here. + return new S2Cap( + axis, S1ChordAngle.fromLength2(Math.max(radius.getLength2(), axis.getDistance2(p)))); + } + } + + /** + * Increase the cap radius if necessary to include the given cap. If the current cap is empty, it + * is set to the given other cap. + */ + @CheckReturnValue + public S2Cap addCap(S2Cap other) { + if (isEmpty()) { + return other; + } else if (other.isEmpty()) { + return this; + } else { + // We round up the distance to ensure that the cap is actually contained. + // TODO(user): Do some error analysis in order to guarantee this. + S1ChordAngle dist = S1ChordAngle.add(new S1ChordAngle(axis, other.axis), other.radius); + S1ChordAngle roundedUp = dist.plusError(S2.DBL_EPSILON * dist.getLength2()); + return new S2Cap(axis, S1ChordAngle.max(radius, roundedUp)); + } + } + + // ////////////////////////////////////////////////////////////////////// + // S2Region interface (see {@code S2Region} for details): + @Override + public S2Cap getCapBound() { + return this; + } + + @Override + public S2LatLngRect getRectBound() { + if (isEmpty()) { + return S2LatLngRect.empty(); + } + if (isFull()) { + return S2LatLngRect.full(); + } + + // Convert the axis to a (lat,lng) pair, and compute the cap angle. + S2LatLng axisLatLng = new S2LatLng(axis); + double capAngle = angle().radians(); + + boolean allLongitudes = false; + double[] lat = new double[2]; + double[] lng = new double[2]; + lng[0] = -S2.M_PI; + lng[1] = S2.M_PI; + + // Check whether cap includes the south pole. + lat[0] = axisLatLng.lat().radians() - capAngle; + if (lat[0] <= -S2.M_PI_2) { + lat[0] = -S2.M_PI_2; + allLongitudes = true; + } + // Check whether cap includes the north pole. + lat[1] = axisLatLng.lat().radians() + capAngle; + if (lat[1] >= S2.M_PI_2) { + lat[1] = S2.M_PI_2; + allLongitudes = true; + } + 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. + double sinA = S1ChordAngle.sin(radius); + double sinC = Math.cos(axisLatLng.lat().radians()); + if (sinA <= sinC) { + double angleA = Math.asin(sinA / sinC); + lng[0] = Platform.IEEEremainder(axisLatLng.lng().radians() - angleA, 2 * S2.M_PI); + lng[1] = Platform.IEEEremainder(axisLatLng.lng().radians() + angleA, 2 * S2.M_PI); + } + } + return new S2LatLngRect(new R1Interval(lat[0], lat[1]), new S1Interval(lng[0], lng[1])); + } + + @Override + public boolean contains(S2Cell cell) { + // If the cap does not contain all cell vertices, return false. + // We check the vertices before taking the Complement() because we can't + // accurately represent the complement of a very small cap (a height + // of 2-epsilon is rounded off to 2). + S2Point[] vertices = new S2Point[4]; + for (int k = 0; k < 4; ++k) { + vertices[k] = cell.getVertex(k); + if (!contains(vertices[k])) { + return false; + } + } + // Otherwise, return true if the complement of the cap does not intersect + // the cell. (This test is slightly conservative, because technically we + // want Complement().InteriorIntersects() here.) + return !complement().intersects(cell, vertices); + } + + @Override + public boolean mayIntersect(S2Cell cell) { + // If the cap contains any cell vertex, return true. + S2Point[] vertices = new S2Point[4]; + for (int k = 0; k < 4; ++k) { + vertices[k] = cell.getVertex(k); + if (contains(vertices[k])) { + return true; + } + } + return intersects(cell, vertices); + } + + /** + * Return true if the cap intersects 'cell', given that the cap vertices have already been + * checked. + */ + public boolean intersects(S2Cell cell, S2Point[] vertices) { + // Return true if this cap intersects any point of 'cell' excluding its + // vertices (which are assumed to already have been checked). + + // 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 (radius.compareTo(S1ChordAngle.RIGHT) >= 0) { + return false; + } + + // We need to check for empty caps due to the axis check just below. + if (isEmpty()) { + return false; + } + + // Optimization: return true if the cell contains the cap axis. (This + // allows half of the edge checks below to be skipped.) + if (cell.contains(axis)) { + return true; + } + + // At this point we know that the cell does not contain the cap axis, + // 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. + + double sin2Angle = S1ChordAngle.sin2(radius); + for (int k = 0; k < 4; ++k) { + S2Point edge = cell.getEdgeRaw(k); + double dot = axis.dotProd(edge); + if (dot > 0) { + // The axis is in the interior half-space defined by the edge. We don't + // 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 we know the axis is not contained with the cell). + continue; + } + // The Norm2() factor is necessary because "edge" is not normalized. + if (dot * dot > sin2Angle * edge.norm2()) { + return false; // Entire cap is on the exterior side of this edge. + } + // 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. + S2Point dir = S2Point.crossProd(edge, axis); + if (dir.dotProd(vertices[k]) < 0 && dir.dotProd(vertices[(k + 1) & 3]) > 0) { + return true; + } + } + return false; + } + + @Override + public boolean contains(S2Point p) { + // The point 'p' should be a unit-length vector. + // assert (S2.isUnitLength(p)); + return new S1ChordAngle(axis, p).compareTo(radius) <= 0; + } + + /** Return true if two caps are identical. */ + @Override + public boolean equals(Object that) { + if (that instanceof S2Cap) { + S2Cap other = (S2Cap) that; + return (axis.equalsPoint(other.axis) && radius.equals(other.radius)) + || (isEmpty() && other.isEmpty()) + || (isFull() && other.isFull()); + } else { + return false; + } + } + + @Override + public int hashCode() { + if (isFull()) { + return 17; + } else if (isEmpty()) { + return 37; + } else { + return 37 * (17 * 37 + axis.hashCode()) + radius.hashCode(); + } + } + + // ///////////////////////////////////////////////////////////////////// + // The following static methods are convenience functions for assertions + // and testing purposes only. + + /** + * Returns true if the radian angle between axes of this and 'other' is at most 'maxError', and + * the chord distance radius between this and 'other' is at most 'maxError'. + */ + boolean approxEquals(S2Cap other, double maxError) { + double r2 = radius.getLength2(); + double otherR2 = other.radius.getLength2(); + return (S2.approxEquals(axis, other.axis, maxError) && Math.abs(r2 - otherR2) <= maxError) + || (isEmpty() && otherR2 <= maxError) + || (other.isEmpty() && r2 <= maxError) + || (isFull() && otherR2 >= 2 - maxError) + || (other.isFull() && r2 >= 2 - maxError); + } + + boolean approxEquals(S2Cap other) { + return approxEquals(other, 1e-14); + } + + @Override + public String toString() { + return "[Point = " + axis + " Radius = " + radius + "]"; + } + + /** Writes this cap to the given output stream. */ + public void encode(OutputStream os) throws IOException { + encode(new LittleEndianOutput(os)); + } + + /** Writes this cap to the given little endian output stream. */ + void encode(LittleEndianOutput os) throws IOException { + axis.encode(os); + os.writeDouble(radius.getLength2()); + } + + /** Returns a new S2Cap decoded from the given input stream. */ + public static S2Cap decode(InputStream is) throws IOException { + return decode(new LittleEndianInput(is)); + } + + /** Returns a new S2Cap decoded from the given little endian input stream. */ + static S2Cap decode(LittleEndianInput is) throws IOException { + S2Point axis = S2Point.decode(is); + S1ChordAngle chord = S1ChordAngle.fromLength2(is.readDouble()); + return S2Cap.fromAxisChord(axis, chord); + } +} diff --git a/core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/S2Cell.java b/core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/S2Cell.java new file mode 100644 index 0000000000..0bd71cdd27 --- /dev/null +++ b/core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/S2Cell.java @@ -0,0 +1,785 @@ +/* + * Copyright 2005 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.mogo.eagle.core.utilcode.geometry; + +import static com.mogo.eagle.core.utilcode.geometry.S2Projections.PROJ; + +import com.google.common.annotations.GwtCompatible; +import com.google.common.collect.Ordering; +import com.google.common.primitives.Doubles; +import java.io.Serializable; + +/** + * An S2Cell is an S2Region object that represents a cell. Unlike S2CellIds, it supports efficient + * containment and intersection tests. However, it is also a more expensive representation. + * + */ +@GwtCompatible(serializable = true) +public final strictfp class S2Cell implements S2Region, Serializable { + byte face; + byte level; + byte orientation; + S2CellId cellId; + double uMin; + double uMax; + double vMin; + double vMax; + + /** Default constructor used only internally. */ + S2Cell() {} + + /** + * An S2Cell always corresponds to a particular S2CellId. The other constructors are just + * convenience methods. + */ + public S2Cell(S2CellId id) { + init(id); + } + + /** Returns the cell corresponding to the given S2 cube face. */ + public static S2Cell fromFace(int face) { + return new S2Cell(S2CellId.fromFace(face)); + } + + /** + * Returns a cell given its face (range 0..5), Hilbert curve position within that face (an + * unsigned integer with {@link S2CellId#POS_BITS} bits), and level (range 0..kMaxLevel). The + * given position will be modified to correspond to the Hilbert curve position at the center of + * the returned cell. This is a static function rather than a constructor in order to indicate + * what the arguments represent. + */ + public static S2Cell fromFacePosLevel(int face, long pos, int level) { + return new S2Cell(S2CellId.fromFacePosLevel(face, pos, level)); + } + + // Convenience methods. + public S2Cell(S2Point p) { + init(S2CellId.fromPoint(p)); + } + + public S2Cell(S2LatLng ll) { + init(S2CellId.fromLatLng(ll)); + } + + public S2CellId id() { + return cellId; + } + + public int face() { + return face; + } + + public byte level() { + return level; + } + + public byte orientation() { + return orientation; + } + + /** Returns true if this cell is a leaf-cell, i.e. it has no children. */ + public boolean isLeaf() { + return level == S2CellId.MAX_LEVEL; + } + + /** As {@link #getVertexRaw(int)}, except the point is normalized to unit length. */ + public S2Point getVertex(int k) { + return S2Point.normalize(getVertexRaw(k)); + } + + /** + * Returns the kth vertex of the cell (k = 0,1,2,3). Vertices are returned in CCW order + * (lower left, lower right, upper right, upper left in the UV plane). The points are not + * necessarily unit length. + */ + public S2Point getVertexRaw(int k) { + // Vertices are returned in the order SW, SE, NE, NW. + return S2Projections.faceUvToXyz( + face, ((k >> 1) ^ (k & 1)) == 0 ? uMin : uMax, (k >> 1) == 0 ? vMin : vMax); + } + + /** As {@link #getEdgeRaw(int)}, except the point is normalized to unit length. */ + public S2Point getEdge(int k) { + return S2Point.normalize(getEdgeRaw(k)); + } + + /** + * Returns the inward-facing normal of the great circle passing through the edge from vertex k to + * vertex k+1 (mod 4). The normals returned by getEdgeRaw are not necessarily unit length. + */ + public S2Point getEdgeRaw(int k) { + switch (k) { + case 0: + return S2Projections.getVNorm(face, vMin); // Bottom + case 1: + return S2Projections.getUNorm(face, uMax); // Right + case 2: + return S2Point.neg(S2Projections.getVNorm(face, vMax)); // Top + default: + return S2Point.neg(S2Projections.getUNorm(face, uMin)); // Left + } + } + + /** As {@link S2CellId#getSizeIJ(int)}, using the level of this cell. */ + public int getSizeIJ() { + return S2CellId.getSizeIJ(level()); + } + + /** + * Returns true if this is not a leaf cell, in which case the array, which must contain at least + * four non-null cells in indices 0..3, will be set to the four children of this cell in traversal + * order. Otherwise, if this is a leaf cell, false is returned without touching the array. + * + *

This method is equivalent to the following: + * + *

+   * for (pos=0, id=childBegin(); !id.equals(childEnd()); id = id.next(), ++pos) {
+   *   children[i].init(id);
+   * }
+   * 
+ * + *

except that it is more than two times faster. + */ + public boolean subdivide(S2Cell[] children) { + // This function is equivalent to just iterating over the child cell ids + // and calling the S2Cell constructor, but it is about 2.5 times faster. + + if (cellId.isLeaf()) { + return false; + } + + // Create four children with the appropriate bounds. + S2CellId id = cellId.childBegin(); + R2Vector mid = getCenterUV(); + double uMid = mid.x(); + double vMid = mid.y(); + for (int pos = 0; pos < 4; ++pos, id = id.next()) { + S2Cell child = children[pos]; + child.face = face; + child.level = (byte) (level + 1); + child.orientation = (byte) (orientation ^ S2.posToOrientation(pos)); + child.cellId = id; + // We want to split the cell in half in "u" and "v". To decide which + // side to set equal to the midpoint value, we look at cell's (i,j) + // position within its parent. The index for "i" is in bit 1 of ij. + int ij = S2.posToIJ(orientation, pos); + // The dimension 0 index (i/u) is in bit 1 of ij. + if ((ij & 0x2) != 0) { + child.uMin = uMid; + child.uMax = uMax; + } else { + child.uMin = uMin; + child.uMax = uMid; + } + // The dimension 1 index (j/v) is in bit 0 of ij. + if ((ij & 0x1) != 0) { + child.vMin = vMid; + child.vMax = vMax; + } else { + child.vMin = vMin; + child.vMax = vMid; + } + } + return true; + } + + /** + * Return the direction vector corresponding to the center in (s,t)-space of the given cell. This + * is the point at which the cell is divided into four subcells; it is not necessarily the + * centroid of the cell in (u,v)-space or (x,y,z)-space. The point returned by GetCenterRaw is not + * necessarily unit length. + */ + public S2Point getCenter() { + return S2Point.normalize(getCenterRaw()); + } + + public S2Point getCenterRaw() { + return cellId.toPointRaw(); + } + + /** Returns the bounds of this cell in (u,v)-space. */ + public R2Rect getBoundUV() { + R2Rect rect = new R2Rect(); + setBoundUV(rect); + return rect; + } + + /** + * Sets the bounds of this cell in (u,v)-space into 'bound'. + * + *

Package private to avoid leaking object mutation outside the api. + */ + void setBoundUV(R2Rect bound) { + bound.x().set(uMin, uMax); + bound.y().set(vMin, vMax); + } + + /** + * Return the center of the cell in (u,v) coordinates (see {@code S2Projections}). Note that the + * center of the cell is defined as the point at which it is recursively subdivided into four + * children; in general, it is not at the midpoint of the (u,v) rectangle covered by the cell + */ + public R2Vector getCenterUV() { + return cellId.getCenterUV(); + } + + /** Return the average area in steradians for cells at the given level. */ + public static double averageArea(int level) { + return PROJ.avgArea.getValue(level); + } + + /** + * Return the average area in steradians of cells at this level. This is accurate to within a + * factor of 1.7 (for S2_QUADRATIC_PROJECTION) and is extremely cheap to compute. + */ + public double averageArea() { + return averageArea(level); + } + + /** + * Return the approximate area of this cell in steradians. 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. + * 300km square or smaller). It is moderately cheap to compute. + */ + public double approxArea() { + + // All cells at the first two levels have the same area. + if (level < 2) { + return averageArea(level); + } + + // 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. + double flatArea = + 0.5 + * S2Point.crossProd( + S2Point.sub(getVertex(2), getVertex(0)), + S2Point.sub(getVertex(3), getVertex(1))) + .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 == flat_area to find the equivalent disc. + return flatArea * 2 / (1 + Math.sqrt(1 - Math.min(S2.M_1_PI * flatArea, 1.0))); + } + + /** + * Return the area in steradians of this cell as accurately as possible. This method is more + * expensive but it is accurate to 6 digits of precision even for leaf cells (whose area is + * approximately 1e-18). + */ + public double exactArea() { + S2Point v0 = getVertex(0); + S2Point v1 = getVertex(1); + S2Point v2 = getVertex(2); + S2Point v3 = getVertex(3); + return S2.area(v0, v1, v2) + S2.area(v0, v2, v3); + } + + // ////////////////////////////////////////////////////////////////////// + // S2Region interface (see {@code S2Region} for details): + + // NOTE: This should be marked as @Override, but clone() isn't present in GWT's version of + // Object, so we can't mark it as such. + @SuppressWarnings("MissingOverride") + public S2Region clone() { + S2Cell clone = new S2Cell(); + clone.face = this.face; + clone.level = this.level; + clone.orientation = this.orientation; + clone.uMin = this.uMin; + clone.uMax = this.uMax; + clone.vMin = this.vMin; + clone.vMax = this.vMax; + return clone; + } + + @Override + public S2Cap getCapBound() { + // 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. + // + // It's possible to show that the two vertices that are furthest from + // the (u,v)-origin never determine the maximum cap size (this is a + // possible future optimization). + + S2Point center = S2Point.normalize(S2Projections.faceUvToXyz(face, getCenterUV())); + S2Cap cap = S2Cap.fromAxisHeight(center, 0); + for (int k = 0; k < 4; ++k) { + cap = cap.addPoint(getVertex(k)); + } + return cap; + } + + /** + * 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 * DBL_EPSILON. + */ + private static final double POLE_MIN_LAT = Math.asin(Math.sqrt(1. / 3)) - 0.5 * S2.DBL_EPSILON; + + @Override + public S2LatLngRect getRectBound() { + 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. + double u = uMin + uMax; + double v = vMin + vMax; + int i = (S2Projections.getUAxis(face).z == 0 ? (u < 0) : (u > 0)) ? 1 : 0; + int j = (S2Projections.getVAxis(face).z == 0 ? (v < 0) : (v > 0)) ? 1 : 0; + R1Interval lat = + R1Interval.fromPointPair( + S2LatLng.latitude(getPoint(i, j)).radians(), + S2LatLng.latitude(getPoint(1 - i, 1 - j)).radians()); + S1Interval lng = + S1Interval.fromPointPair( + S2LatLng.longitude(getPoint(i, 1 - j)).radians(), + S2LatLng.longitude(getPoint(1 - i, j)).radians()); + + // We grow the bounds slightly to make sure that the bounding rectangle + // contains S2LatLng(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 * DBL_EPSILON 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 * DBL_EPSILON. In + // the case of latitude, the normalization error can shift the latitude by + // up to 0.5 * DBL_EPSILON and the other sources of error can cause the + // two latitudes to differ by up to another 1.5 * DBL_EPSILON, which also + // leads to a maximum difference of 2 * DBL_EPSILON. + return new S2LatLngRect(lat, lng) + .expanded(S2LatLng.fromRadians(2 * S2.DBL_EPSILON, 2 * S2.DBL_EPSILON)) + .polarClosure(); + } + + // The face centers are the +X, +Y, +Z, -X, -Y, -Z axes in that order. + // assert (((face < 3) ? 1 : -1) == S2Projections.getNorm(face).get(face % 3)); + + S2LatLngRect bound; + switch (face) { + case 0: + bound = + new S2LatLngRect( + new R1Interval(-S2.M_PI_4, S2.M_PI_4), new S1Interval(-S2.M_PI_4, S2.M_PI_4)); + break; + case 1: + bound = + new S2LatLngRect( + new R1Interval(-S2.M_PI_4, S2.M_PI_4), new S1Interval(S2.M_PI_4, 3 * S2.M_PI_4)); + break; + case 2: + bound = new S2LatLngRect(new R1Interval(POLE_MIN_LAT, S2.M_PI_2), S1Interval.full()); + break; + case 3: + bound = + new S2LatLngRect( + new R1Interval(-S2.M_PI_4, S2.M_PI_4), + new S1Interval(3 * S2.M_PI_4, -3 * S2.M_PI_4)); + break; + case 4: + bound = + new S2LatLngRect( + new R1Interval(-S2.M_PI_4, S2.M_PI_4), new S1Interval(-3 * S2.M_PI_4, -S2.M_PI_4)); + break; + default: + bound = new S2LatLngRect(new R1Interval(-S2.M_PI_2, -POLE_MIN_LAT), S1Interval.full()); + break; + } + + // Finally, we expand the bound to account for the error when a point P is + // converted to an S2LatLng to test for containment. (The bound should be + // large enough so that it contains the computed S2LatLng 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 atan2(), + // which is guaranteed to be semi-monotonic. (In fact the Gnu implementation + // is also correctly rounded, but we don't even need that here.) + return bound.expanded(S2LatLng.fromRadians(S2.DBL_EPSILON, 0)); + } + + @Override + public boolean mayIntersect(S2Cell cell) { + return cellId.intersects(cell.cellId); + } + + @Override + public boolean contains(S2Point p) { + // We can't just call XYZtoFaceUV, because for points that lie on the + // boundary between two faces (i.e. u or v is +1/-1) we need to return + // true for both adjacent cells. + R2Vector uvPoint = S2Projections.faceXyzToUv(face, p); + if (uvPoint == null) { + return false; + } + return (uvPoint.x() >= uMin + && uvPoint.x() <= uMax + && uvPoint.y() >= vMin + && uvPoint.y() <= vMax); + } + + // The point 'p' does not need to be normalized. + @Override + public boolean contains(S2Cell cell) { + return cellId.contains(cell.cellId); + } + + @SuppressWarnings("AndroidJdkLibsChecker") + private double vertexChordDist2(S2Point uvw, DoubleBinaryOperator reducer) { + double d1 = chordDist2(uvw, uMin, vMin); + double d2 = chordDist2(uvw, uMin, vMax); + double d3 = chordDist2(uvw, uMax, vMin); + double d4 = chordDist2(uvw, uMax, vMax); + return reducer.applyAsDouble(d1, reducer.applyAsDouble(d2, reducer.applyAsDouble(d3, d4))); + } + + /** Returns the squared chord distance from {@code uvw} to position {@code uv}. */ + private static double chordDist2(S2Point uvw, double u, double v) { + return uvw.getDistance2(new S2Point(u, v, 1).normalize()); + } + + /** + * Given a point {@code p} and either the lower or upper edge of the {@link S2Cell} (specified by + * setting {@code vEnd} to false or true respectively), returns true if {@code p} is closer to the + * interior of that edge than it is to either endpoint. + */ + private boolean uEdgeIsClosest(S2Point p, boolean vEnd) { + double v = vEnd ? vMax : vMin; + // These are the normals to the planes that are perpendicular to the edge and pass through one + // of its two endpoints. + S2Point dir0 = new S2Point(v * v + 1, -uMin * v, -uMin); + S2Point dir1 = new S2Point(v * v + 1, -uMax * v, -uMax); + return p.dotProd(dir0) > 0 && p.dotProd(dir1) < 0; + } + + /** + * Given a point {@code p} and either the left or right edge of the {@link S2Cell} (specified by + * setting {@code uEnd} to false or true respectively), returns true if {@code p} is closer to the + * interior of that edge than it is to either endpoint. + */ + private boolean vEdgeIsClosest(S2Point p, boolean uEnd) { + double u = uEnd ? uMax : uMin; + // See comments above. + S2Point dir0 = new S2Point(-u * vMin, u * u + 1, -vMin); + S2Point dir1 = new S2Point(-u * vMax, u * u + 1, -vMax); + return p.dotProd(dir0) > 0 && p.dotProd(dir1) < 0; + } + + /** + * Given the dot product of a point P with the normal of a u- or v-edge at the given coordinate + * value, returns the distance from P to that edge. + */ + private static double edgeDistance(double dirIJ, double uv) { + // Let P be the target point and let R be the closest point on the given edge AB. The desired + // distance XR can be expressed as PR^2 = PQ^2 + QR^2 where Q is the point P projected onto the + // plane through the great circle through AB. We can compute the distance PQ^2 perpendicular to + // the plane from "dirIJ" (the dot product of the target point P with the edge normal) and the + // squared length of the edge normal (1 + uv**2). + double pq2 = (dirIJ * dirIJ) / (1 + uv * uv); + + // We can compute the distance QR as (1 - OQ) where O is the sphere origin, + // and we can compute OQ^2 = 1 - PQ^2 using the Pythagorean theorem. + // (This calculation loses accuracy as the angle approaches Pi/2.) + double qr = 1 - Math.sqrt(1 - pq2); + return pq2 + qr * qr; + } + + /** + * Returns the distance from the given point to the cell. Returns zero if the point is inside the + * cell. + */ + public S1ChordAngle getDistance(S2Point targetXyz) { + return S1ChordAngle.fromLength2(getDistanceInternal(targetXyz, true)); + } + + /** Returns the distance to the given cell. Returns zero if one cell contains the other. */ + public S1ChordAngle getDistance(S2Cell target) { + // If the cells intersect, the distance is zero. We use the (u,v) ranges rather than + // S2CellId.intersects() so that cells that share a partial edge or a corner are considered to + // intersect. + if (face == target.face + && uMin <= target.uMax && uMax >= target.uMin + && vMin <= target.vMax && vMax >= target.vMin) { + return S1ChordAngle.ZERO; + } + + // Otherwise, the minimum distance always occurs between a vertex of one cell and an edge of the + // other cell (including the edge endpoints). This represents a total of 32 possible + // (vertex, edge) pairs. + // + // TODO(user): This could be optimized to be at least 5x faster by pruning the set of possible + // closest vertex/edge pairs using the faces and (u,v) ranges of both cells. + S2Point[] va = new S2Point[4]; + S2Point[] vb = new S2Point[4]; + for (int i = 0; i < 4; i++) { + va[i] = getVertex(i); + vb[i] = target.getVertex(i); + } + S1ChordAngle minDist = S1ChordAngle.INFINITY; + for (int i = 0; i < 4; i++) { + for (int j = 0; j < 4; j++) { + minDist = S2EdgeUtil.updateMinDistance(va[i], vb[j], vb[(j + 1) & 3], minDist); + minDist = S2EdgeUtil.updateMinDistance(vb[i], va[j], va[(j + 1) & 3], minDist); + } + } + return minDist; + } + + /** Returns the chord distance to targetXyz, with interior distance 0 iff toInterior is true. */ + private double getDistanceInternal(S2Point targetXyz, boolean toInterior) { + // All calculations are done in the (u,v,w) coordinates of this cell's face. + S2Point targetUvw = S2Projections.faceXyzToUvw(face, targetXyz); + + // Compute dot products with all four upward or rightward-facing edge normals. "dirIJ" is the + // dot product for the edge corresponding to axis I, endpoint J. For example, dir01 is the + // right edge of the S2Cell (corresponding to the upper endpoint of the u-axis). + double dir00 = targetUvw.x - targetUvw.z * uMin; + double dir01 = targetUvw.x - targetUvw.z * uMax; + double dir10 = targetUvw.y - targetUvw.z * vMin; + double dir11 = targetUvw.y - targetUvw.z * vMax; + boolean inside = true; + if (dir00 < 0) { + inside = false; // Target is to the left of the cell. + if (vEdgeIsClosest(targetUvw, false)) { + return edgeDistance(-dir00, uMin); + } + } + if (dir01 > 0) { + inside = false; // Target is to the right of the cell. + if (vEdgeIsClosest(targetUvw, true)) { + return edgeDistance(dir01, uMax); + } + } + if (dir10 < 0) { + inside = false; // Target is below the cell. + if (uEdgeIsClosest(targetUvw, false)) { + return edgeDistance(-dir10, vMin); + } + } + if (dir11 > 0) { + inside = false; // Target is above the cell. + if (uEdgeIsClosest(targetUvw, true)) { + return edgeDistance(dir11, vMax); + } + } + if (inside) { + if (toInterior) { + return 0; + } + + // Although you might think of S2Cells as rectangles, they are actually arbitrary + // quadrilaterals after they are projected onto the sphere. Therefore the simplest approach is + // just to find the minimum distance to any of the four edges. + return Doubles.min( + edgeDistance(-dir00, uMin), + edgeDistance(dir01, uMax), + edgeDistance(-dir10, vMin), + edgeDistance(dir11, vMax)); + } + + // Otherwise, the closest point is one of the four cell vertices. Note that + // it is *not* trivial to narrow down the candidates based on the edge sign + // tests above, because (1) the edges don't meet at right angles and (2) + // there are points on the far side of the sphere that are both above *and* + // below the cell, etc. + return vertexChordDist2(targetUvw, Math::min); + } + + /** Returns the maximum distance from the cell (including its interior) to the given point. */ + public S1ChordAngle getMaxDistance(S2Point target) { + // First check the 4 cell vertices. If all are within the hemisphere centered around target, + // the max distance will be to one of these vertices. + S2Point targetUvw = S2Projections.faceXyzToUvw(face, target); + double maxDist = vertexChordDist2(targetUvw, Math::max); + if (maxDist <= S1ChordAngle.RIGHT.getLength2()) { + return S1ChordAngle.fromLength2(maxDist); + } + + // Otherwise, find the minimum distance d_min to the antipodal point and the maximum distance + // will be Pi - d_min. + return S1ChordAngle.sub(S1ChordAngle.STRAIGHT, getDistance(target.neg())); + } + + /** Returns the maximum distance from the cell (including its interior) to the given edge AB. */ + public S1ChordAngle getMaxDistance(S2Point a, S2Point b) { + // If the maximum distance from both endpoints to the cell is less than Pi/2 then the maximum + // distance from the edge to the cell is the maximum of the two endpoint distances. + S1ChordAngle da = getMaxDistance(a); + S1ChordAngle db = getMaxDistance(b); + S1ChordAngle maxDist = da.compareTo(db) < 0 ? db : da; + // TODO(eengle): Use a thresholded distance via S2Predicates. + if (maxDist.compareTo(S1ChordAngle.RIGHT) <= 0) { + return maxDist; + } else { + return S1ChordAngle.sub(S1ChordAngle.STRAIGHT, getDistanceToEdge(a.neg(), b.neg())); + } + } + + /** Returns the maximum distance from the cell, including interior, to the given target cell. */ + public S1ChordAngle getMaxDistance(S2Cell target) { + // If the antipodal target intersects the cell, the distance is S1ChordAngle.STRAIGHT. + // The antipodal UV is the transpose of the original UV, interpreted within the opposite face. + if (face == (target.face >= 3 ? target.face - 3 : target.face + 3) + && uMin <= target.vMax && uMax >= target.vMin + && vMin <= target.uMax && vMax >= target.uMin) { + return S1ChordAngle.STRAIGHT; + } + + // Otherwise, the maximum distance always occurs between a vertex of one cell and an edge of the + // other cell (including the edge endpoints). This represents a total of 32 possible + // (vertex, edge) pairs. + // + // TODO(eengle): When the maximum distance is at most Pi/2, the maximum is always attained + // between a pair of vertices, and this could be made much faster by testing each vertex pair + // once rather than the current 4 times. + S2Point[] va = new S2Point[4]; + S2Point[] vb = new S2Point[4]; + for (int i = 0; i < 4; i++) { + va[i] = getVertex(i); + vb[i] = target.getVertex(i); + } + S1ChordAngle maxDist = S1ChordAngle.NEGATIVE; + for (int i = 0; i < 4; i++) { + for (int j = 0; j < 4; j++) { + maxDist = S2EdgeUtil.updateMaxDistance(va[i], vb[j], vb[(j + 1) & 3], maxDist); + maxDist = S2EdgeUtil.updateMaxDistance(vb[i], va[j], va[(j + 1) & 3], maxDist); + } + } + return maxDist; + } + + /** + * Returns the minimum distance from the cell to the given edge AB, or zero if the edge intersects + * the cell interior. + */ + public S1ChordAngle getDistanceToEdge(S2Point a, S2Point b) { + // Possible optimizations: + // - Currently the (cell vertex, edge endpoint) distances are computed twice each, and the + // length of AB is computed 4 times. + // - To fix this, refactor GetDistance(target) so that it skips calculating the distance to + // each cell vertex. Instead, compute the cell vertices and distances in this function, and + // add a low-level UpdateMinDistance that allows the XA, XB, and AB distances to be passed + // in. + // - It might also be more efficient to do all calculations in UVW-space, since this would + // involve transforming 2 points rather than 4. + + // First, check the minimum distance to the edge endpoints A and B. (This also detects whether + // either endpoint is inside the cell.) + S1ChordAngle minDist = Ordering.natural().min(getDistance(a), getDistance(b)); + if (minDist.isZero()) { + return minDist; + } + + // Otherwise, check whether the edge crosses the cell boundary. + S2Point[] v = new S2Point[4]; + for (int i = 0; i < 4; i++) { + v[i] = getVertex(i); + } + S2EdgeUtil.EdgeCrosser crosser = new S2EdgeUtil.EdgeCrosser(a, b, v[3]); + for (int i = 0; i < 4; i++) { + if (crosser.robustCrossing(v[i]) >= 0) { + return S1ChordAngle.ZERO; + } + } + + // Finally, check whether the minimum distance occurs between a cell vertex and the interior of + // the edge AB. (Some of this work is redundant, since it also checks the distance to the + // endpoints A and B again.) + // + // Note that we don't need to check the distance from the interior of AB to the interior of a + // cell edge, because the only way that this distance can be minimal is if the two edges cross + // (already checked above). + for (int i = 0; i < 4; i++) { + minDist = S2EdgeUtil.updateMinDistance(v[i], a, b, minDist); + } + + return minDist; + } + + /** Returns the distance from the cell boundary to the given point. */ + public S1ChordAngle getBoundaryDistance(S2Point target) { + return S1ChordAngle.fromLength2(getDistanceInternal(target, false)); + } + + private void init(S2CellId id) { + // Set cell properties from the ID and the FaceIJ of the ID. + cellId = id; + face = (byte) id.face(); + long ijo = id.toIJOrientation(); + orientation = (byte) S2CellId.getOrientation(ijo); + level = (byte) id.level(); + int i = S2CellId.getI(ijo); + int j = S2CellId.getJ(ijo); + int cellSize = id.getSizeIJ(); + this.uMin = S2Projections.PROJ.ijToUV(i, cellSize); + this.uMax = S2Projections.PROJ.ijToUV(i + cellSize, cellSize); + this.vMin = S2Projections.PROJ.ijToUV(j, cellSize); + this.vMax = S2Projections.PROJ.ijToUV(j + cellSize, cellSize); + } + + private S2Point getPoint(int i, int j) { + return S2Projections.faceUvToXyz(face, i == 0 ? uMin : uMax, j == 0 ? vMin : vMax); + } + + @Override + public String toString() { + return "[" + face + ", " + level + ", " + orientation + ", " + cellId + "]"; + } + + @Override + public int hashCode() { + int value = 17; + value = 37 * (37 * (37 * value + face) + orientation) + level; + return 37 * value + id().hashCode(); + } + + @Override + public boolean equals(Object that) { + if (that instanceof S2Cell) { + S2Cell thatCell = (S2Cell) that; + return this.face == thatCell.face + && this.level == thatCell.level + && this.orientation == thatCell.orientation + && this.cellId.equals(thatCell.cellId); + } + return false; + } + + /* A double function of two double parameters. */ + // TODO(eengle): Remove this once the android JDK has this interface. + private interface DoubleBinaryOperator { + /** Returns the result of this function applied to {@code a} and {@code b}. */ + double applyAsDouble(double a, double b); + } +} diff --git a/core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/S2CellId.java b/core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/S2CellId.java new file mode 100644 index 0000000000..3473d6cebc --- /dev/null +++ b/core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/S2CellId.java @@ -0,0 +1,1323 @@ +/* + * Copyright 2005 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.mogo.eagle.core.utilcode.geometry; + +import static com.mogo.eagle.core.utilcode.geometry.S2Projections.PROJ; + +import com.google.common.annotations.GwtCompatible; +import com.google.common.base.Ascii; +import com.google.common.base.CharMatcher; +import com.google.common.base.Preconditions; +import com.google.common.base.Strings; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Lists; +import com.google.common.collect.UnmodifiableIterator; +import com.google.common.primitives.UnsignedLongs; +import com.google.errorprone.annotations.Immutable; +import java.io.Serializable; +import java.util.Collection; +import java.util.Iterator; +import java.util.List; +import java.util.NoSuchElementException; + +/** + * An S2CellId is a 64-bit unsigned integer that uniquely identifies a cell in the S2 cell + * decomposition. It has the following format: + * + *

+ * id = [face][face_pos]
+ * 
+ * + *

face: a 3-bit number (range 0..5) encoding the cube face. + * + *

face_pos: a 61-bit number encoding the position of the center of this cell along the Hilbert + * curve over this face (see the Wiki pages for details). + * + *

Sequentially increasing cell ids follow a continuous space-filling curve over the entire + * sphere. They have the following properties: + * + *

    + *
  • The id of a cell at level k consists of a 3-bit face number followed by k bit pairs that + * recursively select one of the four children of each cell. The next bit is always 1, and all + * other bits are 0. Therefore, the level of a cell is determined by the position of its + * lowest-numbered bit that is turned on (for a cell at level k, this position is 2 * + * (MAX_LEVEL - k).) + *
  • The id of a parent cell is at the midpoint of the range of ids spanned by its children (or + * by its descendants at any level). + *
+ * + *

Leaf cells are often used to represent points on the unit sphere, and this class provides + * methods for converting directly between these two representations. For cells that represent 2D + * regions rather than discrete point, it is better to use the S2Cell class. + * + */ +@Immutable +@GwtCompatible(emulated = true, serializable = true) +public final strictfp class S2CellId implements Comparable, Serializable { + // Although only 60 bits are needed to represent the index of a leaf + // cell, we need an extra bit in order to represent the position of + // the center of the leaf cell along the Hilbert curve. + public static final int FACE_BITS = 3; + public static final int NUM_FACES = 6; + public static final int MAX_LEVEL = 30; // Valid levels: 0..MAX_LEVEL + public static final int POS_BITS = 2 * MAX_LEVEL + 1; + public static final int MAX_SIZE = 1 << MAX_LEVEL; + + /** The change in ST coordinates for each unit change in IJ coordinates. */ + private static final double IJ_TO_ST = 1.0 / MAX_SIZE; + + // Constant related to unsigned long's + public static final long MAX_UNSIGNED = -1L; // Equivalent to 0xffffffffffffffffL + + // Used to encode the i, j, and orientation values into primitive longs. + private static final int I_SHIFT = 33; + private static final int J_SHIFT = 2; + private static final long J_MASK = (1L << 31) - 1; + private static final long ORIENTATION_MASK = (1L << 2) - 1; + + // Used to encode the si and ti values into primitive longs. + private static final int SI_SHIFT = 32; + private static final long TI_MASK = (1L << 32) - 1; + + // The following lookup tables are used to convert efficiently between an + // (i,j) cell index and the corresponding position along the Hilbert curve. + // "LOOKUP_POS" maps 4 bits of "i", 4 bits of "j", and 2 bits representing the + // orientation of the current cell into 8 bits representing the order in which + // that subcell is visited by the Hilbert curve, plus 2 bits indicating the + // new orientation of the Hilbert curve within that subcell. (Cell + // orientations are represented as combination of SWAP_MASK and INVERT_MASK.) + // + // "LOOKUP_IJ" is an inverted table used for mapping in the opposite + // direction. + // + // We also experimented with looking up 16 bits at a time (14 bits of position + // plus 2 of orientation) but found that smaller lookup tables gave better + // performance. (2KB fits easily in the primary cache.) + + // Values for these constants are *declared* in the *.h file. Even though + // the declaration specifies a value for the constant, that declaration + // is not a *definition* of storage for the value. Because the values are + // supplied in the declaration, we don't need the values here. Failing to + // define storage causes link errors for any code that tries to take the + // address of one of these values. + private static final int LOOKUP_BITS = 4; + private static final int SWAP_MASK = 0x01; + private static final int INVERT_MASK = 0x02; + private static final int LOOKUP_MASK = (1 << LOOKUP_BITS) - 1; + + private static final int[] LOOKUP_POS = new int[1 << (2 * LOOKUP_BITS + 2)]; + private static final int[] LOOKUP_IJ = new int[1 << (2 * LOOKUP_BITS + 2)]; + + private static final S2CellId NONE = new S2CellId(); + private static final S2CellId SENTINEL = new S2CellId(MAX_UNSIGNED); + + /** + * This is the offset required to wrap around from the beginning of the Hilbert curve to the end + * or vice versa; see nextWrap() and prevWrap(). + */ + private static final long WRAP_OFFSET = ((long) NUM_FACES) << POS_BITS; + + static { + initLookupCell(0, 0, 0, 0, 0, 0); + initLookupCell(0, 0, 0, SWAP_MASK, 0, SWAP_MASK); + initLookupCell(0, 0, 0, INVERT_MASK, 0, INVERT_MASK); + initLookupCell(0, 0, 0, SWAP_MASK | INVERT_MASK, 0, SWAP_MASK | INVERT_MASK); + } + + public static final S2CellId[] FACE_CELLS = new S2CellId[6]; + + static { + for (int face = 0; face < 6; face++) { + FACE_CELLS[face] = fromFace(face); + } + } + + /** The id of the cell. */ + private final long id; + + public S2CellId(long id) { + this.id = id; + } + + public S2CellId() { + this.id = 0; + } + + /** The default constructor returns an invalid cell id. */ + public static S2CellId none() { + return NONE; + } + + /** + * Returns an invalid cell id guaranteed to be larger than any valid cell id. Useful for creating + * indexes. + */ + public static S2CellId sentinel() { + return SENTINEL; // -1 + } + + /** Returns the cell corresponding to a given S2 cube face. */ + public static S2CellId fromFace(int face) { + return new S2CellId(fromFaceAsLong(face)); + } + + /** + * Returns a cell given its face (range 0..5), Hilbert curve position within that face (an + * unsigned integer with {@link #POS_BITS} bits), and level (range 0..MAX_LEVEL). The given + * position will be modified to correspond to the Hilbert curve position at the center of the + * returned cell. This is a static function rather than a constructor in order to indicate what + * the arguments represent. + */ + public static S2CellId fromFacePosLevel(int face, long pos, int level) { + return new S2CellId(fromFacePosLevelAsLong(face, pos, level)); + } + + /** + * Return the leaf cell containing the given point (a direction vector, not necessarily unit + * length). + */ + public static S2CellId fromPoint(S2Point p) { + int face = S2Projections.xyzToFace(p); + S2Projections.UvTransform t = S2Projections.faceToUvTransform(face); + int i = S2Projections.stToIj(PROJ.uvToST(t.xyzToU(p.x, p.y, p.z))); + int j = S2Projections.stToIj(PROJ.uvToST(t.xyzToV(p.x, p.y, p.z))); + return fromFaceIJ(face, i, j); + } + + /** Return the leaf cell containing the given S2LatLng. */ + public static S2CellId fromLatLng(S2LatLng ll) { + return fromPoint(ll.toPoint()); + } + + /** + * Returns the center of the cell in (u,v) coordinates. Note that the center of the cell is + * defined as the point at which it is recursively subdivided into four children; in general, it + * is not at the midpoint of the (u,v) rectangle covered by the cell. + */ + public R2Vector getCenterUV() { + long center = getCenterSiTi(); + return new R2Vector( + PROJ.stToUV(S2Projections.siTiToSt(getSi(center))), + PROJ.stToUV(S2Projections.siTiToSt(getTi(center)))); + } + + /** Returns the center of the cell in (s,t) coordinates. */ + public R2Vector getCenterST() { + long center = getCenterSiTi(); + return new R2Vector( + S2Projections.siTiToSt(getSi(center)), S2Projections.siTiToSt(getTi(center))); + } + + /** Returns the bounds of this cell in (s,t)-space. */ + public R2Rect getBoundST() { + double size = getSizeST(); + return R2Rect.fromCenterSize(getCenterST(), new R2Vector(size, size)); + } + + /** Returns the bounds of this cell in (u,v)-space. */ + public R2Rect getBoundUV() { + long ijo = toIJOrientation(); + return ijLevelToBoundUv(getI(ijo), getJ(ijo), level()); + } + + public S2Point toPoint() { + return S2Point.normalize(toPointRaw()); + } + + /** + * Return the direction vector corresponding to the center of the given cell. The vector returned + * by toPointRaw is not necessarily unit length. + */ + public S2Point toPointRaw() { + long center = getCenterSiTi(); + return PROJ.faceSiTiToXyz(face(), getSi(center), getTi(center)); + } + + /** + * Returns a loop along the boundary of this cell, with vertices at intersections with the cell + * grid at {@code level}. Equivalent to the union of new S2Polygon(new S2Cell(child)), for each + * child in {@link #childrenAtLevel(int)} for the given level, but radically faster. + */ + public S2Loop toLoop(int level) { + S2Projections p = S2Projections.PROJ; + int depth = level - level(); + Preconditions.checkState(depth >= 0); + R2Rect rect = getBoundST(); + int face = face(); + int n = 1 << depth; + double step = Math.scalb(1, -depth); + List points = Lists.newArrayListWithCapacity(4 * n); + R2Vector b = rect.getVertex(3); + for (int corner = 0; corner < 4; corner++) { + R2Vector a = b; + b = rect.getVertex(corner); + points.add(S2Point.normalize(S2Projections.faceUvToXyz(face, p.stToUV(a.x), p.stToUV(a.y)))); + for (double d = step; d < 1; d += step) { + double s = (1 - d) * a.x + d * b.x; + double t = (1 - d) * a.y + d * b.y; + points.add(S2Point.normalize(S2Projections.faceUvToXyz(face, p.stToUV(s), p.stToUV(t)))); + } + } + return new S2Loop(points); + } + + /** + * Returns the (si, ti) coordinates of the center of the cell. The returned long packs the values + * into one long, such that bits 32-63 contain si, and bits 0-31 contain ti. + * + *

Note that although (si, ti) coordinates span the range [0,2**31] in general, the cell center + * coordinates are always in the range [1,2**31-1] and therefore can be represented using a signed + * 32-bit integer. + * + *

Use {@link #getSi(long)} and {@link #getTi(long)} to extract integer values for si and ti, + * respectively. + */ + long getCenterSiTi() { + // First we compute the discrete (i,j) coordinates of a leaf cell contained + // within the given cell. Given that cells are represented by the Hilbert + // curve position corresponding at their center, it turns out that the cell + // returned by ToFaceIJOrientation is always one of two leaf cells closest + // to the center of the cell (unless the given cell is a leaf cell itself, + // in which case there is only one possibility). + // + // Given a cell of size s >= 2 (i.e. not a leaf cell), and letting (imin, + // jmin) be the coordinates of its lower left-hand corner, the leaf cell + // returned by ToFaceIJOrientation() is either (imin + s/2, jmin + s/2) or + // (imin + s/2 - 1, jmin + s/2 - 1). The first case is the one we want. + // We can distinguish these two cases by looking at the low bit of "i" or + // "j". In the second case the low bit is one, unless s == 2 (i.e. the + // level just above leaf cells) in which case the low bit is zero. + // + // In the code below, the expression ((i ^ ((int) id >> 2)) & 1) is nonzero + // if we are in the second case described above. + long ijo = toIJOrientation(); + int i = getI(ijo); + int j = getJ(ijo); + int delta = isLeaf() ? 1 : (((i ^ (((int) id) >>> 2)) & 1) != 0) ? 2 : 0; + // Note that (2 * {i,j} + delta) will never overflow a 32-bit integer. Thus, + // we can embed both integers into a single primitive long. Bits 32-63 hold + // the value for si, and bits 0-31 hold the value for ti. + return (((long) (2 * i + delta)) << SI_SHIFT) | ((2 * j + delta) & TI_MASK); + } + + /** + * Returns the "si" coordinate from bits 32-63 in the given {@code center} primitive long returned + * by {@link #getCenterSiTi()}. + */ + static int getSi(long center) { + return (int) (center >> SI_SHIFT); + } + + /** + * Returns the "ti" coordinate from bits 0-31 in the given {@code center} primitive long returned + * by {@link #getCenterSiTi()}. + */ + static int getTi(long center) { + return (int) center; + } + + /** Return the S2LatLng corresponding to the center of the given cell. */ + public S2LatLng toLatLng() { + return new S2LatLng(toPointRaw()); + } + + /** The 64-bit unique identifier for this cell. */ + public long id() { + return id; + } + + /** Return true if id() represents a valid cell. */ + public boolean isValid() { + return face() < NUM_FACES && ((lowestOnBit() & (0x1555555555555555L)) != 0); + } + + /** Which cube face this cell belongs to, in the range 0..5. */ + public int face() { + return (int) (id >>> POS_BITS); + } + + /** + * The position of the cell center along the Hilbert curve over this face, in the range + * 0..(2**kPosBits-1). + */ + public long pos() { + return (id & (-1L >>> FACE_BITS)); + } + + /** Return the subdivision level of the cell (range 0..MAX_LEVEL). */ + public int level() { + // Fast path for leaf cells (benchmarking shows this is worthwhile.) + if (isLeaf()) { + return MAX_LEVEL; + } + return MAX_LEVEL - (Long.numberOfTrailingZeros(id) >> 1); + } + + /** As {@link #getSizeIJ(int)}, using the level of this cell. */ + public int getSizeIJ() { + return getSizeIJ(level()); + } + + /** As {@link #getSizeST(int)}, using the level of this cell. */ + public double getSizeST() { + return getSizeST(level()); + } + + /** Returns the edge length of cells at the given level in (i,j)-space. */ + public static int getSizeIJ(int level) { + return 1 << (MAX_LEVEL - level); + } + + /** Returns the edge length of cells at the given level in (s,t)-space. */ + public static double getSizeST(int level) { + return S2Projections.ijToStMin(getSizeIJ(level)); + } + + /** + * Return true if this is a leaf cell (more efficient than checking whether level() == MAX_LEVEL). + */ + public boolean isLeaf() { + return ((int) id & 1) != 0; + } + + /** + * Return true if this is a top-level face cell (more efficient than checking whether level() == + * 0). + */ + public boolean isFace() { + return (id & (lowestOnBitForLevel(0) - 1)) == 0; + } + + /** + * Return 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..MAX_LEVEL. For example, childPosition(1) returns + * the position of this cell's level-1 ancestor within its top-level face cell. + */ + public int childPosition(int level) { + return (int) (id >>> (2 * (MAX_LEVEL - level) + 1)) & 3; + } + + /** + * Returns the start of the range of cell ids that are contained within this cell (including + * itself.) The range is *inclusive* (i.e. test using >= and <=) and the return values of both + * this method and {@link #rangeMax()} are valid leaf cell ids. + */ + public S2CellId rangeMin() { + return new S2CellId(rangeMinAsLong(id)); + } + + /** + * Returns the end of the range of cell ids that are contained within this cell (including + * itself.) The range is *inclusive* (i.e. test using >= and <=) and the return values of both + * this method and {@link #rangeMin()} are valid leaf cell ids. + * + *

Note that because the range max is inclusive, care should be taken to iterate accordingly, + * for example: + * for (S2CellId min = x.rangeMin(); min.compareTo(x.rangeMax()) <= 0; min = min.next()) {...} + * If you need to convert the range to a semi-open interval [min, limit), for example to + * use a key-value store that only supports semi-open range queries, then do not attempt to define + * "limit" as rangeMax.next(). The problem is that leaf S2CellIds are 2 units apart, so the + * semi-open interval [min, limit) includes an additional value (rangeMax.id() + 1) which happens + * to be a valid S2CellId about one-third of the time and is never contained by this cell. (It + * always corresponds to a cell ID larger than this ID). You can define "limit" as {@code + * rangeMax.id() + 1} if necessary; this is not always a valid S2CellId but can still be used with + * fromToken/toToken. You may also convert rangeMax() to the key space of your key- value store + * and define "limit" as the next larger key. + * + *

Note that sentinel().rangeMin(), sentinel.rangeMax(), and sentinel() are all equal. + * + * @see S2CellId#rangeMin + * @see S2CellId#childBegin(int) + * @see S2CellId#childEnd(int) + * @see S2CellId#childrenAtLevel(int) + */ + public S2CellId rangeMax() { + return new S2CellId(rangeMaxAsLong(id)); + } + + /** Return true if the given cell is contained within this one. */ + public boolean contains(S2CellId other) { + // assert (isValid() && other.isValid()); + return unsignedLongGreaterOrEquals(other.id, rangeMinAsLong(id)) + && unsignedLongLessOrEquals(other.id, rangeMaxAsLong(id)); + } + + /** Return true if the given cell intersects this one. */ + public boolean intersects(S2CellId other) { + // assert (isValid() && other.isValid()); + return unsignedLongLessOrEquals(rangeMinAsLong(other.id), rangeMaxAsLong(id)) + && unsignedLongGreaterOrEquals(rangeMaxAsLong(other.id), rangeMinAsLong(id)); + } + + public S2CellId parent() { + // assert (isValid() && level() > 0); + return new S2CellId(parentAsLong(id)); + } + + /** + * Return the cell at the previous level or at the given level (which must be less than or equal + * to the current level). + */ + public S2CellId parent(int level) { + // assert (isValid() && level >= 0 && level <= this.level()); + return new S2CellId(parentAsLong(id, level)); + } + + /** + * Returns the immediate child of this cell at the given traversal order position (in the range 0 + * to 3). Results are undefined if this is a leaf cell. + */ + public S2CellId child(int position) { + // assert (isValid()); + // assert (!isLeaf()); + // To change the level, we need to move the least-significant bit two positions downward. We do + // this by subtracting (4 * new_lsb) and adding new_lsb. Then to advance to the given child + // cell, we add (2 * position * new_lsb). + long newLsb = lowestOnBit() >>> 2; + return new S2CellId(id + (2 * position + 1 - 4) * newLsb); + } + + public Iterable children() { + if (isLeaf()) { + return ImmutableList.of(); + } else { + return childrenAtLevel(level() + 1); + } + } + + public Iterable childrenAtLevel(final int level) { + Preconditions.checkState(isValid()); + Preconditions.checkArgument(level >= this.level() && level <= MAX_LEVEL); + return new Iterable() { + @Override + public Iterator iterator() { + return new UnmodifiableIterator() { + private S2CellId next = childBegin(level); + private long childEnd = childEnd(level).id(); + + @Override + public boolean hasNext() { + return next.id() != childEnd; + } + + @Override + public S2CellId next() { + if (!hasNext()) { + throw new NoSuchElementException(); + } + S2CellId oldNext = next; + next = next.next(); + return oldNext; + } + }; + } + }; + } + + // Iterator-style methods for traversing the immediate children of a cell or + // all of the children at a given level (greater than or equal to the current + // level). Note that the end value is exclusive, just like standard STL + // iterators, and may not even be a valid cell id. You should iterate using + // code like this: + // + // for (S2CellId c = id.childBegin(); !c.equals(id.childEnd()); c = c.next()) + // ... + // + // The convention for advancing the iterator is "c = c.next()", so be sure + // to use 'equals()' in the loop guard, or compare 64-bit cell id's, + // rather than "c != id.childEnd()". + + /** + * Returns the first child in a traversal of the children of this cell, in Hilbert curve order. + */ + public S2CellId childBegin() { + // assert (isValid() && level() < MAX_LEVEL); + return new S2CellId(childBeginAsLong(id)); + } + + /** + * Returns the first cell in a traversal of children a given level deeper than this cell, in + * Hilbert curve order. Requires that the given level be greater or equal to this cell level. + */ + public S2CellId childBegin(int level) { + // assert (isValid() && level >= this.level() && level <= MAX_LEVEL); + return new S2CellId(childBeginAsLong(id, level)); + } + + /** + * Returns the first cell after a traversal of the children of this cell in Hilbert curve order. + * This cell can be invalid. + */ + public S2CellId childEnd() { + // assert (isValid() && level() < MAX_LEVEL); + return new S2CellId(childEndAsLong(id)); + } + + /** + * Returns the first cell after the last child in a traversal of children a given level deeper + * than this cell, in Hilbert curve order. This cell can be invalid. + */ + public S2CellId childEnd(int level) { + // assert (isValid() && level >= this.level() && level <= MAX_LEVEL); + return new S2CellId(childEndAsLong(id, level)); + } + + /** + * Return the next cell at the same level along the Hilbert curve. Works correctly when advancing + * from one face to the next, but does *not* wrap around from the last face to the first or vice + * versa. + */ + public S2CellId next() { + return new S2CellId(id + (lowestOnBit() << 1)); + } + + /** + * Return the previous cell at the same level along the Hilbert curve. Works correctly when + * advancing from one face to the next, but does *not* wrap around from the last face to the first + * or vice versa. + */ + public S2CellId prev() { + return new S2CellId(id - (lowestOnBit() << 1)); + } + + /** + * Like next(), but wraps around from the last face to the first and vice versa. Should *not* be + * used for iteration in conjunction with childBegin(), childEnd(), Begin(), or End(). + */ + public S2CellId nextWrap() { + S2CellId n = next(); + if (unsignedLongLessThan(n.id, WRAP_OFFSET)) { + return n; + } + return new S2CellId(n.id - WRAP_OFFSET); + } + + /** + * Like prev(), but wraps around from the last face to the first and vice versa. Should *not* be + * used for iteration in conjunction with childBegin(), childEnd(), Begin(), or End(). + */ + public S2CellId prevWrap() { + S2CellId p = prev(); + if (UnsignedLongs.compare(p.id, WRAP_OFFSET) < 0) { + return p; + } + return new S2CellId(p.id + WRAP_OFFSET); + } + + /** + * Returns the first cell in an ordered traversal along the Hilbert curve at a given level (across + * all 6 faces of the cube). + */ + public static S2CellId begin(int level) { + return new S2CellId(childBeginAsLong(fromFaceAsLong(0), level)); + } + + /** + * Returns the first cell after an ordered traversal along the Hilbert curve at a given level + * (across all 6 faces of the cube). The end value is exclusive, and is not a valid cell id. + */ + public static S2CellId end(int level) { + return new S2CellId(childEndAsLong(fromFaceAsLong(5), level)); + } + + /** + * This method advances or retreats the indicated number of steps along the Hilbert curve at the + * current level, and returns the new position. The position never advances past {@link #end(int)} + * or before {@link #begin(int)}, and remains at the current level. + */ + public S2CellId advance(long steps) { + if (steps == 0) { + return this; + } + + // 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. + int stepShift = 2 * (MAX_LEVEL - level()) + 1; + if (steps < 0) { + long minSteps = -(id >>> stepShift); + if (steps < minSteps) { + steps = minSteps; + } + } else { + long maxSteps = (WRAP_OFFSET + lowestOnBit() - id) >>> stepShift; + if (steps > maxSteps) { + steps = maxSteps; + } + } + return new S2CellId(id + (steps << stepShift)); + } + + /** + * This method advances or retreats the indicated number of steps along the Hilbert curve at the + * current level, and returns the new position. The position wraps between the first and last + * faces as necessary. The input must be a valid cell id. + */ + public S2CellId advanceWrap(long steps) { + // assert (isValid()); + if (steps == 0) { + return this; + } + + int stepShift = 2 * (MAX_LEVEL - level()) + 1; + if (steps < 0) { + long minSteps = -(id >>> stepShift); + if (steps < minSteps) { + long stepWrap = WRAP_OFFSET >>> stepShift; + steps %= stepWrap; + if (steps < minSteps) { + steps += stepWrap; + } + } + } else { + // Unlike advance(), we don't want to return end(level). + long maxSteps = (WRAP_OFFSET - id) >>> stepShift; + if (steps > maxSteps) { + long stepWrap = WRAP_OFFSET >>> stepShift; + steps %= stepWrap; + if (steps > maxSteps) { + steps -= stepWrap; + } + } + } + return new S2CellId(id + (steps << stepShift)); + } + + /** + * Returns the level of the "lowest common ancestor" of this cell and "other". Note that because + * of the way that cell levels are numbered, this is actually the *highest* level of any shared + * ancestor. Returns -1 if the two cells do not have any common ancestor (i.e., they are from + * different faces). + */ + public int getCommonAncestorLevel(S2CellId other) { + // Basically we find the first bit position at which the two S2CellIds differ and convert that + // to a level. The max() below is necessary for the case where one S2CellId is a descendant of + // the other. + long bits = UnsignedLongs.max(id ^ other.id, lowestOnBit(), other.lowestOnBit()); + + // Compute the position of the most significant bit, and then map + // {0} -> 30, {1,2} -> 29, {3,4} -> 28, ... , {59,60} -> 0, {61,62,63} -> -1. + return Math.max(Long.numberOfLeadingZeros(bits) - 3, -1) >> 1; + } + + + /** + * Decodes the cell id from a compact text string suitable for display or indexing. Cells at lower + * levels (i.e. larger cells) are encoded into fewer characters. The maximum token length is 16. + * + * @param token the token to decode + * @return the S2CellId for that token + * @throws NumberFormatException if the token is not formatted correctly + */ + public static S2CellId fromToken(String token) { + return fromTokenImpl(token, true); + } + + /** + * Returns the cell id for the given token, which will be implicitly zero-right-padded to length + * 16 if 'implicitZeroes' is true. + */ + private static S2CellId fromTokenImpl(String token, boolean implicitZeroes) { + if (token == null) { + throw new NumberFormatException("Null string in S2CellId.fromToken"); + } + if (token.isEmpty()) { + throw new NumberFormatException("Empty string in S2CellId.fromToken"); + } + int length = token.length(); + if (length > 16 || "X".equals(token)) { + return none(); + } + + long value = 0; + for (int pos = 0; pos < length; pos++) { + int digitValue = Character.digit(token.charAt(pos), 16); + if (digitValue == -1) { + throw new NumberFormatException(token); + } + value = value * 16 + digitValue; + } + + if (implicitZeroes) { + value = value << (4 * (16 - length)); + } + + return new S2CellId(value); + } + + /** + * Encodes the cell id to compact text strings suitable for display or indexing. Cells at lower + * levels (i.e. larger cells) are encoded into fewer characters. The maximum token length is 16. + * + *

Simple implementation: convert the id to hex and strip trailing zeros. We could use base-32 + * or base-64, but assuming the cells used for indexing regions are at least 100 meters across + * (level 16 or less), the savings would be at most 3 bytes (9 bytes hex vs. 6 bytes base-64). + * + * @return the encoded cell id + */ + public String toToken() { + if (id == 0) { + return "X"; + } + + // Convert to a hex string with as many digits as necessary. + String hex = Ascii.toLowerCase(Long.toHexString(id)); + // Prefix 0s to get a length 16 string. + String padded = Strings.padStart(hex, 16, '0'); + // Trim zeroes off the end. + return MATCHES_ZERO.trimTrailingFrom(padded); + } + + /** Matches literal '0' characters. */ + private static final CharMatcher MATCHES_ZERO = CharMatcher.is('0'); + + public String toTokenOld() { + String hex = Ascii.toLowerCase(Long.toHexString(id)); + StringBuilder sb = new StringBuilder(16); + for (int i = hex.length(); i < 16; i++) { + sb.append('0'); + } + sb.append(hex); + for (int len = 16; len > 0; len--) { + if (sb.charAt(len - 1) != '0') { + return sb.substring(0, len); + } + } + + throw new RuntimeException("Shouldn't make it here"); + } + + /** + * Return the four cells that are adjacent across the cell's four edges. Neighbors are returned in + * the order defined by S2Cell::GetEdge. All neighbors are guaranteed to be distinct. + * + *

Requires that this cell is valid. + */ + public void getEdgeNeighbors(S2CellId[] neighbors) { + int level = this.level(); + int size = getSizeIJ(level); + int face = face(); + + long ijo = toIJOrientation(); + int i = getI(ijo); + int j = getJ(ijo); + + // Edges 0, 1, 2, 3 are in the down, right, up, left directions. + neighbors[0] = fromFaceIJSame(face, i, j - size, j - size >= 0).parent(level); + neighbors[1] = fromFaceIJSame(face, i + size, j, i + size < MAX_SIZE).parent(level); + neighbors[2] = fromFaceIJSame(face, i, j + size, j + size < MAX_SIZE).parent(level); + neighbors[3] = fromFaceIJSame(face, i - size, j, i - size >= 0).parent(level); + } + + /** + * Return the neighbors of closest vertex to this cell at the given level, by appending them to + * "output". Normally there are four neighbors, but the closest vertex may only have three + * neighbors if it is one of the 8 cube vertices. + * + *

Requires that level < this.level(), so that we can determine which vertex is closest (in + * particular, level == MAX_LEVEL is not allowed). Also requires that this cell is valid. + */ + public void getVertexNeighbors(int level, Collection output) { + // "level" must be strictly less than this cell's level so that we can + // determine which vertex this cell is closest to. + // assert (level < this.level()); + long ijo = toIJOrientation(); + int i = getI(ijo); + int j = getJ(ijo); + + // Determine the i- and j-offsets to the closest neighboring cell in each + // direction. This involves looking at the next bit of "i" and "j" to + // determine which quadrant of this->parent(level) this cell lies in. + int halfsize = getSizeIJ(level + 1); + int size = halfsize << 1; + boolean isame; + boolean jsame; + int ioffset; + int joffset; + if ((i & halfsize) != 0) { + ioffset = size; + isame = (i + size) < MAX_SIZE; + } else { + ioffset = -size; + isame = (i - size) >= 0; + } + if ((j & halfsize) != 0) { + joffset = size; + jsame = (j + size) < MAX_SIZE; + } else { + joffset = -size; + jsame = (j - size) >= 0; + } + + int face = face(); + output.add(parent(level)); + output.add(fromFaceIJSame(face, i + ioffset, j, isame).parent(level)); + output.add(fromFaceIJSame(face, i, j + joffset, jsame).parent(level)); + // If i- and j- edge neighbors are *both* on a different face, then this + // vertex only has three neighbors (it is one of the 8 cube vertices). + if (isame || jsame) { + output.add(fromFaceIJSame(face, i + ioffset, j + joffset, isame && jsame).parent(level)); + } + } + + /** + * Append all neighbors of this cell at the given level to "output". Two cells X and Y are + * neighbors if their boundaries intersect but their interiors do not. In particular, two cells + * that intersect at a single point are neighbors. + * + *

Requires that nbrLevel >= this->level(). Note that for cells adjacent to a face vertex, the + * same neighbor may be appended more than once. Also requires that this cell is valid. + */ + public void getAllNeighbors(int nbrLevel, List output) { + long ijo = toIJOrientation(); + + // Find the coordinates of the lower left-hand leaf cell. We need to + // normalize (i,j) to a known position within the cell because nbrLevel + // may be larger than this cell's level. + int size = getSizeIJ(); + int face = face(); + int i = getI(ijo) & -size; + int j = getJ(ijo) & -size; + + int nbrSize = getSizeIJ(nbrLevel); + // assert (nbrSize <= size); + + // We compute the top-bottom, left-right, and diagonal neighbors in one pass. + // The loop test is at the end of the loop to avoid 32-bit overflow. + for (int k = -nbrSize; ; k += nbrSize) { + boolean sameFace; + if (k < 0) { + sameFace = j + k >= 0; + } else if (k >= size) { + sameFace = j + k < MAX_SIZE; + } else { + sameFace = true; + // Top and bottom neighbors. + output.add(fromFaceIJSame(face, i + k, j - nbrSize, j - size >= 0).parent(nbrLevel)); + output.add(fromFaceIJSame(face, i + k, j + size, j + size < MAX_SIZE).parent(nbrLevel)); + } + // Left, right, and diagonal neighbors. + output.add( + fromFaceIJSame(face, i - nbrSize, j + k, sameFace && i - size >= 0).parent(nbrLevel)); + output.add( + fromFaceIJSame(face, i + size, j + k, sameFace && i + size < MAX_SIZE).parent(nbrLevel)); + if (k >= size) { + break; + } + } + } + + // /////////////////////////////////////////////////////////////////// + // Low-level methods. + + /** Return a leaf cell given its cube face (range 0..5) and i- and j-coordinates (see s2.h). */ + public static S2CellId fromFaceIJ(int face, int i, int j) { + // Optimization notes: + // - Non-overlapping bit fields can be combined with either "+" or "|". + // Generally "+" seems to produce better code, but not always. + + // gcc doesn't have very good code generation for 64-bit operations. + // We optimize this by computing the result as two 32-bit integers + // and combining them at the end. Declaring the result as an array + // rather than local variables helps the compiler to do a better job + // of register allocation as well. Note that the two 32-bits halves + // get shifted one bit to the left when they are combined. + long lsb = 0; + long msb = ((long) face) << (POS_BITS - 33); + + // Alternating faces have opposite Hilbert curve orientations; this + // is necessary in order for all faces to have a right-handed + // coordinate system. + int bits = (face & SWAP_MASK); + + // 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 (int k = 7; k >= 4; --k) { + bits = lookupBits(i, j, k, bits); + msb = updateBits(msb, k, bits); + bits = maskBits(bits); + } + for (int k = 3; k >= 0; --k) { + bits = lookupBits(i, j, k, bits); + lsb = updateBits(lsb, k, bits); + bits = maskBits(bits); + } + + return new S2CellId((((msb << 32) + lsb) << 1) + 1); + } + + private static final int lookupBits(int i, int j, int k, int bits) { + bits += (((i >> (k * LOOKUP_BITS)) & LOOKUP_MASK) << (LOOKUP_BITS + 2)); + bits += (((j >> (k * LOOKUP_BITS)) & LOOKUP_MASK) << 2); + return LOOKUP_POS[bits]; + } + + private static final long updateBits(long sb, int k, int bits) { + return sb | ((((long) bits) >> 2) << ((k & 0x3) * 2 * LOOKUP_BITS)); + } + + private static final int maskBits(int bits) { + return bits & (SWAP_MASK | INVERT_MASK); + } + + /** + * Returns the (i, j) coordinates for the leaf cell corresponding to this cell id, and the + * orientation the i- and j-axes follow at that level. The returned long packs the values into one + * long, such that bits 33-63 contain i, bits 2-32 contain j, and bits 0-1 contain the + * orientation. + * + *

Since cells are represented by the Hilbert curve position at the center of the cell, the + * returned (i, j) for non-leaf cells will be a leaf cell adjacent to the cell center. + * + *

Use {@link #getI(long)}, {@link #getJ(long)}, and {@link #getOrientation(long)} to extract + * integer values for i, j, and orientation, respectively. + */ + long toIJOrientation() { + int face = face(); + int bits = (face & SWAP_MASK); + + // Each iteration maps 8 bits of the Hilbert curve position into + // 4 bits of "i" and "j". The lookup table transforms a key of the + // form "ppppppppoo" to a value of the form "iiiijjjjoo", where the + // letters [ijpo] represents bits of "i", "j", the Hilbert curve + // position, and the Hilbert curve orientation respectively. + // + // On the first iteration we need to be careful to clear out the bits + // representing the cube face. + int i = 0; + int j = 0; + for (int k = 7; k >= 0; --k) { + final int nbits = (k == 7) ? (MAX_LEVEL - 7 * LOOKUP_BITS) : LOOKUP_BITS; + bits += (((int) (id >>> (k * 2 * LOOKUP_BITS + 1)) & ((1 << (2 * nbits)) - 1))) << 2; + bits = LOOKUP_IJ[bits]; + i += (bits >> (LOOKUP_BITS + 2)) << (k * LOOKUP_BITS); + j += (((bits >> 2) & LOOKUP_MASK)) << (k * LOOKUP_BITS); + bits = maskBits(bits); + } + + // The position of a non-leaf cell at level "n" consists of a prefix of + // 2*n bits that identifies the cell, followed by a suffix of + // 2*(MAX_LEVEL-n)+1 bits of the form 10*. If n==MAX_LEVEL, the suffix is + // just "1" and has no effect. Otherwise, it consists of "10", followed + // by (MAX_LEVEL-n-1) repetitions of "00", followed by "0". The "10" has + // no effect, while each occurrence of "00" has the effect of reversing + // the SWAP_MASK bit. + // assert (S2.POS_TO_ORIENTATION[2] == 0); + // assert (S2.POS_TO_ORIENTATION[0] == S2.SWAP_MASK); + if ((lowestOnBit() & 0x1111111111111110L) != 0) { + bits ^= S2.SWAP_MASK; + } + int orientation = bits; + + // Since i and j are non-negative ints, we only need 31 bits to represent + // each value. Thus, bits 33-63 of the {@code ijo} primitive long hold the + // value for i, and bits 2-32 hold the value for j. Bits 0-1 hold the value + // of the 2-bit orientation. + return (((long) i) << I_SHIFT) | (((long) j) << J_SHIFT) | orientation; + } + + /** Returns the "i" coordinate of this S2 cell ID. */ + public int getI() { + return getI(toIJOrientation()); + } + + /** + * Returns the "i" coordinate from bits 33-63 in the given {@code ijo} primitive long returned by + * {@link #toIJOrientation()}. + */ + static int getI(long ijo) { + return (int) (ijo >>> I_SHIFT); + } + + /** Returns the "j" coordinate of this S2 cell ID. */ + public int getJ() { + return getJ(toIJOrientation()); + } + + /** + * Returns the "j" coordinate from bits 2-32 in the given {@code ijo} primitive long returned by + * {@link #toIJOrientation()}. + */ + static int getJ(long ijo) { + return (int) ((ijo >>> J_SHIFT) & J_MASK); + } + + /** Returns the orientation of this S2 cell ID. */ + public int getOrientation() { + return getOrientation(toIJOrientation()); + } + + /** + * Returns the orientation from bits 0-1 in the given {@code ijo} primitive long returned by + * {@link #toIJOrientation()}. + */ + static int getOrientation(long ijo) { + return (int) (ijo & ORIENTATION_MASK); + } + + /** + * Returns the lowest-numbered bit that is on for this cell id, which is equal to {@code 1L << (2 + * * (MAX_LEVEL - level))}. So for example, a.lsb() <= b.lsb() if and only if a.level() >= + * b.level(), but the first test is more efficient. + */ + public long lowestOnBit() { + return lowestOnBit(id); + } + + /** Return the lowest-numbered bit that is on for cells at the given level. */ + public static long lowestOnBitForLevel(int level) { + return 1L << (2 * (MAX_LEVEL - level)); + } + + /** + * Returns the bound in (u,v)-space for the cell at the given level containing the leaf cell with + * the given (i,j)-coordinates. + */ + static R2Rect ijLevelToBoundUv(int i, int j, int level) { + R2Rect bound = new R2Rect(); + int cellSize = getSizeIJ(level); + setAxisRange(i, cellSize, bound.x()); + setAxisRange(j, cellSize, bound.y()); + return bound; + } + + private static void setAxisRange(int ij, int cellSize, R1Interval interval) { + interval.set( + S2Projections.PROJ.ijToUV(ij, cellSize), + S2Projections.PROJ.ijToUV(ij + cellSize, cellSize)); + } + + /** + * Given a face and a point (i,j) where either i or j is outside the valid range [0..MAX_SIZE-1], + * this function first determines which neighboring face "contains" (i,j), and then returns the + * leaf cell on that face which is adjacent to the given face and whose distance from (i,j) is + * minimal. + */ + private static S2CellId fromFaceIJWrap(int face, int i, int j) { + // 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. + i = Math.max(-1, Math.min(MAX_SIZE, i)); + j = Math.max(-1, Math.min(MAX_SIZE, j)); + + // 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 + // S2::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. + final double kLimit = 1.0 + S2.DBL_EPSILON; + double u = Math.max(-kLimit, Math.min(kLimit, IJ_TO_ST * ((i << 1) + 1 - MAX_SIZE))); + double v = Math.max(-kLimit, Math.min(kLimit, IJ_TO_ST * ((j << 1) + 1 - MAX_SIZE))); + + // Find the leaf cell coordinates on the adjacent face, and convert + // them to a cell id at the appropriate level. + S2Projections.XyzTransform xyzTransform = S2Projections.faceToXyzTransform(face); + double x = xyzTransform.uvToX(u, v); + double y = xyzTransform.uvToY(u, v); + double z = xyzTransform.uvToZ(u, v); + face = S2Projections.xyzToFace(x, y, z); + S2Projections.UvTransform uvTransform = S2Projections.faceToUvTransform(face); + return fromFaceIJ( + face, + S2Projections.stToIj(0.5 * (1 + uvTransform.xyzToU(x, y, z))), + S2Projections.stToIj(0.5 * (1 + uvTransform.xyzToV(x, y, z)))); + } + + /** + * Public helper function that calls FromFaceIJ if sameFace is true, or FromFaceIJWrap if sameFace + * is false. + */ + public static S2CellId fromFaceIJSame(int face, int i, int j, boolean sameFace) { + if (sameFace) { + return S2CellId.fromFaceIJ(face, i, j); + } else { + return S2CellId.fromFaceIJWrap(face, i, j); + } + } + + @Override + public boolean equals(Object that) { + if (!(that instanceof S2CellId)) { + return false; + } + S2CellId x = (S2CellId) that; + return id() == x.id(); + } + + /** Returns true if x1 < x2, when both values are treated as unsigned. */ + public static boolean unsignedLongLessThan(long x1, long x2) { + return (x1 + Long.MIN_VALUE) < (x2 + Long.MIN_VALUE); + } + + /** Returns true if x1 <= x2, when both values are treated as unsigned. */ + public static boolean unsignedLongLessOrEquals(long x1, long x2) { + return (x1 + Long.MIN_VALUE) <= (x2 + Long.MIN_VALUE); + } + + /** Returns true if x1 > x2, when both values are treated as unsigned. */ + public static boolean unsignedLongGreaterThan(long x1, long x2) { + return (x1 + Long.MIN_VALUE) > (x2 + Long.MIN_VALUE); + } + + /** Returns true if x1 >= x2, when both values are treated as unsigned. */ + public static boolean unsignedLongGreaterOrEquals(long x1, long x2) { + return (x1 + Long.MIN_VALUE) >= (x2 + Long.MIN_VALUE); + } + + public boolean lessThan(S2CellId x) { + return unsignedLongLessThan(id, x.id); + } + + public boolean greaterThan(S2CellId x) { + return unsignedLongGreaterThan(id, x.id); + } + + public boolean lessOrEquals(S2CellId x) { + return unsignedLongLessOrEquals(id, x.id); + } + + public boolean greaterOrEquals(S2CellId x) { + return unsignedLongGreaterOrEquals(id, x.id); + } + + @Override + public int hashCode() { + return (int) ((id >>> 32) + id); + } + + @Override + public String toString() { + return "(face=" + face() + ", pos=" + Long.toHexString(pos()) + ", level=" + level() + ")"; + } + + private static void initLookupCell( + int level, int i, int j, int origOrientation, int pos, int orientation) { + if (level == LOOKUP_BITS) { + int ij = (i << LOOKUP_BITS) + j; + LOOKUP_POS[(ij << 2) + origOrientation] = (pos << 2) + orientation; + LOOKUP_IJ[(pos << 2) + origOrientation] = (ij << 2) + orientation; + } else { + level++; + i <<= 1; + j <<= 1; + pos <<= 2; + // Initialize each sub-cell recursively. + for (int subPos = 0; subPos < 4; subPos++) { + int ij = S2.posToIJ(orientation, subPos); + int orientationMask = S2.posToOrientation(subPos); + initLookupCell( + level, + i + (ij >>> 1), + j + (ij & 1), + origOrientation, + pos + subPos, + orientation ^ orientationMask); + } + } + } + + @Override + public int compareTo(S2CellId that) { + return unsignedLongLessThan(this.id, that.id) + ? -1 + : unsignedLongGreaterThan(this.id, that.id) ? 1 : 0; + } + + private static long fromFaceAsLong(int face) { + return (((long) face) << POS_BITS) + lowestOnBitForLevel(0); + } + + private static long childBeginAsLong(long id) { + long oldLsb = lowestOnBit(id); + return id - oldLsb + (oldLsb >>> 2); + } + + private static long childBeginAsLong(long id, int level) { + return id - lowestOnBit(id) + lowestOnBitForLevel(level); + } + + private static long childEndAsLong(long id) { + long oldLsb = lowestOnBit(id); + return id + oldLsb + (oldLsb >>> 2); + } + + private static long childEndAsLong(long id, int level) { + return id + lowestOnBit(id) + lowestOnBitForLevel(level); + } + + private static long rangeMinAsLong(long id) { + return id - (lowestOnBit(id) - 1); + } + + private static long rangeMaxAsLong(long id) { + return id + (lowestOnBit(id) - 1); + } + + private static long lowestOnBit(long id) { + return Long.lowestOneBit(id); + } + + private static long parentAsLong(long id) { + long newLsb = lowestOnBit(id) << 2; + return (id & -newLsb) | newLsb; + } + + private static long parentAsLong(long id, int level) { + long newLsb = lowestOnBitForLevel(level); + return (id & -newLsb) | newLsb; + } + + private static long fromFacePosLevelAsLong(int face, long pos, int level) { + return parentAsLong((((long) face) << POS_BITS) + (pos | 1), level); + } + + /** + * Returns a cell id decoded from a simple debug format. This function is reasonably efficient, + * but is only intended for use in tests; no promises are made about the durability of the + * encoding over time. + */ + static S2CellId fromDebugString(String str) { + int level = str.length() - 2; + if (level < 0 || level > S2CellId.MAX_LEVEL) { + return S2CellId.NONE; + } + int face = str.charAt(0) - '0'; + if (face < 0 || face > 5 || str.charAt(1) != '/') { + return S2CellId.NONE; + } + S2CellId id = S2CellId.fromFace(face); + for (int i = 2; i < str.length(); ++i) { + int childPos = str.charAt(i) - '0'; + if (childPos < 0 || childPos > 3) { + return S2CellId.NONE; + } + id = id.child(childPos); + } + return id; + } +} diff --git a/core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/S2CellIdVector.java b/core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/S2CellIdVector.java new file mode 100644 index 0000000000..54be63d863 --- /dev/null +++ b/core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/S2CellIdVector.java @@ -0,0 +1,35 @@ +/* + * Copyright 2019 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.mogo.eagle.core.utilcode.geometry; + +import com.google.common.annotations.GwtCompatible; +import java.util.AbstractList; + +/** + * A list of {@link S2CellId}s, and specialized methods for directly operating on the encoded form. + */ +@GwtCompatible +abstract class S2CellIdVector extends AbstractList { + + /** + * Returns the index of the first element {@code x} such that {@code (x >= target)}, or {@link + * #size()} if no such element exists. + * + *

The list must be sorted into ascending order prior to making this call. If it is not sorted, + * the results are undefined. + */ + abstract int lowerBound(S2CellId target); +} diff --git a/core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/S2CellIdVectorCoder.java b/core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/S2CellIdVectorCoder.java new file mode 100644 index 0000000000..6a49a9e61b --- /dev/null +++ b/core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/S2CellIdVectorCoder.java @@ -0,0 +1,218 @@ +/* + * Copyright 2018 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.mogo.eagle.core.utilcode.geometry; + +import com.google.common.annotations.GwtCompatible; +import com.mogo.eagle.core.utilcode.geometry.PrimitiveArrays.Bytes; +import com.mogo.eagle.core.utilcode.geometry.PrimitiveArrays.Cursor; +import com.mogo.eagle.core.utilcode.geometry.PrimitiveArrays.Longs; +import com.google.common.primitives.UnsignedLongs; +import java.io.IOException; +import java.io.OutputStream; +import java.util.List; + +/** An encoder/decoder of {@link List}s. */ +@GwtCompatible +class S2CellIdVectorCoder implements S2Coder> { + + /** An instance of an {@code S2CellIdVectorCoder}. */ + static final S2CellIdVectorCoder INSTANCE = new S2CellIdVectorCoder(); + + @Override + public void encode(List values, OutputStream output) throws IOException { + // The encoding format is as follows: + // + // byte 0, bits 0-2: baseBytes + // byte 0, bits 3-7: shiftCode + // byte 1: extended shiftCode (only written for odd shift >= 5) + // 0-7 bytes: base + // values.size() encoded uint64s of deltas (encoded by UintVectorCoder.UINT64.encode) + // + // base consists of 0-7 bytes, and is always shifted so its bytes are the most-significant + // bytes of a uint64 (little-endian). + // + // shift is in the range 0-56. shift is odd only if all S2CellIds are at the same + // level, in which case the bit at position (shift - 1) in base is automatically set to 1. + // + // base (3 bits) and shift (6 bits) are encoded in either one or two bytes as follows: + // - if (shift <= 4 or shift is even), then 1 byte + // - else 2 bytes + // + // (shift == 1) means that all S2CellIds are leaf cells, and (shift == 2) means that + // all S2CellIds are at level 29. + + long valuesOr = 0L; + long valuesAnd = ~0L; + long valuesMin = ~0L; + long valuesMax = 0L; + + for (S2CellId cellId : values) { + valuesOr |= cellId.id(); + valuesAnd &= cellId.id(); + valuesMin = UnsignedLongs.min(valuesMin, cellId.id()); + valuesMax = UnsignedLongs.max(valuesMax, cellId.id()); + } + + long base = 0L; + // The number of bytes required to encode base. + int baseBytes = 0; + int shift = 0; + // The bit position of the most-significant bit of the largest delta. + int maxDeltaMsb = 0; + + if (UnsignedLongs.compare(valuesOr, 0) > 0) { + // We only allow even shift, unless all values have the same low bit (in which case shift is + // odd and the preceding bit is implicitly on). There is no point in allowing shifts > 56 + // because deltas are encoded in at least 1 byte each. + shift = Math.min(56, Long.numberOfTrailingZeros(valuesOr) & ~1); + if ((valuesAnd & (1L << shift)) != 0) { + // All S2CellIds are at the same level. + shift++; + } + + // base consists of the baseBytes most-significant bytes of the minimum S2CellId. We consider + // all possible values of baseBytes (0-7) and choose the one that minimizes the total encoding + // size. + // The best encoding size so far. + int minBytes = -1; + for (int tmpBaseBytes = 0; tmpBaseBytes < 8; tmpBaseBytes++) { + // The base value being tested (first tmpBaseBytes of valuesMin). + long tmpBase = valuesMin & ~(~0L >>> (8 * tmpBaseBytes)); + // The most-significant bit position of the largest delta (or zero if there are no deltas + // [i.e., if values.size == 0]). + int tmpMaxDeltaMsb = + Math.max(0, 63 - Long.numberOfLeadingZeros((valuesMax - tmpBase) >>> shift)); + // The total size of the variable portion of the encoding. + int candidateBytes = tmpBaseBytes + values.size() * ((tmpMaxDeltaMsb >> 3) + 1); + + if (UnsignedLongs.compare(candidateBytes, minBytes) < 0) { + base = tmpBase; + baseBytes = tmpBaseBytes; + maxDeltaMsb = tmpMaxDeltaMsb; + minBytes = candidateBytes; + } + } + // It takes one extra byte to encode odd shifts (i.e., the case where all S2CellIds are at the + // same level), so we check whether we can get the same encoding size per delta using an even + // shift. + if (((shift & 1) != 0) && (maxDeltaMsb & 7) != 7) { + shift--; + } + } + assert (shift <= 56); + + // shift and baseBytes are encoded in 1 or 2 bytes. + // shiftCode is 5 bits, values: + // - <= 28 represent even shifts in the range 0-56. + // - 29, 30 represent odd shifts 1 and 3. + // - 31 indicates that the shift is odd and encoded in the next byte. + int shiftCode = shift >> 1; + if ((shift & 1) != 0) { + shiftCode = Math.min(31, shiftCode + 29); + } + output.write((byte) ((shiftCode << 3) | baseBytes)); + if (shiftCode == 31) { + // shift is always odd, so 3 bits unused. + output.write((byte) (shift >> 1)); + } + + // Encode the baseBytes most-significant bytes of base. + long baseCode = base >>> (64 - 8 * Math.max(1, baseBytes)); + EncodedInts.encodeUintWithLength(output, baseCode, baseBytes); + + // Encode the vector of deltas. + long tmpBase = base; + long tmpShift = shift; + UintVectorCoder.UINT64.encode( + new Longs() { + @Override + public long get(int position) { + return (values.get(position).id() - tmpBase) >>> tmpShift; + } + + @Override + public int length() { + return values.size(); + } + }, + output); + } + + @Override + public S2CellIdVector decode(Bytes data, Cursor cursor) { + // See encode for documentation on the encoding format. + + // Invert the encoding of (shiftCode, baseBytes). + int shiftCodeBaseBytes = data.get(cursor.position++) & 0xff; + int shiftCode = shiftCodeBaseBytes >> 3; + if (shiftCode == 31) { + shiftCode = 29 + (data.get(cursor.position++) & 0xff); + } + + // Decode the baseBytes most-significant bytes of base. + int baseBytes = shiftCodeBaseBytes & 7; + long base = data.readUintWithLength(cursor, baseBytes); + base <<= 64 - 8 * Math.max(1, baseBytes); + + // Invert the encoding of shiftCode. + long shift; + if (shiftCode >= 29) { + shift = 2L * (shiftCode - 29) + 1; + base |= 1L << (shift - 1); + } else { + shift = 2L * shiftCode; + } + + long tmpBase = base; + Longs deltas = UintVectorCoder.UINT64.decode(data, cursor); + return new S2CellIdVector() { + @Override + public int size() { + return deltas.length(); + } + + @Override + public S2CellId get(int index) { + return new S2CellId((deltas.get(index) << shift) + tmpBase); + } + + @Override + int lowerBound(S2CellId target) { + if (UnsignedLongs.compare(target.id(), tmpBase) <= 0) { + return 0; + } + if (target.greaterOrEquals(S2CellId.end(S2CellId.MAX_LEVEL))) { + return size(); + } + int low = 0; + int high = deltas.length(); + long needle = (target.id() - tmpBase + (1L << shift) - 1) >>> shift; + + // Binary search for the index of the first element in deltas that is >= needle. + while (low < high) { + int mid = (low + high) >> 1; + long value = deltas.get(mid); + if (UnsignedLongs.compare(value, needle) < 0) { + low = mid + 1; + } else { + high = mid; + } + } + return low; + } + }; + } +} diff --git a/core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/S2CellIndex.java b/core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/S2CellIndex.java new file mode 100644 index 0000000000..64c9b2aca5 --- /dev/null +++ b/core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/S2CellIndex.java @@ -0,0 +1,662 @@ +/* + * Copyright 2019 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.mogo.eagle.core.utilcode.geometry; + +import com.google.common.annotations.GwtCompatible; +import com.google.common.base.Preconditions; +import java.util.AbstractList; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Comparator; +import java.util.List; + +/** + * A collection of (cellId, label) pairs. The S2CellIds may be overlapping or contain duplicate + * values. For example, an S2CellIndex could store a collection of S2CellUnions, where each + * S2CellUnion has its own label. Labels are 32-bit non-negative integers, and are typically used to + * map the results of queries back to client data structures. + * + *

To build an S2CellIndex, call add() for each (cellId, label) pair, and then call the build() + * method. There is also a convenience method to associate a label with each cell in a union. + * + *

Note that the index is not dynamic; the contents of the index cannot be changed once it has + * been built. However, the same memory space can be reused by {@link #clear clearing} the index. + * + *

There are several options for retrieving data from the index. The simplest is to use a + * built-in method such as getIntersectingLabels, which returns the labels of all cells that + * intersect a given target S2CellUnion. Alternatively, you can access the index contents directly. + * + *

Internally, the index consists of a set of non-overlapping leaf cell ranges that subdivide the + * sphere and such that each range intersects a particular set of (cellId, label) pairs. Data is + * accessed using the following iterator types: + * + *

    + *
  • {@link RangeIterator}: used to seek and iterate over the non-overlapping leaf cell ranges. + *
  • {@link NonEmptyRangeIterator}: like RangeIterator, but skips ranges whose contents are + * empty. + *
  • {@link ContentsIterator}: iterates over the (cellId, label) pairs that intersect a given + * range. + *
  • {@link CellIterator}: iterates over the entire set of (cellId, label) pairs. + *
+ * + *

Note that these are low-level, efficient types intended mainly for implementing new query + * classes. Most clients should use either the built-in methods such as {@link + * #visitIntersectingCells} and {@link #getIntersectingLabels}. + */ +@GwtCompatible +public class S2CellIndex { + /** + * A tree of (cellId, label) pairs such that if X is an ancestor of Y, then X.cellId contains + * Y.cellId. The contents of a given range of leaf cells can be represented by pointing to a node + * of this tree. + */ + private final List cellNodes = new ArrayList<>(); + + /** + * The last element of nodes is a sentinel value, which is necessary in order to represent the + * range covered by the previous element. + */ + private final ArrayList rangeNodes = new ArrayList<>(); + + /** Returns the number of (cellId, label) pairs in the index. */ + public int numCells() { + return cellNodes.size(); + } + + /** + * Adds the given (cellId, label) pair to the index. Note that the index is not valid until + * {@link #build} is called. + * + *

The S2CellIds in the index may overlap (including duplicate values). Duplicate (cellId, + * label) pairs are also allowed, although query tools often remove duplicates. + * + *

Results are undefined unless all cells are {@link S2CellId#isValid valid}. + */ + public void add(S2CellId cellId, int label) { + assert cellId.isValid(); + assert label >= 0; + cellNodes.add(new CellNode(cellId, label, -1)); + } + + /** Convenience function that adds a collection of cells with the same label. */ + public void add(Iterable cellIds, int label) { + for (S2CellId cellId : cellIds) { + add(cellId, label); + } + } + + /** + * Builds the index. This method may only be called once. No iterators may be used until the index + * is built. + */ + public void build() { + // Create two deltas for each (cellId, label) pair: one to add the pair to the stack (at the + // start of its leaf cell range), and one to remove it from the stack (at the end of its leaf + // cell range). + Delta[] deltas = new Delta[2 * cellNodes.size() + 2]; + int size = 0; + for (CellNode node : cellNodes) { + deltas[size++] = new Delta(node.cellId.rangeMin(), node.cellId, node.label); + deltas[size++] = new Delta(node.cellId.rangeMax().next(), S2CellId.sentinel(), -1); + } + + // We also create two special deltas to ensure that a RangeNode is emitted at the beginning and + // end of the S2CellId range. + deltas[size++] = new Delta(S2CellId.begin(S2CellId.MAX_LEVEL), S2CellId.none(), -1); + deltas[size++] = new Delta(S2CellId.end(S2CellId.MAX_LEVEL), S2CellId.none(), -1); + Arrays.sort(deltas, Delta.BY_START_CELL_NEG_LABEL); + + // Now walk through the deltas to build the leaf cell ranges and cell tree (which is essentially + // a permanent form of the "stack" described above). + cellNodes.clear(); + rangeNodes.ensureCapacity(deltas.length); + int contents = -1; + for (int i = 0; i < deltas.length; ) { + // Process all the deltas associated with the current startId. + S2CellId startId = deltas[i].startId; + for (; i < deltas.length && deltas[i].startId.equals(startId); i++) { + if (deltas[i].label >= 0) { + cellNodes.add(new CellNode(deltas[i].cellId, deltas[i].label, contents)); + contents = cellNodes.size() - 1; + } else if (deltas[i].cellId.equals(S2CellId.sentinel())) { + contents = cellNodes.get(contents).parent; + } + } + rangeNodes.add(new RangeNode(startId, contents)); + } + } + + /** Returns an iterator over the cells of this index. */ + public CellIterator cells() { + Preconditions.checkState(!rangeNodes.isEmpty(), "Call build() first."); + return new CellIterator(); + } + + /** Returns an iterator over the ranges of this index. */ + public RangeIterator ranges() { + Preconditions.checkState(!rangeNodes.isEmpty(), "Call build() first."); + return new RangeIterator(); + } + + /** Returns an iterator over the non-empty ranges of this index. */ + public NonEmptyRangeIterator nonEmptyRanges() { + return new NonEmptyRangeIterator(); + } + + /** Returns an iterator over the contents of this index. */ + public ContentsIterator contents() { + Preconditions.checkState(!rangeNodes.isEmpty(), "Call build() first."); + return new ContentsIterator(); + } + + /** + * To build the cell tree and leaf cell ranges, we maintain a stack of (cellId, label) pairs that + * contain the current leaf cell. This class represents an instruction to push or pop a (cellId, + * label) pair. + * + *

If label >= 0, the (cellId, label) pair is pushed on the stack. If cellId == + * S2CellId.SENTINEL, a pair is popped from the stack. Otherwise the stack is unchanged but a + * RangeNode is still emitted. + */ + private static final class Delta { + /** + * Deltas are sorted first by startId, then in reverse order by cellId, and then by label. This + * is necessary to ensure that (1) larger cells are pushed on the stack before smaller cells, + * and (2) cells are popped off the stack before any new cells are added. + */ + public static final Comparator BY_START_CELL_NEG_LABEL = (a, b) -> { + int result = a.startId.compareTo(b.startId); + if (result != 0) { + return result; + } + result = -a.cellId.compareTo(b.cellId); + if (result != 0) { + return result; + } + return Integer.compare(a.label, b.label); + }; + + private final S2CellId startId; + private final S2CellId cellId; + private final int label; + + private Delta(S2CellId startId, S2CellId cellId, int label) { + this.startId = startId; + this.cellId = cellId; + this.label = label; + } + } + + /** Clears the index so that it can be re-used. */ + public void clear() { + cellNodes.clear(); + rangeNodes.clear(); + } + + /** + * Visits all (cellId, label) pairs in the given index that intersect the given S2CellUnion + * "target" and returns true, or terminates early and returns false if {@link CellVisitor#visit} + * ever returns false. + * + *

Each (cellId, label) pair in the index is visited at most once. If the index contains + * duplicates, then each copy is visited. + */ + public boolean visitIntersectingCells(S2CellUnion target, CellVisitor visitor) { + if (target.size() == 0) { + return true; + } + + ContentsIterator contents = contents(); + RangeIterator range = ranges(); + for (int i = 0; i < target.size(); ) { + S2CellId id = target.cellId(i); + + // seek the range to this target cell when necessary. + if (range.limitId().lessOrEquals(id.rangeMin())) { + range.seek(id.rangeMin()); + } + + // Visit contents of this range that intersect this cell. + for (; range.startId().lessOrEquals(id.rangeMax()); range.next()) { + for (contents.startUnion(range); !contents.done(); contents.next()) { + if (!visitor.visit(contents.cellId(), contents.label())) { + return false; + } + } + } + + // Check whether the next target cell is also contained by the leaf cell range that we just + // processed. If so, we can skip over all such cells using binary search. This speeds up + // benchmarks by 2-10x when the average number of intersecting cells is small (< 1). + i++; + if (i != target.size()) { + id = target.cellId(i); + if (id.rangeMax().lessThan(range.startId())) { + // Skip to the first target cell that extends past the previous range. + i = S2ShapeUtil.lowerBound(i + 1, target.size(), + j -> range.startId().greaterThan(target.cellId(j))); + if (target.cellId(i - 1).rangeMax().greaterOrEquals(range.startId())) { + i--; + } + } + } + } + + return true; + } + + /** Returns the distinct sorted labels that intersect the given target. */ + public Labels getIntersectingLabels(S2CellUnion target) { + Labels result = new Labels(); + getIntersectingLabels(target, result); + result.normalize(); + return result; + } + + /** Appends labels intersecting 'target', in unspecified order, with possible duplicates. */ + public void getIntersectingLabels(S2CellUnion target, Labels results) { + visitIntersectingCells(target, (cellId, label) -> results.add(label)); + } + + /** A function that is called with each (cellId, label) pair to be visited. */ + public interface CellVisitor { + /** Provides a (cellId, label) pair to this visitor, which may return true to keep searching. */ + boolean visit(S2CellId cellId, int label); + } + + /** + * A set of labels that can be grown by {@link #getIntersectingLabels(S2CellUnion, Labels)} and + * shrunk via {@link #clear} or {@link #normalize}. May contain duplicates or be unsorted unless + * {@link #normalize} is called. + */ + public static class Labels extends AbstractList { + private int[] labels = new int[8]; + private int size; + + @Override + public void clear() { + size = 0; + } + + private boolean add(int label) { + if (labels.length == size) { + labels = Arrays.copyOf(labels, size * 2); + } + labels[size++] = label; + return true; + } + + @Override + public int size() { + return size; + } + + @Override + public Integer get(int index) { + return getInt(index); + } + + /** As {@link #get(int)} but without the overhead of boxing. */ + public int getInt(int index) { + return labels[index]; + } + + /** Sorts the labels and removes duplicates. */ + public void normalize() { + if (size == 0) { + return; + } + Arrays.sort(labels, 0, size); + int lastIndex = 0; + for (int i = 1; i < size; i++) { + if (labels[lastIndex] != labels[i]) { + labels[++lastIndex] = labels[i]; + } + } + size = lastIndex + 1; + } + } + + /** + * Represents a node in the (cellId, label) tree. Cells are organized in a tree such that the + * ancestors of a given node contain that node. + */ + private static final class CellNode { + private S2CellId cellId; + private int label; + private int parent; + + private CellNode(S2CellId cellId, int label, int parent) { + this.cellId = cellId; + this.label = label; + this.parent = parent; + } + + private void setFrom(CellNode node) { + this.cellId = node.cellId; + this.label = node.label; + this.parent = node.parent; + } + } + + /** An iterator over all (cellId, label) pairs in an unspecified order. */ + public final class CellIterator { + /** Offset into {@link S2CellIndex#cellNodes}. */ + private int offset; + /** Current node pointed to by 'offset', or null if {@link #done}. */ + private CellNode cell; + + // Initializes a CellIterator for the given S2CellIndex, positioned at the first cell (if any). + private CellIterator() { + Preconditions.checkState(!rangeNodes.isEmpty(), "Call build() first."); + seek(0); + } + + /** Returns the S2CellId of the current (cellId, label) pair. */ + public S2CellId cellId() { + assert !done(); + return cell.cellId; + } + + /** Returns the label of the current (cellId, label) pair. */ + public int label() { + assert !done(); + return cell.label; + } + + /** Returns true if all (cellId, label) pairs have been visited. */ + public boolean done() { + return offset == cellNodes.size(); + } + + /** Advances this iterator to the next (cellId, label) pair. */ + public void next() { + assert !done(); + seek(offset + 1); + } + + /** Sets the offset and sets 'cell' accordingly. */ + private void seek(int offset) { + this.offset = offset; + this.cell = done() ? null : cellNodes.get(offset); + } + } + + /** A range of leaf S2CellIds, from the level 30 leaf cell of this range to the next range. */ + private static class RangeNode { + /** First leaf cell contained by this range. */ + private final S2CellId startId; + /** Index in {@link S2CellIndex#cellNodes} for the cells that overlap this range. */ + private final int contents; + + private RangeNode(S2CellId startId, int contents) { + this.startId = startId; + this.contents = contents; + } + } + + /** + * An iterator that seeks and iterates over a set of non-overlapping leaf cell ranges that cover + * the entire sphere. The indexed (s2cell_id, label) pairs that intersect the current leaf cell + * range can be visited using ContentsIterator (see below). + */ + public class RangeIterator { + /** Offset into {@link S2CellIndex#rangeNodes}. */ + private int offset; + /** Current node pointed to by 'offset'. */ + private RangeNode node = rangeNodes.get(offset); + + /** + * Returns the start of the current range of leaf S2CellIds. When {@link #done}, this returns + * the {@link S2CellId#end end} of the {@link S2CellId#MAX_LEVEL max level} of cells, so that + * most loops may test this method instead of done(). + */ + public S2CellId startId() { + return node.startId; + } + + /** The (non-inclusive) end of the current range of leaf S2CellIds. */ + public S2CellId limitId() { + assert (!done()); + return rangeNodes.get(offset + 1).startId; + } + + /** Returns true if the iterator is positioned beyond the last valid range. */ + public boolean done() { + // Note that the last element of rangeNodes is a sentinel value. + return offset >= rangeNodes.size() - 1; + } + + /** Positions this iterator at the first range of leaf cells (if any). */ + public void begin() { + seekAndLoad(0); + } + + /** Positions the iterator so that done() is true. */ + public void finish() { + // Note that the last element of rangeNodes is a sentinel value. + seekAndLoad(rangeNodes.size() - 1); + } + + /** Advances the iterator to the next range of leaf cells. */ + public void next() { + assert (!done()); + seekAndLoad(offset + 1); + } + + /** + * Returns false if the iterator was already positioned at the beginning, otherwise positions + * the iterator at the previous entry and returns true. + */ + public boolean prev() { + if (offset == 0) { + return false; + } + seekAndLoad(offset - 1); + return true; + } + + /** + * Positions the iterator at the range containing "target". Such a range exists as long as the + * target is a valid leaf cell. + * + * @param target a valid leaf (level 30) cell to seek to + */ + public void seek(S2CellId target) { + assert target.isLeaf(); + seekAndLoad(S2ShapeUtil.upperBound(0, rangeNodes.size(), + i -> target.lessThan(rangeNodes.get(i).startId)) - 1); + } + + /** Returns true if no (s2cell_id, label) pairs intersect this range, or if {@link #done}. */ + public boolean isEmpty() { + return node.contents == ContentsIterator.DONE; + } + + /** + * Advances this iterator 'n' times and returns true, or if doing so would advance this iterator + * past the end, leaves the iterator unmodified and returns false. + */ + public boolean advance(int n) { + // Note that the last element of rangeNodes is a sentinel value. + if (n >= rangeNodes.size() - 1 - offset) { + return false; + } + seekAndLoad(offset + n); + return true; + } + + private void seekAndLoad(int offset) { + this.offset = offset; + this.node = rangeNodes.get(offset); + } + } + + /** As {@link RangeIterator} but only visits range nodes that overlap (cellId, label) pairs. */ + public class NonEmptyRangeIterator extends RangeIterator { + @Override + public void begin() { + super.begin(); + while (isEmpty() && !done()) { + super.next(); + } + } + + @Override + public void next() { + do { + super.next(); + } while (isEmpty() && !done()); + } + + @Override + public boolean prev() { + while (super.prev()) { + if (!isEmpty()) { + return true; + } + } + // Return the iterator to its original position. + if (isEmpty() && !done()) { + next(); + } + return false; + } + + // Positions the iterator at the range that contains or follows "target", or at the end if no + // such range exists. (Note that start_id() may still be called in the latter case.) + @Override + public void seek(S2CellId target) { + super.seek(target); + while (isEmpty() && !done()) { + super.next(); + } + } + } + + /** + * An iterator that visits the (cellId, label) pairs that cover a set of leaf cell ranges (see + * RangeIterator). To use it, construct an instance or {@link #clear} an existing instance, and + * {@link #startUnion} to visit the contents of each desired leaf cell range. + * + *

Note that when multiple leaf cell ranges are visited, this class only guarantees that each + * result will be reported at least once, i.e. duplicate values may be suppressed. If you want + * duplicate values to be reported again, be sure to call {@link #clear} first. + * + *

In particular, the implementation guarantees that when multiple leaf cell ranges are visited + * in monotonically increasing order, then each (cellId, label) pair is reported exactly once. + */ + public class ContentsIterator { + /** A special label indicating that {@link #done} is true. */ + private static final int DONE = -1; + + /** + * The value of it.startId() from the previous call to startUnion(). This is used to check + * whether these values are monotonically increasing. + */ + private S2CellId prevStartId; + + /** + * The maximum index within {@link #cellNodes} visited during the previous call to startUnion(). + * This is used to eliminate duplicate values when startUnion() is called multiple times. + */ + private int nodeCutoff; + + /** + * The maximum index within {@link #cellNodes} visited during the current call to startUnion(). + * This is used to update nodeCutoff. + */ + private int nextNodeCutoff; + + /** A copy of the current node in the cell tree. */ + private final CellNode node = new CellNode(null, DONE, -1); + + /** Creates a new iterator. Call {@link #startUnion} next. */ + private ContentsIterator() { + clear(); + } + + /** Clears all state with respect to which range(s) have been visited. */ + public void clear() { + prevStartId = S2CellId.none(); + nodeCutoff = -1; + nextNodeCutoff = -1; + setDone(); + } + + /** + * Positions the ContentsIterator at the first (cellId, label) pair that covers the given leaf + * cell range. Note that when multiple leaf cell ranges are visited using the same + * ContentsIterator, duplicate values may be suppressed. If you don't want this behavior, call + * clear() first. + */ + public void startUnion(RangeIterator range) { + if (range.startId().lessThan(prevStartId)) { + // Can't automatically eliminate duplicates. + nodeCutoff = -1; + } + prevStartId = range.startId(); + int contents = range.node.contents; + if (contents <= nodeCutoff) { + setDone(); + } else { + node.setFrom(cellNodes.get(contents)); + } + + // When visiting ancestors, we can stop as soon as the node index is smaller than any + // previously visited node index. Because indexes are assigned using a preorder traversal, + // such nodes are guaranteed to have already been reported. + nextNodeCutoff = contents; + } + + /** Returns the S2CellId of the current (cellId, label) pair. */ + public S2CellId cellId() { + assert !done(); + return node.cellId; + } + + /** Returns the label of the current (cellId, label) pair. */ + public int label() { + assert !done(); + return node.label; + } + + /** Returns true if all (cellId, label) pairs have been visited. */ + public boolean done() { + return node.label == DONE; + } + + /** + * Advances the iterator to the next (cellId, label) pair covered by the current leaf cell + * range. + */ + public void next() { + assert !done(); + if (node.parent <= nodeCutoff) { + // We have already processed this node and its ancestors. + nodeCutoff = nextNodeCutoff; + setDone(); + } else { + node.setFrom(cellNodes.get(node.parent)); + } + } + + /** Sets the current node label to DONE to indicate that iteration has finished. */ + private void setDone() { + node.label = DONE; + } + } +} diff --git a/core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/S2CellUnion.java b/core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/S2CellUnion.java new file mode 100644 index 0000000000..c33a79fd0d --- /dev/null +++ b/core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/S2CellUnion.java @@ -0,0 +1,837 @@ +/* + * Copyright 2005 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.mogo.eagle.core.utilcode.geometry; + +import static com.mogo.eagle.core.utilcode.geometry.S2Projections.PROJ; + +import com.google.common.annotations.GwtCompatible; +import com.google.common.collect.Lists; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; + +/** + * An S2CellUnion is a region consisting of cells of various sizes. Typically a cell union is used + * to approximate some other shape. There is a tradeoff between the accuracy of the approximation + * and how many cells are used. Unlike polygons, cells have a fixed hierarchical structure. This + * makes them more suitable for optimizations based on preprocessing. + * + *

An S2CellUnion is represented as a vector of sorted, non-overlapping S2CellIds. By default the + * vector is also "normalized", meaning that groups of 4 child cells have been replaced by their + * parent cell whenever possible. S2CellUnions are not required to be normalized, but certain + * operations will return different results if they are not, e.g. {@link #contains(S2CellUnion)}. + * + */ +@GwtCompatible(serializable = true) +public strictfp class S2CellUnion implements S2Region, Iterable, Serializable { + private static final long serialVersionUID = 1L; + + private static final byte LOSSLESS_ENCODING_VERSION = 1; + + /** The CellIds that form the Union */ + private ArrayList cellIds = new ArrayList(); + + public S2CellUnion() {} + + /** + * Populates a cell union with the given S2CellIds, and then calls normalize(). This directly uses + * the input list, without copying it. + */ + public void initFromCellIds(ArrayList cellIds) { + initRawCellIds(cellIds); + normalize(); + } + + /** Populates a cell union with the given 64-bit cell ids, and then calls normalize(). */ + public void initFromIds(List cellIds) { + initRawIds(cellIds); + normalize(); + } + + /** + * Populates a cell union with the given S2CellIds. The input list is copied, and then cleared. + */ + public void initSwap(List cellIds) { + initRawSwap(cellIds); + normalize(); + } + + /** + * Populates a cell union with the given S2CellIds. This does not call normalize, see {@link + * #initRawSwap} for details. This directly uses the input list, without copying it. + */ + public void initRawCellIds(ArrayList cellIds) { + this.cellIds = cellIds; + } + + /** + * Populates a cell union with the given 64 bit cell ids. This does not call normalize, see {@link + * #initRawSwap} for details. The input list is copied. + */ + // TODO(eengle): Make a constructed S2CellUnion immutable, and port other init methods from C++. + public void initRawIds(List cellIds) { + int size = cellIds.size(); + this.cellIds = new ArrayList(size); + for (Long id : cellIds) { + this.cellIds.add(new S2CellId(id)); + } + } + + /** + * Like the initFrom*() constructors, but does not call normalize(). The cell union *must* be + * normalized before doing any calculations with it, so it is the caller's * responsibility to + * make sure that the input is normalized. This method is useful when converting cell unions to + * another representation and back. + * + *

The input list is copied, and then cleared. + */ + public void initRawSwap(List cellIds) { + this.cellIds = new ArrayList(cellIds); + cellIds.clear(); + } + + /** + * Create a cell union that corresponds to a continuous range of cell ids. The output is a + * normalized collection of cell ids that covers the leaf cells between "minId" and "maxId" + * inclusive. + * + *

Requires that minId.isLeaf(), maxId.isLeaf(), and minId <= maxId. + */ + public void initFromMinMax(S2CellId minId, S2CellId maxId) { + // assert minId.isLeaf(); + // assert maxId.isLeaf(); + // assert minId.compareTo(maxId) <= 0; + // assert minId.isValid() && maxId.isValid(); + initFromBeginEnd(minId, maxId.next()); + } + + /** + * As {@link #initFromMinMax(S2CellId, S2CellId)}, except that the union covers the range of leaf + * cells from "begin" (inclusive) to "end" (exclusive.) If {@code begin.equals(end)}, the result + * is empty. + * + *

Requires that begin.isLeaf(), end.isLeaf(), and begin <= end. + */ + public void initFromBeginEnd(S2CellId begin, S2CellId end) { + // assert (begin.isLeaf()); + // assert (end.isLeaf()); + // assert (begin.compareTo(end) <= 0); + + // We repeatedly add the largest cell we can, in sorted order. + cellIds.clear(); + for (S2CellId nextBegin = begin; nextBegin.compareTo(end) < 0; ) { + // assert(nextBegin.isLeaf()); + + // Find the largest cell that starts at "next_begin" and ends before "end". + S2CellId nextId = nextBegin; + while (!nextId.isFace() + && nextId.parent().rangeMin().equals(nextBegin) + && nextId.parent().rangeMax().compareTo(end) < 0) { + nextId = nextId.parent(); + } + cellIds.add(nextId); + nextBegin = nextId.rangeMax().next(); + } + + // The output should already be sorted and normalized. + // assert(!normalize()); + } + + public int size() { + return cellIds.size(); + } + + /** Convenience methods for accessing the individual cell ids. */ + public S2CellId cellId(int i) { + return cellIds.get(i); + } + + /** Enable iteration over the union's cells. */ + @Override + public Iterator iterator() { + return cellIds.iterator(); + } + + /** Direct access to the underlying vector for iteration . */ + public ArrayList cellIds() { + return cellIds; + } + + /** + * Returns true if the cell union is valid, meaning that the S2CellIds are non-overlapping and + * sorted in increasing order. + */ + public boolean isValid() { + for (int i = 1; i < cellIds.size(); i++) { + if (cellIds.get(i - 1).rangeMax().compareTo(cellIds.get(i).rangeMin()) >= 0) { + return false; + } + } + return true; + } + + /** + * Returns true if the cell union is normalized, meaning that it {@link #isValid()} is true and + * that no four cells have a common parent. + * + *

Certain operations such as {@link #contains(S2CellUnion)} may return a different result if + * the cell union is not normalized. + */ + public boolean isNormalized() { + for (int i = 1; i < cellIds.size(); i++) { + if (cellIds.get(i - 1).rangeMax().compareTo(cellIds.get(i).rangeMin()) >= 0) { + return false; + } + if (i >= 3 + && areSiblings( + cellIds.get(i - 3), cellIds.get(i - 2), + cellIds.get(i - 1), cellIds.get(i))) { + return false; + } + } + return true; + } + + /** + * Returns true if the given four cells have a common parent. + * + *

Requires the four cells are distinct. + */ + private static boolean areSiblings(S2CellId a, S2CellId b, S2CellId c, S2CellId d) { + // A necessary (but not sufficient) condition is that the XOR of the four cells must be zero. + // This is also very fast to test. + if ((a.id() ^ b.id() ^ c.id()) != d.id()) { + return false; + } + + // Now we do a slightly more expensive but exact test. First, compute a mask that blocks out + // the two bits that encode the child position of "id" with respect to its parent, then check + // that the other three children all agree with "mask". + long mask = d.lowestOnBit() << 1; + mask = ~(mask + (mask << 1)); + long idMasked = d.id() & mask; + return !d.isFace() + && (a.id() & mask) == idMasked + && (b.id() & mask) == idMasked + && (c.id() & mask) == idMasked; + } + + /** + * Replaces "output" with an expanded version of the cell union where any cells whose level is + * less than "min_level" or where (level - min_level) is not a multiple of "level_mod" are + * replaced by their children, until either both of these conditions are satisfied or the maximum + * level is reached. + * + *

This method allows a covering generated by S2RegionCoverer using min_level() or level_mod() + * constraints to be stored as a normalized cell union (which allows various geometric + * computations to be done) and then converted back to the original list of cell ids that + * satisfies the desired constraints. + */ + public void denormalize(int minLevel, int levelMod, ArrayList output) { + // assert (minLevel >= 0 && minLevel <= S2CellId.MAX_LEVEL); + // assert (levelMod >= 1 && levelMod <= 3); + + output.clear(); + output.ensureCapacity(size()); + for (S2CellId id : this) { + int level = id.level(); + int newLevel = Math.max(minLevel, level); + if (levelMod > 1) { + // Round up so that (new_level - min_level) is a multiple of level_mod. + // (Note that S2CellId::kMaxLevel is a multiple of 1, 2, and 3.) + newLevel += (S2CellId.MAX_LEVEL - (newLevel - minLevel)) % levelMod; + newLevel = Math.min(S2CellId.MAX_LEVEL, newLevel); + } + if (newLevel == level) { + output.add(id); + } else { + S2CellId end = id.childEnd(newLevel); + for (id = id.childBegin(newLevel); !id.equals(end); id = id.next()) { + output.add(id); + } + } + } + } + + /** + * If there are more than "excess" elements of the cell_ids() vector that are allocated but + * unused, reallocate the array to eliminate the excess space. This reduces memory usage when many + * cell unions need to be held in memory at once. + */ + public void pack() { + cellIds.trimToSize(); + } + + /** + * Return true if the cell union contains the given cell id. Containment is defined with respect + * to regions, e.g. a cell contains its 4 children. This is a fast operation (logarithmic in the + * size of the cell union). + * + *

CAVEAT: If you have constructed a valid but non-normalized S2CellUnion, note that groups of + * 4 child cells are not considered to contain their parent cell. To get this behavior + * you must construct a normalized cell union, or call {@link #normalize()} prior to this method. + */ + public boolean contains(S2CellId id) { + // This is an exact test. Each cell occupies a linear span of the S2 + // space-filling curve, and the cell id is simply the position at the center + // of this span. The cell union ids are sorted in increasing order along + // the space-filling curve. So we simply find the pair of cell ids that + // surround the given cell id (using binary search). There is containment + // if and only if one of these two cell ids contains this cell. + + int pos = Collections.binarySearch(cellIds, id); + if (pos < 0) { + pos = -pos - 1; + } + if (pos < cellIds.size() && cellIds.get(pos).rangeMin().lessOrEquals(id)) { + return true; + } + return pos != 0 && cellIds.get(pos - 1).rangeMax().greaterOrEquals(id); + } + + /** + * Return true if the cell union intersects the given cell id. This is a fast operation + * (logarithmic in the size of the cell union). + */ + public boolean intersects(S2CellId id) { + // This is an exact test; see the comments for Contains() above. + int pos = Collections.binarySearch(cellIds, id); + + if (pos < 0) { + pos = -pos - 1; + } + + if (pos < cellIds.size() && cellIds.get(pos).rangeMin().lessOrEquals(id.rangeMax())) { + return true; + } + return pos != 0 && cellIds.get(pos - 1).rangeMax().greaterOrEquals(id.rangeMin()); + } + + /** + * Returns true if this cell union contains {@code that}. + * + *

CAVEAT: If you have constructed a valid but non-normalized S2CellUnion, note that groups of + * 4 child cells are not considered to contain their parent cell. To get this behavior + * you must construct a normalized cell union, or call {@link #normalize()} prior to this method. + */ + public boolean contains(S2CellUnion that) { + S2CellUnion result = new S2CellUnion(); + result.getIntersection(this, that); + return result.cellIds.equals(that.cellIds); + } + + /** This is a fast operation (logarithmic in the size of the cell union). */ + @Override + public boolean contains(S2Cell cell) { + return contains(cell.id()); + } + + /** Return true if this cell union intersects {@code union}. */ + public boolean intersects(S2CellUnion union) { + S2CellUnion result = new S2CellUnion(); + result.getIntersection(this, union); + return result.size() > 0; + } + + /** Sets this cell union to the union of {@code x} and {@code y}. */ + public void getUnion(S2CellUnion x, S2CellUnion y) { + // assert (x != this && y != this); + cellIds.clear(); + cellIds.ensureCapacity(x.size() + y.size()); + cellIds.addAll(x.cellIds); + cellIds.addAll(y.cellIds); + normalize(); + } + + /** + * Specialized version of GetIntersection() that gets the intersection of a cell union with the + * given cell id. This can be useful for "splitting" a cell union into chunks. + * + *

Note: {@code x} and {@code y} must be normalized. + */ + public void getIntersection(S2CellUnion x, S2CellId id) { + // assert (x != this); + cellIds.clear(); + if (x.contains(id)) { + cellIds.add(id); + } else { + int pos = Collections.binarySearch(x.cellIds, id.rangeMin()); + + if (pos < 0) { + pos = -pos - 1; + } + + S2CellId idmax = id.rangeMax(); + int size = x.cellIds.size(); + while (pos < size && x.cellIds.get(pos).lessOrEquals(idmax)) { + cellIds.add(x.cellIds.get(pos++)); + } + } + // assert isNormalized() || !x.isNormalized(); + } + + /** + * Initializes this cell union to the intersection of the two given cell unions. Requires: x != + * this and y != this. + * + *

Note: {@code x} and {@code y} must be normalized. + */ + public void getIntersection(S2CellUnion x, S2CellUnion y) { + getIntersection(x.cellIds, y.cellIds, cellIds); + // The output is normalized as long as at least one input is normalized. + // assert isNormalized() || (!x.isNormalized() && !y.isNormalized()); + } + + /** + * Like {@code #getIntersection(S2CellUnion, S2CellUnion)}, but works directly with lists of + * S2CellIds, and this method has slightly more relaxed normalization requirements: the input + * vectors may contain groups of 4 child cells that all have the same parent. (In a normalized + * S2CellUnion, such groups are always replaced by the parent cell.) + * + *

Note: {@code x} and {@code y} must be sorted. + */ + public static void getIntersection(List x, List y, List results) { + // assert (x != results && y != results); + + // This is a fairly efficient calculation that uses binary search to skip + // over sections of both input vectors. It takes constant time if all the + // cells of "x" come before or after all the cells of "y" in S2CellId order. + results.clear(); + int i = 0; + int j = 0; + while (i < x.size() && j < y.size()) { + S2CellId xCell = x.get(i); + S2CellId xMin = xCell.rangeMin(); + S2CellId yCell = y.get(j); + S2CellId yMin = yCell.rangeMin(); + if (xMin.greaterThan(yMin)) { + // Either j->contains(xCell) or the two cells are disjoint. + if (xCell.lessOrEquals(yCell.rangeMax())) { + results.add(xCell); + i++; + } else { + // Advance "j" to the first cell possibly contained by xCell. + j = indexedBinarySearch(y, xMin, j + 1); + // The previous cell *(j-1) may now contain xCell. + if (xCell.lessOrEquals(y.get(j - 1).rangeMax())) { + --j; + } + } + } else if (yMin.greaterThan(xMin)) { + // Identical to the code above with "i" and "j" reversed. + if (yCell.lessOrEquals(xCell.rangeMax())) { + results.add(yCell); + j++; + } else { + i = indexedBinarySearch(x, yMin, i + 1); + if (yCell.lessOrEquals(x.get(i - 1).rangeMax())) { + --i; + } + } + } else { + // "i" and "j" have the same rangeMin(), so one contains the other. + if (xCell.lessThan(yCell)) { + results.add(xCell); + i++; + } else { + results.add(yCell); + j++; + } + } + } + } + + /** Initiaizes this cell union to the difference of the two given cell unions. */ + public void getDifference(S2CellUnion x, S2CellUnion y) { + // TODO(user): this is approximately O(N*log(N)), but could probably use similar techniques as + // getIntersection() to be more efficient. + cellIds.clear(); + for (S2CellId id : x) { + getDifferenceInternal(id, y); + } + // The output is normalized as long as the first argument is normalized. + // assert isNormalized() || !x.isNormalized(); + } + + private void getDifferenceInternal(S2CellId cell, S2CellUnion y) { + // Add the difference between cell and y to cellIds. If they intersect but the difference is + // non-empty, divide and conquer. + if (!y.intersects(cell)) { + cellIds.add(cell); + } else if (!y.contains(cell)) { + for (int i = 0; i < 4; i++) { + getDifferenceInternal(cell.child(i), y); + } + } + } + + /** + * Just as normal binary search, except that it allows specifying the starting value for the lower + * bound. + * + * @return The position of the searched element in the list (if found), or the position where the + * element could be inserted without violating the order. + */ + private static int indexedBinarySearch(List l, S2CellId key, int low) { + int high = l.size() - 1; + + while (low <= high) { + int mid = (low + high) >> 1; + S2CellId midVal = l.get(mid); + int cmp = midVal.compareTo(key); + + if (cmp < 0) { + low = mid + 1; + } else if (cmp > 0) { + high = mid - 1; + } else { + return mid; // key found + } + } + return low; // key not found + } + + /** + * Expands the cell union such that it contains all cells of the given level that are adjacent to + * any cell of the original union. Two cells are defined as adjacent if their boundaries have any + * points in common, i.e. most cells have 8 adjacent cells (not counting the cell itself). + * + *

Note that the size of the output is exponential in "level". For example, if level == 20 and + * the input has a cell at level 10, there will be on the order of 4000 adjacent cells in the + * output. For most applications the Expand(min_fraction, min_distance) method below is easier to + * use. + */ + public void expand(int level) { + ArrayList output = new ArrayList(); + long levelLsb = S2CellId.lowestOnBitForLevel(level); + for (int i = size(); --i >= 0; ) { + S2CellId id = cellId(i); + if (id.lowestOnBit() < levelLsb) { + id = id.parent(level); + // Optimization: skip over any cells contained by this one. This is + // especially important when very small regions are being expanded. + while (i > 0 && id.contains(cellId(i - 1))) { + --i; + } + } + output.add(id); + id.getAllNeighbors(level, output); + } + initSwap(output); + } + + /** + * Expand the cell union such that it contains all points whose distance to the cell union is at + * most minRadius, but do not use cells that are more than maxLevelDiff levels higher than the + * largest cell in the input. The second parameter controls the tradeoff between accuracy and + * output size when a large region is being expanded by a small amount (e.g. expanding Canada by + * 1km). + * + *

For example, if maxLevelDiff == 4, the region will always be expanded by approximately 1/16 + * the width of its largest cell. Note that in the worst case, the number of cells in the output + * can be up to 4 * (1 + 2 ** maxLevelDiff) times larger than the number of cells in the input. + */ + public void expand(S1Angle minRadius, int maxLevelDiff) { + int minLevel = S2CellId.MAX_LEVEL; + for (S2CellId id : this) { + minLevel = Math.min(minLevel, id.level()); + } + // Find the maximum level such that all cells are at least "min_radius" + // wide. + int radiusLevel = PROJ.minWidth.getMaxLevel(minRadius.radians()); + if (radiusLevel == 0 && minRadius.radians() > PROJ.minWidth.getValue(0)) { + // The requested expansion is greater than the width of a face cell. + // The easiest way to handle this is to expand twice. + expand(0); + } + expand(Math.min(minLevel + maxLevelDiff, radiusLevel)); + } + + // NOTE: This should be marked as @Override, but clone() isn't present in GWT's version of + // Object, so we can't mark it as such. + @SuppressWarnings("MissingOverride") + public S2Region clone() { + S2CellUnion copy = new S2CellUnion(); + copy.initRawCellIds(Lists.newArrayList(cellIds)); + return copy; + } + + @Override + public S2Cap getCapBound() { + // Compute the approximate centroid of the region. This won't produce the + // bounding cap of minimal area, but it should be close enough. + if (cellIds.isEmpty()) { + return S2Cap.empty(); + } + S2Point centroid = S2Point.ORIGIN; + for (S2CellId id : this) { + double area = S2Cell.averageArea(id.level()); + centroid = S2Point.add(centroid, S2Point.mul(id.toPoint(), area)); + } + if (centroid.equalsPoint(S2Point.ORIGIN)) { + centroid = S2Point.X_POS; + } else { + centroid = S2Point.normalize(centroid); + } + + // Use the centroid as the cap axis, and expand the cap angle so that it + // contains the bounding caps of all the individual cells. Note that it is + // *not* sufficient to just bound all the cell vertices because the bounding + // cap may be concave (i.e. cover more than one hemisphere). + S2Cap cap = S2Cap.fromAxisChord(centroid, S1ChordAngle.ZERO); + for (S2CellId id : this) { + cap = cap.addCap(new S2Cell(id).getCapBound()); + } + return cap; + } + + @Override + public S2LatLngRect getRectBound() { + S2LatLngRect.Builder builder = S2LatLngRect.Builder.empty(); + for (S2CellId id : this) { + builder.union(new S2Cell(id).getRectBound()); + } + return builder.build(); + } + + /** This is a fast operation (logarithmic in the size of the cell union). */ + @Override + public boolean mayIntersect(S2Cell cell) { + return intersects(cell.id()); + } + + /** + * The point 'p' does not need to be normalized. This is a fast operation (logarithmic in the size + * of the cell union). + */ + @Override + public boolean contains(S2Point p) { + return contains(S2CellId.fromPoint(p)); + } + + /** + * The number of leaf cells covered by the union. This will be no more than 6*2^60 for the whole + * sphere. + * + * @return the number of leaf cells covered by the union + */ + public long leafCellsCovered() { + long numLeaves = 0; + for (S2CellId cellId : cellIds) { + int invertedLevel = S2CellId.MAX_LEVEL - cellId.level(); + numLeaves += (1L << (invertedLevel << 1)); + } + return numLeaves; + } + + /** + * Approximate this cell union's area by summing the average area of each contained cell's average + * area, using {@link S2Cell#averageArea()}. This is equivalent to the number of leaves covered, + * multiplied by the average area of a leaf. + * + *

Note that {@link S2Cell#averageArea()} does not take into account distortion of cell, and + * thus may be off by up to a factor of 1.7. NOTE: Since this is proportional to + * LeafCellsCovered(), it is always better to use the other function if all you care about is the + * relative average area between objects. + * + * @return the sum of the average area of each contained cell's average area + */ + public double averageBasedArea() { + return S2Cell.averageArea(S2CellId.MAX_LEVEL) * leafCellsCovered(); + } + + /** + * Calculates this cell union's area by summing the approximate area for each contained cell, + * using {@link S2Cell#approxArea()}. + * + * @return approximate area of the cell union + */ + public double approxArea() { + double area = 0; + for (S2CellId cellId : cellIds) { + area += new S2Cell(cellId).approxArea(); + } + return area; + } + + /** + * Calculates this cell union's area by summing the exact area for each contained cell, using the + * {@link S2Cell#exactArea()}. + * + * @return the exact area of the cell union + */ + public double exactArea() { + double area = 0; + for (S2CellId cellId : cellIds) { + area += new S2Cell(cellId).exactArea(); + } + return area; + } + + /** Return true if two cell unions are identical. */ + @Override + public boolean equals(Object that) { + if (!(that instanceof S2CellUnion)) { + return false; + } + S2CellUnion union = (S2CellUnion) that; + return this.cellIds.equals(union.cellIds); + } + + @Override + public int hashCode() { + int value = 17; + for (S2CellId id : this) { + value = 37 * value + id.hashCode(); + } + return value; + } + + /** + * Normalizes the cell union by discarding cells that are contained by other cells, replacing + * groups of 4 child cells by their parent cell whenever possible, and sorting all the cell ids in + * increasing order. Returns true if the number of cells was reduced. + * + *

This method *must* be called before doing any calculations on the cell union, such as + * Intersects() or Contains(). + * + * @return true if the normalize operation had any effect on the cell union, false if the union + * was already normalized + */ + public boolean normalize() { + return normalize(cellIds); + } + + /** Like {@link #normalize()}, but works directly with a vector of S2CellIds. */ + public static boolean normalize(List ids) { + // Optimize the representation by looking for cases where all subcells of a parent cell are + // present. + Collections.sort(ids); + int out = 0; + for (int i = 0; i < ids.size(); i++) { + S2CellId id = ids.get(i); + + // Check whether this cell is contained by the previous cell. + if (out > 0 && ids.get(out - 1).contains(id)) { + continue; + } + + // Discard any previous cells contained by this cell. + while (out > 0 && id.contains(ids.get(out - 1))) { + out--; + } + + // Check whether the last 3 elements of "output" plus "id" can be collapsed into a single + // parent cell. + while (out >= 3) { + // A necessary (but not sufficient) condition is that the XOR of the + // four cells must be zero. This is also very fast to test. + if ((ids.get(out - 3).id() ^ ids.get(out - 2).id() ^ ids.get(out - 1).id()) != id.id()) { + break; + } + + // Now we do a slightly more expensive but exact test. First, compute a + // mask that blocks out the two bits that encode the child position of + // "id" with respect to its parent, then check that the other three + // children all agree with "mask. + long mask = id.lowestOnBit() << 1; + mask = ~(mask + (mask << 1)); + long idMasked = (id.id() & mask); + if ((ids.get(out - 3).id() & mask) != idMasked + || (ids.get(out - 2).id() & mask) != idMasked + || (ids.get(out - 1).id() & mask) != idMasked + || id.isFace()) { + break; + } + + // Replace four children by their parent cell. + id = id.parent(); + out -= 3; + } + + ids.set(out++, id); + } + + int size = ids.size(); + boolean trimmed = out < size; + while (out < size) { + size--; + ids.remove(size); + } + return trimmed; + } + + /** + * Writes a simple lossless encoding of this cell union to the given output stream. This encoding + * uses 1 byte for a version number, and N+1 64-bit longs where the first is the number of longs + * that follow. + * + * @throws IOException there is a problem writing to the underlying stream + */ + public void encode(OutputStream output) throws IOException { + encode(new LittleEndianOutput(output)); + } + + /** + * As {@link #encode(OutputStream)}, but avoids creating a little endian output helper. + * + *

Use this method if a number of S2 objects will be encoded to the same underlying stream. + */ + public void encode(LittleEndianOutput output) throws IOException { + output.writeByte(LOSSLESS_ENCODING_VERSION); + output.writeLong(cellIds.size()); + for (S2CellId cellId : this) { + output.writeLong(cellId.id()); + } + } + + /** + * Decodes an S2CellUnion encoded with Encode(). Returns true on success. + * + * @throws IOException there is a problem reading from the underlying stream, the version number + * doesn't match, or the number of elements to read is not between 0 and 2^31-1. + */ + public static S2CellUnion decode(InputStream input) throws IOException { + return decode(new LittleEndianInput(input)); + } + + /** + * As {@link #decode(InputStream)}, but avoids creating a little endian input helper. + * + *

Use this method if a number of S2 objects will be decoded from the same underlying stream. + */ + public static S2CellUnion decode(LittleEndianInput input) throws IOException { + // Should contain at least version and vector length. + byte version = input.readByte(); + if (version != LOSSLESS_ENCODING_VERSION) { + throw new IOException("Unrecognized version number " + version); + } + long numCells = input.readLong(); + if (numCells < 0 || numCells > Integer.MAX_VALUE) { + throw new IOException("Unsupported number of cells encountered: " + numCells); + } + S2CellUnion result = new S2CellUnion(); + for (int i = 0; i < numCells; i++) { + result.cellIds().add(new S2CellId(input.readLong())); + } + return result; + } +} diff --git a/core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/S2ClosestPointQuery.java b/core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/S2ClosestPointQuery.java new file mode 100644 index 0000000000..e85e4fa967 --- /dev/null +++ b/core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/S2ClosestPointQuery.java @@ -0,0 +1,660 @@ +/* + * Copyright 2015 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.mogo.eagle.core.utilcode.geometry; + +import com.google.common.annotations.GwtCompatible; +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Preconditions; +import com.google.common.collect.Iterables; +import com.google.common.collect.Lists; +import com.mogo.eagle.core.utilcode.geometry.S2PointIndex.Entry; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.PriorityQueue; + +/** + * Given a set of points stored in an S2PointIndex, S2ClosestPointQuery provides methods that find + * the closest point(s) to a given query point. + * + *

Example usage: + * + *

+ * void test(List points, List targets) {
+ *   // The template argument allows auxiliary data to be attached to each point (in this case, the
+ *   // array index).
+ *   S2PointIndex index = new S2PointIndex<>();
+ *   for (int i = 0; i < points.size(); i++) {
+ *     index.add(points.get(i), i);
+ *   }
+ *   S2ClosestPointQuery query = new S2ClosestPointQuery<>(index);
+ *   query.setMaxPoints(15);
+ *   for (S2Point target : targets) {
+ *     for (Result result : query.findClosestPoints(target)) {
+ *       // result.entry().point() is one of the found closest points.
+ *       // result.entry().data() is the auxiliary data (the "points" array index).
+ *       // result.distance() is the distance to the target point.
+ *       doSomething(target, result.entry().point(), result.entry().data(), result.distance());
+ *     }
+ *   }
+ * }
+ * + *

You can find either the k closest points, or all points within a given radius, or both (i.e., + * the k closest points up to a given maximum radius). E.g. to find all the points within 5 + * kilometers, call {@code query.setMaxDistance(S1Angle.fromEarthDistance(5000));}. + * + *

You can also restrict the results to an arbitrary S2Region via {@link #setRegion(S2Region)}. + * + *

The implementation is designed to be very fast for both small and large point sets. + * + *

This class is not thread-safe. In particular, setters must not be called during queries. + */ +@GwtCompatible +public final class S2ClosestPointQuery { + // TODO(eengle): retune the constants. + + /** The maximum number of points to process by brute force. */ + private static final int MAX_BRUTE_FORCE_POINTS = 150; + + /** The maximum number of points to process without subdividing further. */ + private static final int MAX_LEAF_POINTS = 12; + + /** The index being queried. */ + private final S2PointIndex index; + + /** The max number of closest points to find. */ + private int maxPoints; + + /** The max distance to search for points. */ + private S1Angle maxDistance; + + /** The region to restrict closest point search to. */ + private S2Region region; + + /** Whether to use brute force, which is cheaper when the index has few edges. */ + private boolean useBruteForce; + + /** A small (<6) cell covering of the indexed points. */ + private List indexCovering = Lists.newArrayList(); + + /** Unprocessed cells for the current query being processed. */ + private final PriorityQueue queue = new PriorityQueue<>(); + + /** The iterator for the last-known state of the index. New instance built by {@link #reset()}. */ + private S2Iterator> iter; + + /** The covering of {@link #indexCovering}. Type is ArrayList due to {@link S2RegionCoverer}. */ + private ArrayList regionCovering = Lists.newArrayList(); + + /** The covering of {@link #maxDistance}. Type is ArrayList due to {@link S2RegionCoverer}. */ + private final ArrayList maxDistanceCovering = Lists.newArrayList(); + + /** The intersection between the index and {@link #regionCovering}. */ + private final List intersectionWithRegion = Lists.newArrayList(); + + /** The intersection between the index and {@link #maxDistance}. */ + private final List intersectionWithMaxDistance = Lists.newArrayList(); + + /** Temporary storage for index entries that are of interest during query processing. */ + @SuppressWarnings({"rawtypes", "unchecked"}) + private final Entry[] tmpPoints = new Entry[MAX_LEAF_POINTS]; + + /** Temporary queue of results sorted in descending order. */ + private final PriorityQueue> results = new PriorityQueue<>(); + + /** + * Temporary distance to continue searching during a query, generally the distance of the furthest + * point in the results found so far. Beyond this distance, we can safely ignore further candidate + * points. Candidates that are exactly at the limit are ignored; this makes things easier in the + * case of S2ClosestEdgeQuery and should not affect clients since distance measurements have a + * small amount of error anyway. + * + *

Initially this is the same as the maximum distance specified by the user, but it can also be + * updated by the algorithm (see maybeAddResult). + */ + private S1ChordAngle maxDistanceLimit; + + /** + * Construct a new query for the given index. Must call reset() before using the query, if the + * index has been modified since the query was constructed. + */ + public S2ClosestPointQuery(S2PointIndex index) { + this.index = index; + maxPoints = Integer.MAX_VALUE; + maxDistance = S1Angle.INFINITY; + region = null; + reset(); + } + + /** Resets the query state. This method must be called after modifying the underlying index. */ + public void reset() { + iter = index.iterator(); + useBruteForce(index.numPoints() <= MAX_BRUTE_FORCE_POINTS); + } + + /** Returns the underlying S2PointIndex. */ + public S2PointIndex index() { + return index; + } + + /** Returns the max number of closest points to find. */ + public int getMaxPoints() { + return maxPoints; + } + + /** Sets a new max number of closest points to find. */ + public void setMaxPoints(int maxPoints) { + Preconditions.checkArgument(maxPoints >= 1, "Must be at least 1."); + this.maxPoints = maxPoints; + } + + /** Returns the max distance between returned points and the given target. Default is +inf. */ + public S1Angle getMaxDistance() { + return maxDistance; + } + + /** Sets a new max distance to search for points. */ + public void setMaxDistance(S1Angle maxDistance) { + this.maxDistance = maxDistance; + } + + /** Returns the region in which point searches will be done. */ + public S2Region getRegion() { + return region; + } + + /* + * Sets the region in which point searches will be done, or clears the region if {@code region} is + * null. + * + *

Note that if you want to set the region to a disc around the target point, it is faster to + * use setMaxDistance() instead. You can also call both methods, e.g. if you want to limit the + * maximum distance to the target and also require that points lie within a given rectangle. + */ + public void setRegion(S2Region region) { + this.region = region; + } + + /** + * Sets whether distances are computed using "brute force" (i.e., by examining every point) rather + * than using the S2PointIndex. + * + *

This is package private, as it is intended only for testing, benchmarking, and debugging. + * + *

Do not call before init(). + */ + @VisibleForTesting + void useBruteForce(boolean useBruteForce) { + this.useBruteForce = useBruteForce; + if (!useBruteForce) { + initIndexCovering(); + } + } + + /** + * Creates an empty list if 'list' is null, and then polls all results out of {@link #results} + * into the given list in reverse order, and returns it. + */ + private List> toList(List> list) { + int size = results.size(); + int index = size; + if (list == null) { + list = Lists.newArrayListWithCapacity(size); + } else { + index += list.size(); + } + // Allocate 'size' elements at the end of the list, and fill the items in reverse. + list.addAll(Collections.>nCopies(size, null)); + while (size-- > 0) { + list.set(--index, results.poll()); + } + return list; + } + + /** + * Returns the closest points to {@code target} that satisfy the {@link #getMaxDistance()}, {@link + * #getMaxPoints()}, and {@link #getRegion()} criteria, ordered by increasing distance. If there + * are no criteria set, then all points are returned. + */ + public List> findClosestPoints(S2Point target) { + findClosestPointsToTarget(new PointTarget(target)); + return toList(null); + } + + /** + * As {@link #findClosestPoints(S2Point)}, but sorts the results and adds them at the end of the + * given list. + */ + public void findClosestPoints(List> results, S2Point target) { + findClosestPointsToTarget(new PointTarget(target)); + toList(results); + } + + /** + * Convenience method that returns the closest point to the given target point, or null if no + * points satisfy the {@link #getMaxDistance()} and {@link #getRegion()} criteria. + */ + public Result findClosestPoint(S2Point target) { + setMaxPoints(1); + return Iterables.getOnlyElement(findClosestPoints(target), null); + } + + /** + * Returns the closest points to the given edge AB. Otherwise similar to {@link + * #findClosestPoints(S2Point)}. + */ + public List> findClosestPointsToEdge(S2Point a, S2Point b) { + findClosestPointsToTarget(new EdgeTarget(a, b)); + return toList(null); + } + + /** As {@link #findClosestPointsToEdge(S2Point, S2Point)}, but adds results to the given list. */ + public void findClosestPointsToEdge(List> results, S2Point a, S2Point b) { + findClosestPointsToTarget(new EdgeTarget(a, b)); + toList(results); + } + + /** A kind of query target. */ + private interface Target { + /** Returns the approximate center of the target. */ + S2Point center(); + /** Returns the distance between this target and the given cell. */ + S1ChordAngle getDistance(S2Cell cell); + /** Returns the radian radius of an angular cap that encloses this target. */ + double radius(); + /** Returns the smaller of {@code distance} and a new distance from target to {@code point}. */ + S1ChordAngle getMinDistance(S2Point point, S1ChordAngle distance); + } + + /** A point query, used to find the closest points to a query point. */ + private static class PointTarget implements Target { + private final S2Point point; + + public PointTarget(S2Point point) { + this.point = point; + } + + @Override + public S2Point center() { + return point; + } + + @Override + public double radius() { + return 0; + } + + @Override + public S1ChordAngle getMinDistance(S2Point x, S1ChordAngle minDist) { + S1ChordAngle angle = new S1ChordAngle(x, point); + // See comment regarding ">=" in the findClosestPoints() main loop. + return angle.compareTo(minDist) > 0 ? minDist : angle; + } + + @Override + public S1ChordAngle getDistance(S2Cell cell) { + return cell.getDistance(point); + } + } + + /** An edge query, used to find the closest points to a query edge. */ + private static class EdgeTarget implements Target { + private S2Point a; + private S2Point b; + + public EdgeTarget(S2Point a, S2Point b) { + this.a = a; + this.b = b; + } + + @Override + public S2Point center() { + return S2Point.normalize(S2Point.add(a, b)); + } + + @Override + public double radius() { + return 0.5 * a.angle(b); + } + + @Override + public S1ChordAngle getMinDistance(S2Point x, S1ChordAngle minDist) { + return S2EdgeUtil.updateMinDistance(x, a, b, minDist); + } + + @Override + public S1ChordAngle getDistance(S2Cell cell) { + return cell.getDistanceToEdge(a, b); + } + } + + /** + * Computes the "index covering", which is a small number of S2CellIds that cover the indexed + * points. + */ + private void initIndexCovering() { + // There are two cases: + // - If the index spans more than one face, then there is one covering cell per spanned face, + // just big enough to cover the index cells on that face. + // - If the index spans only one face, then we find the smallest cell "C" that covers the index + // cells on that face (just like the case above). Then for each of the 4 children of "C", if + // the child contains any index cells then we create a covering cell that is big enough to + // just fit those index cells (i.e., shrinking the child as much as possible to fit its + // contents). This essentially replicates what would happen if we started with "C" as the + // covering cell, since "C" would immediately be split, except that we take the time to prune + // the children further since this will save work on every subsequent query. + indexCovering.clear(); + iter.restart(); + if (iter.done()) { + // Empty index. + return; + } + + S2Iterator> nextIt = iter.copy(); + S2CellId indexNext = nextIt.id(); + S2Iterator> lastIt = iter.copy(); + lastIt.finish(); + lastIt.prev(); + S2CellId indexLast = lastIt.id(); + if (!nextIt.equalIterators(lastIt)) { + // The index has at least two cells. Choose a level such that the entire index can be spanned + // with at most 6 cells (if the index spans multiple faces) or 4 cells (if the index spans a + // single face). + int level = indexNext.getCommonAncestorLevel(indexLast) + 1; + + // Visit each potential covering cell except the last (handled below). + S2CellId coverLast = indexLast.parent(level); + for (S2CellId cover = indexNext.parent(level); + !cover.equals(coverLast) && !nextIt.done(); + cover = cover.next()) { + // Skip any covering cells that don't contain any index cells. + S2CellId coverMax = cover.rangeMax(); + if (nextIt.compareTo(coverMax) <= 0) { + // Find the range of index cells contained by this covering cell and then shrink the cell + // if necessary so that it just covers them. + S2CellId prevId = indexNext; + nextIt.seek(coverMax.next()); + indexNext = nextIt.id(); + S2Iterator> cellLast = nextIt.copy(); + cellLast.prev(); + coverRange(prevId, cellLast.id()); + } + } + } + coverRange(indexNext, indexLast); + } + + /** + * Adds a cell to indexCovering that covers the given inclusive range. This is done with {@link + * S2CellId#getCommonAncestorLevel(S2CellId)}, which requires the cells have a common ancestor. + */ + private void coverRange(S2CellId firstId, S2CellId lastId) { + int level = firstId.getCommonAncestorLevel(lastId); + indexCovering.add(firstId.parent(level)); + } + + private void findClosestPointsToTarget(Target target) { + maxDistanceLimit = S1ChordAngle.fromS1Angle(maxDistance); + if (useBruteForce) { + findClosestPointsBruteForce(target); + } else { + findClosestPointsOptimized(target); + } + } + + private void findClosestPointsBruteForce(Target target) { + for (iter.restart(); !iter.done(); iter.next()) { + maybeAddResult(iter.entry(), target); + } + } + + private void findClosestPointsOptimized(Target target) { + initQueue(target); + while (!queue.isEmpty()) { + QueueEntry entry = queue.poll(); + if (entry.distance().compareTo(maxDistanceLimit) >= 0) { + queue.clear(); + break; + } + S2CellId child = entry.id.childBegin(); + // We already know that it has too many points, so process its children. Each child may either + // be processed directly or enqueued again. The loop is optimized so that we don't seek + // unnecessarily. + boolean seek = true; + for (int i = 0; i < 4; i++, child = child.next()) { + seek = addCell(child, iter, seek, target); + } + } + } + + private void maybeAddResult(Entry entry, Target target) { + S1ChordAngle distance = target.getMinDistance(entry.point(), maxDistanceLimit); + if (distance == maxDistanceLimit) { + // The previous 'max' reference is returned in this case, so only check object identity. + return; + } + if (region != null && !region.contains(entry.point())) { + return; + } + + // Add this point to results. + if (results.size() >= maxPoints) { + // Replace the furthest result point. + results.poll(); + } + results.add(new Result<>(distance, entry)); + if (results.size() >= maxPoints) { + maxDistanceLimit = results.peek().distance(); + } + } + + private void initQueue(Target target) { + // assert queue.isEmpty(); + + // Optimization: rather than starting with the entire index, see if we can limit the search + // region to a small disc. Then we can find a covering for that disc and intersect it with the + // covering for the index. This can save a lot of work when the search region is small. + + if (maxPoints == 1) { + // If the user is searching for just the closest point, we can compute an upper bound on + // search radius by seeking to the target point in the index and looking at the adjacent index + // points (in S2CellId order). The minimum distance to either of these points is an upper + // bound on the search radius. + // + // TODO(user): The same strategy would also work for small values of maxPoints() > 1, e.g. + // maxPoints() == 20, except that we would need to examine more neighbors (at least 20, and + // preferably 20 in each direction). It's not clear whether this is a common case, though, + // and also this would require extending maybeAddResult() so that it can remove duplicate + // entries. (The points added here may be re-added by addCell(), but this is okay when + // maxPoints() == 1.) + iter.seek(S2CellId.fromPoint(target.center())); + if (!iter.done()) { + maybeAddResult(iter.entry(), target); + } + if (!iter.atBegin()) { + iter.prev(); + maybeAddResult(iter.entry(), target); + } + } + + // We start with a covering of the set of indexed points, then intersect it with the given + // region (if any) and maximum search radius disc (if any). + List initialCells = indexCovering; + S2RegionCoverer coverer = S2RegionCoverer.builder().setMaxCells(4).build(); + if (region != null) { + coverer.getCovering(region, regionCovering); + S2CellUnion.getIntersection(indexCovering, regionCovering, intersectionWithRegion); + initialCells = intersectionWithRegion; + } + if (!maxDistanceLimit.isInfinity()) { + S2Cap searchCap = + S2Cap.fromAxisAngle( + target.center(), + S1Angle.radians(target.radius() + maxDistanceLimit.toAngle().radians())); + coverer.getFastCovering(searchCap, maxDistanceCovering); + S2CellUnion.getIntersection(initialCells, maxDistanceCovering, intersectionWithMaxDistance); + initialCells = intersectionWithMaxDistance; + } + iter.restart(); + for (int i = 0; i < initialCells.size() && !iter.done(); i++) { + S2CellId id = initialCells.get(i); + boolean seek = iter.compareTo(id.rangeMin()) <= 0; + addCell(id, iter, seek, target); + } + } + + /** + * Processes the cell at {@code id}, adding the contents of the cell immediately, or if there are + * too many points, adding it to the queue to be subdivided. If {@code seek} is false, then {@code + * iter} must already be positioned at the first indexed point within this cell. + * + * @return true if the cell was added to the queue, and false if it was processed immediately (in + * which case {@code iter} is left positioned at the next cell in S2CellId order. + */ + private boolean addCell(S2CellId id, S2Iterator> iter, boolean seek, Target target) { + if (seek) { + iter.seek(id.rangeMin()); + } + if (id.isLeaf()) { + // Leaf cells can't be subdivided. + for (; !iter.done() && iter.compareTo(id) == 0; iter.next()) { + maybeAddResult(iter.entry(), target); + } + // No need to seek to next child. + return false; + } + S2CellId last = id.rangeMax(); + int numPoints = 0; + for (; !iter.done() && iter.compareTo(last) <= 0; iter.next()) { + if (numPoints == MAX_LEAF_POINTS) { + // This child cell has too many points, so enqueue it. + S2Cell cell = new S2Cell(id); + S1ChordAngle distance = target.getDistance(cell); + if (distance.compareTo(maxDistanceLimit) < 0) { + // We delay checking "region_" as long as possible because it may be + // relatively expensive. + if (region == null || region.mayIntersect(cell)) { + queue.add(new QueueEntry(distance, id)); + } + } + // Seek to next child. + return true; + } + tmpPoints[numPoints++] = iter.entry(); + } + // There were few enough points that we might as well process them now. + for (int i = 0; i < numPoints; i++) { + maybeAddResult(tmpPoints[i], target); + } + // No need to seek to next child. + return false; + } + + /** A type that is comparable on distance only. */ + private abstract static class ChordComparable implements Comparable { + protected final S1ChordAngle distance; + + ChordComparable(S1ChordAngle distance) { + this.distance = distance; + } + + public final S1ChordAngle distance() { + return distance; + } + } + + /** + * A query result paired with the distance to the query target. Natural order is by distance in + * descending order. + */ + public static class Result extends ChordComparable { + private final Entry pointData; + + private Result(S1ChordAngle distance, Entry pointData) { + super(distance); + this.pointData = pointData; + } + + public Entry entry() { + return pointData; + } + + @Override + public int hashCode() { + return pointData.hashCode(); + } + + @Override + public boolean equals(Object o) { + if (o instanceof Result) { + // Don't need to test distance, it's derived from point data. + Result other = (Result) o; + return pointData.equals(other.pointData); + } else { + return false; + } + } + + @Override + public String toString() { + return distance().toAngle().degrees() + ": " + pointData; + } + + @Override + public int compareTo(ChordComparable other) { + // The algorithm works by replacing the result whose distance is largest + // when a better candidate is found, so we keep the entries sorted such + // that the largest distance is at the top of the heap. + return other.distance.compareTo(distance); + } + } + + /** + * A queued cell waiting to be processed by the current query, ordered by distance to any point in + * the cell in ascending order. + */ + private static class QueueEntry extends ChordComparable { + private final S2CellId id; + + QueueEntry(S1ChordAngle distance, S2CellId id) { + super(distance); + this.id = id; + } + + @Override + public int hashCode() { + return id.hashCode() * 31 + distance().hashCode(); + } + + @Override + public boolean equals(Object o) { + if (o instanceof QueueEntry) { + QueueEntry q = (QueueEntry) o; + return id.equals(q.id) && distance.equals(q.distance); + } else { + return false; + } + } + + @Override + public int compareTo(ChordComparable other) { + // The algorithm works by replacing the result whose distance is largest + // when a better candidate is found, so we keep the entries sorted such + // that the largest distance is at the top of the heap. + return distance.compareTo(other.distance); + } + } +} diff --git a/core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/S2Coder.java b/core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/S2Coder.java new file mode 100644 index 0000000000..4036733f7f --- /dev/null +++ b/core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/S2Coder.java @@ -0,0 +1,41 @@ +/* + * Copyright 2019 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.mogo.eagle.core.utilcode.geometry; + +import com.google.common.annotations.GwtCompatible; +import java.io.IOException; +import java.io.OutputStream; + +/** + * An interface for encoding and decoding values. + * + *

This is one of several helper classes that allow complex data structures to be initialized + * from an encoded format in constant time and then decoded on demand. This can be a big performance + * advantage when only a small part of the data structure is actually used. + */ +@GwtCompatible +public interface S2Coder { + + /** Encodes {@code value} to {@code output}. */ + void encode(T value, OutputStream output) throws IOException; + + /** + * Decodes a value of type {@link T} from {@code data} starting at {@code cursor.position}. {@code + * cursor.position} is updated to the position of the first byte in {@code data} following the + * encoded value. + */ + T decode(PrimitiveArrays.Bytes data, PrimitiveArrays.Cursor cursor); +} diff --git a/core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/S2ContainsPointQuery.java b/core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/S2ContainsPointQuery.java new file mode 100644 index 0000000000..07eff47724 --- /dev/null +++ b/core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/S2ContainsPointQuery.java @@ -0,0 +1,278 @@ +/* + * Copyright 2017 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.mogo.eagle.core.utilcode.geometry; + +import com.google.common.annotations.GwtCompatible; +import com.google.common.base.Predicate; +import com.google.common.collect.AbstractIterator; +import com.google.common.collect.ImmutableList; +import com.mogo.eagle.core.utilcode.geometry.S2EdgeUtil.EdgeCrosser; +import com.mogo.eagle.core.utilcode.geometry.S2Shape.MutableEdge; +import com.mogo.eagle.core.utilcode.geometry.S2ShapeIndex.S2ClippedShape; +import java.util.Iterator; + +/** + * A query for whether one or more shapes in an {@link S2ShapeIndex} contain a given S2Point. + * + *

The S2ShapeIndex may contain any number of points, polylines, and/or polygons (possibly + * overlapping). Shape boundaries are modeled with a constructor parameter, {@link S2VertexModel}, + * which defaults to {@link S2VertexModel#SEMI_OPEN}. This may be customized to control whether or + * not shapes are considered to contain their vertices. + * + *

This class is not thread-safe. To use it in parallel, each thread should construct its own + * instance (this is not expensive). However, note that if you need to do a large number of point + * containment tests, it is more efficient to re-use the S2ContainsPointQuery object rather than + * constructing a new one each time. + */ +@GwtCompatible +public class S2ContainsPointQuery { + /** The options for building an S2ContainsPointQuery. */ + public static final class Options { + public static final Options OPEN = new Options(S2VertexModel.OPEN); + public static final Options SEMI_OPEN = new Options(S2VertexModel.SEMI_OPEN); + public static final Options CLOSED = new Options(S2VertexModel.CLOSED); + private final S2VertexModel vertexModel; + + private Options(S2VertexModel vertexModel) { + this.vertexModel = vertexModel; + } + /** Returns the vertex model in this options. */ + public S2VertexModel vertexModel() { + return vertexModel; + } + } + + /** A rule for whether shapes are considered to contain their vertices. */ + public enum S2VertexModel { + /** + * In the OPEN model, no shapes contain their vertices (not even points). Therefore + * contains(S2Point) returns true if and only if the point is in the interior of some polygon. + */ + OPEN, + + /** + * In the SEMI_OPEN model, polygon point containment is defined such that if several polygons + * tile the region around a vertex, then exactly one of those polygons contains that vertex. + * Points and polylines still do not contain any vertices. + */ + SEMI_OPEN, + + /** In the CLOSED model, all shapes contain their vertices (including points and polylines). */ + CLOSED; + + /** + * Returns true if the clipped portion of a shape 'clipped' from a cell with center 'cellCenter' + * contains the point 'p' according to this vertex model. + */ + public boolean shapeContains(S2Point cellCenter, S2ClippedShape clipped, S2Point p) { + boolean inside = clipped.containsCenter(); + int numEdges = clipped.numEdges(); + if (numEdges > 0) { + // Points and polylines can be ignored unless the vertex model is CLOSED. + S2Shape shape = clipped.shape(); + if (!shape.hasInterior() && this != S2VertexModel.CLOSED) { + return false; + } + // Test containment by drawing a line segment from the cell center to the + // given point and counting edge crossings. + EdgeCrosser crosser = new EdgeCrosser(cellCenter, p); + MutableEdge edge = new MutableEdge(); + boolean crossing; + for (int i = 0; i < numEdges; ++i) { + shape.getEdge(clipped.edge(i), edge); + switch (crosser.robustCrossing(edge.a, edge.b)) { + case -1: + // Disjoint, so advance to next edge. + continue; + case 1: + // Definitely crossing. + crossing = true; + break; + default: + // Shared vertex, test if we have a vertex crossing. + // For the OPEN and CLOSED models, check whether "p" is a vertex. + if (this != S2VertexModel.SEMI_OPEN && edge.isEndpoint(p)) { + return this == S2VertexModel.CLOSED; + } + crossing = S2EdgeUtil.vertexCrossing(cellCenter, p, edge.a, edge.b); + break; + } + inside ^= crossing; + } + } + return inside; + } + } + + private final Options options; + private final S2Iterator it; + + /** Constructs a semi-open contains-point query from the given iterator. */ + public S2ContainsPointQuery(S2ShapeIndex index) { + this(index, Options.SEMI_OPEN); + } + + /** Constructs a contains-point query from the given iterator, with the specified options. */ + public S2ContainsPointQuery(S2ShapeIndex index, Options options) { + this.it = index.iterator(); + this.options = options; + } + + /** Returns the options used to build this query. */ + public Options options() { + return options; + } + + /** + * Returns true if any shape in the given iterator contains {@code p} under the specified {@link + * S2VertexModel}. + */ + public boolean contains(S2Point p) { + if (!it.locate(p)) { + return false; + } + S2ShapeIndex.Cell cell = it.entry(); + S2Point center = it.center(); + int numClipped = cell.numShapes(); + for (int s = 0; s < numClipped; ++s) { + if (options.vertexModel().shapeContains(center, cell.clipped(s), p)) { + return true; + } + } + return false; + } + + /** + * Returns true if the given shape contains {@code p} under the specified {@link S2VertexModel}. + */ + public boolean shapeContains(S2Shape shape, S2Point p) { + if (!it.locate(p)) { + return false; + } + S2ClippedShape clipped = it.entry().findClipped(shape); + if (clipped == null) { + return false; + } + return options.vertexModel().shapeContains(it.center(), clipped, p); + } + + /** + * A visitor that receives each shape that contains a query point, returning true to continue + * receiving shapes or false to terminate early. + */ + interface ShapeVisitor extends Predicate {} + + /** + * Visits each shape that contains {@code p} under the specified {@link S2VertexModel} exactly + * once, and returns true, or terminates early and returns false if any invocation of {@link + * ShapeVisitor#apply(S2Shape)} returns false. + */ + boolean visitContainingShapes(S2Point p, ShapeVisitor visitor) { + // This function returns "false" only if the algorithm terminates early because the "visitor" + // function returned false. + if (!it.locate(p)) { + return true; + } + S2ShapeIndex.Cell cell = it.entry(); + S2Point center = it.center(); + int numClipped = cell.numShapes(); + for (int s = 0; s < numClipped; ++s) { + S2ClippedShape clipped = cell.clipped(s); + if (options.vertexModel().shapeContains(center, clipped, p) + && !visitor.apply(clipped.shape())) { + return false; + } + } + return true; + } + + /** A convenience function that returns all the shapes that contain {@code p}. */ + public Iterable getContainingShapes(final S2Point p) { + if (!it.locate(p)) { + return ImmutableList.of(); + } else { + // Must copy the iterator immediately, since the Iterable may not be used until after this.it + // has been repositioned. + final S2ShapeIndex.Cell cell = it.entry(); + final S2Point center = it.center(); + return new Iterable() { + @Override + public Iterator iterator() { + return new AbstractIterator() { + int i = 0; + + @Override + protected S2Shape computeNext() { + while (i < cell.numShapes()) { + S2ClippedShape clipped = cell.clipped(i++); + if (options.vertexModel().shapeContains(center, clipped, p)) { + return clipped.shape(); + } + } + return endOfData(); + } + }; + } + }; + } + } + + /** A visitor that receives each edge that has some query point p as an endpoint. */ + interface EdgeVisitor { + /** + * Returns true if the next edge should be received, or false to terminate early. + * + * @param shape The shape of this edge + * @param edgeId The edge ID in 'shape' that produced this edge + * @param a The startpoint of the edge + * @param b The endpoint of the edge + */ + boolean test(S2Shape shape, int edgeId, S2Point a, S2Point b); + } + + /** + * Visits each edge in the index that is incident to {@code p} exactly once, and returns true, or + * terminates early and returns false if {@code visitor} returns false. An "incident edge" is one + * where {@code p} is one of the edge endpoints. The visitor requires the edge endpoints, and so + * this method requires a temporary mutable edge to store edges in. + */ + boolean visitIncidentEdges(S2Point p, EdgeVisitor visitor, MutableEdge tmp) { + // This function returns "false" only if the algorithm terminates early because the "visitor" + // function returned false. + if (!it.locate(p)) { + return true; + } + S2ShapeIndex.Cell cell = it.entry(); + int numClipped = cell.numShapes(); + for (int s = 0; s < numClipped; s++) { + S2ClippedShape clipped = cell.clipped(s); + int numEdges = clipped.numEdges(); + if (numEdges == 0) { + continue; + } + S2Shape shape = clipped.shape(); + for (int i = 0; i < numEdges; i++) { + int edgeId = clipped.edge(i); + shape.getEdge(edgeId, tmp); + if (tmp.isEndpoint(p) && !visitor.test(shape, edgeId, tmp.a, tmp.b)) { + return false; + } + } + } + return true; + } +} diff --git a/core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/S2ContainsVertexQuery.java b/core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/S2ContainsVertexQuery.java new file mode 100644 index 0000000000..7d3afe4791 --- /dev/null +++ b/core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/S2ContainsVertexQuery.java @@ -0,0 +1,124 @@ +/* + * Copyright 2019 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.mogo.eagle.core.utilcode.geometry; + +import com.google.common.annotations.GwtCompatible; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * This class determines whether a polygon contains one of its vertices given the edges incident to + * that vertex. The result is +1 if the vertex is contained, -1 if it is not contained, and 0 if the + * incident edges consist of matched sibling pairs (in which case the result cannot be determined + * locally). + * + *

The {@link S2ContainsPointQuery.S2VertexModel#SEMI_OPEN "semi-open" boundary model} is used to + * define point containment. This means that if several polygons tile the region around a vertex, + * then exactly one of those polygons contains that vertex. + * + *

This class is not thread-safe. + */ +@GwtCompatible +public class S2ContainsVertexQuery { + private final S2Point target; + private final List outgoing = new ArrayList<>(); + private final List incoming = new ArrayList<>(); + + /** Creates a contains vertex query to determine containment of 'target'. */ + public S2ContainsVertexQuery(S2Point target) { + this.target = target; + } + + /** Adds an edge outgoing from 'target' to 'v'. */ + public void addOutgoing(S2Point v) { + outgoing.add(v); + } + + /** Adds an edge from 'v' incoming to 'target'. */ + public void addIncoming(S2Point v) { + incoming.add(v); + } + + /** + * Returns +1 if the vertex is contained, -1 if it is not contained, and 0 if the incident edges + * consisted of matched sibling pairs. + */ + public int containsSign() { + // Find the unmatched edge that is immediately clockwise from S2.ortho(target). + S2Point referenceDir = S2.ortho(target); + S2Point bestPoint = referenceDir; + int bestSum = 0; + // Merge outgoing and incoming lists together, computing a sum of each distinct vertex as the + // count of outgoing occurrences minus the count of incoming occurrences. + Collections.sort(outgoing); + Collections.sort(incoming); + for (int out = 0, in = 0; out < outgoing.size() || in < incoming.size(); ) { + S2Point v; + int direction; + if (out == outgoing.size()) { + v = incoming.get(in++); + direction = -1; + } else if (in == incoming.size()) { + v = outgoing.get(out++); + direction = 1; + } else { + S2Point outPoint = outgoing.get(out); + S2Point inPoint = incoming.get(in); + int diff = outPoint.compareTo(inPoint); + if (diff < 0) { + // The out point is smaller, so increase direction by each occurrence. + v = outPoint; + direction = count(outgoing, out); + out += direction; + } else if (diff > 0) { + // The in point is smaller, so decrease direction by each occurrence. + v = inPoint; + direction = -count(incoming, in); + in -= direction; + } else { + // The points are equal, so increase direction by the difference in counts. + v = outPoint; + int outSum = count(outgoing, out); + int inSum = count(incoming, in); + direction = outSum - inSum; + out += outSum; + in += inSum; + } + assert Math.abs(direction) <= 1; + } + if (direction == 0) { + // This is a "matched" edge. + continue; + } + if (S2Predicates.orderedCCW(referenceDir, bestPoint, v, target)) { + bestPoint = v; + bestSum = direction; + } + } + return bestSum; + } + + /** Returns the count of vertices equal to vertices[start]. */ + private static int count(List vertices, int start) { + S2Point v = vertices.get(start); + int sum = 1; + for (int i = start + 1; i < vertices.size() && vertices.get(i).equalsPoint(v); i++) { + sum++; + } + return sum; + } +} diff --git a/core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/S2ConvexHullQuery.java b/core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/S2ConvexHullQuery.java new file mode 100644 index 0000000000..88074e1595 --- /dev/null +++ b/core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/S2ConvexHullQuery.java @@ -0,0 +1,266 @@ +/* + * Copyright 2016 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.mogo.eagle.core.utilcode.geometry; + +import static com.mogo.eagle.core.utilcode.geometry.S2Predicates.sign; + +import com.google.common.annotations.GwtCompatible; +import com.google.common.base.Preconditions; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Iterables; +import com.google.common.collect.Lists; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; + +/** + * S2ConvexHullQuery builds the convex hull of any collection of points, polylines, loops, and + * polygons. It returns a single convex loop. + * + *

The convex hull is defined as the smallest convex region on the sphere that contains all of + * the input geometry. Recall that a region is "convex" if for every pair of points inside the + * region, the straight edge between them is also inside the region. In our case, a "straight" edge + * is a geodesic, i.e. the shortest path on the sphere between two points. + * + *

Containment of input geometry is defined as follows: + * + *

    + *
  • Each input loop and polygon is contained by the convex hull exactly (i.e., according to + * S2Polygon.contains(S2Polygon)). + *
  • Each input point is either contained by the convex hull or is a vertex of the convex hull. + * (Recall that S2Loops do not necessarily contain their vertices.) + *
  • For each input polyline, the convex hull contains all of its vertices according to the rule + * for points above. (The definition of convexity then ensures that the convex hull also + * contains the polyline edges.) + *
+ * + *

To use this class, call the add*() methods to add your input geometry, and then call + * getConvexHull(). Note that getConvexHull() does *not* reset the state; you can continue adding + * geometry if desired and compute the convex hull again. If you want to start from scratch, simply + * declare a new S2ConvexHullQuery object (they are cheap to create). + * + *

This class is not thread-safe. + */ +@GwtCompatible(serializable = true) +public final strictfp class S2ConvexHullQuery { + /** The length of edges to expand away from degenerate points to form a polygon. */ + private static final double OFFSET_FOR_SINGLE_POINT_LOOP = 1e-15; + + private final S2LatLngRect.Builder bound = S2LatLngRect.Builder.empty(); + private final List points = new ArrayList<>(); + + /** Adds a point to the input geometry. */ + public void addPoint(S2Point point) { + bound.addPoint(point); + points.add(point); + } + + /** Adds a polyline to the input geometry. */ + public void addPolyline(S2Polyline polyline) { + bound.union(polyline.getRectBound()); + points.addAll(polyline.vertices()); + } + + /** Adds a loop to the input geometry. */ + public void addLoop(S2Loop loop) { + // Only loops at depth 0 can contribute to the convex hull. + if (loop.depth() != 0) { + return; + } + bound.union(loop.getRectBound()); + if (loop.isEmptyOrFull()) { + // The empty and full loops consist of a single fake "vertex" that should + // not be added to our point collection. + return; + } + for (int i = 0; i < loop.numVertices(); ++i) { + points.add(loop.vertex(i)); + } + } + + /** Adds a polygon to the input geometry. */ + public void addPolygon(S2Polygon polygon) { + for (int i = 0; i < polygon.numLoops(); ++i) { + addLoop(polygon.loop(i)); + } + } + + /** + * Computes a bounding cap for the input geometry provided. + * + *

Note that this method does not clear the geometry; you can continue adding to it and call + * this method again if desired. + */ + public S2Cap getCapBound() { + // We keep track of a rectangular bound rather than a spherical cap because it is easy to + // compute a tight bound for a union of rectangles, whereas it is quite difficult to compute a + // tight bound around a union of caps. Also, polygons and polylines implement GetCapBound() in + // terms of GetRectBound() for this same reason, so it is much better to keep track of a + // rectangular bound as we go along and convert it at the end. + // + // TODO(user): We could compute an optimal bound by implementing Welzl's algorithm. However we + // would still need to have special handling of loops and polygons, since if a loop spans more + // than 180 degrees in any direction (i.e., if it contains two antipodal points), then it is not + // enough just to bound its vertices. In this case the only convex bounding cap is + // S2Cap.Full(), and the only convex bounding loop is the full loop. + return bound.getCapBound(); + } + + /** + * Computes the convex hull of the input geometry provided. + * + *

If there is no geometry, this method returns an empty loop containing no points (see + * S2Loop.isEmpty()). + * + *

If the geometry spans more than half of the sphere, this method returns a full loop + * containing the entire sphere (see S2Loop.isFull()). + * + *

If the geometry contains 1 or 2 points, or a single edge, this method returns a very small + * loop consisting of three vertices (which are a superset of the input vertices). + * + *

Note that this method does not clear the geometry; you can continue adding to it and call + * this method again if desired. + */ + public S2Loop getConvexHull() { + S2Cap cap = getCapBound(); + if (cap.height() >= 1) { + // The bounding cap is not convex. The current bounding cap implementation is not optimal, + // but nevertheless it is likely that the input geometry itself is not contained by any convex + // polygon. In any case, we need a convex bounding cap to proceed with the algorithm below + // (in order to construct a point "origin" that is definitely outside the convex hull). + return S2Loop.full(); + } + // This code implements Andrew's monotone chain algorithm, which is a simple variant of the + // Graham scan. Rather than sorting by x-coordinate, instead we sort the points in CCW order + // around an origin O such that all points are guaranteed to be on one side of some geodesic + // through O. This ensures that as we scan through the points, each new point can only belong + // at the end of the chain (i.e., the chain is monotone in terms of the angle around O from the + // starting point). + S2Point origin = cap.axis().ortho(); + Collections.sort(points, new OrderedCcwAround(origin)); + + // Remove duplicates. We need to do this before checking whether there are fewer than 3 points. + ImmutableSet uniquePoints = ImmutableSet.copyOf(points); + points.clear(); + points.addAll(uniquePoints); + + // Special cases for fewer than 3 points. + if (points.size() < 3) { + if (points.isEmpty()) { + return S2Loop.empty(); + } else if (points.size() == 1) { + return getSinglePointLoop(points.get(0)); + } else { + return getSingleEdgeLoop(points.get(0), points.get(1)); + } + } + + // Verify that all points lie within a 180 degree span around the origin. + Preconditions.checkState(sign(origin, points.get(0), Iterables.getLast(points)) >= 0); + + // Generate the lower and upper halves of the convex hull. Each half consists of the maximal + // subset of vertices such that the edge chain makes only left (CCW) turns. + List lower = getMonotoneChain(points); + List upper = getMonotoneChain(Lists.reverse(points)); + + // Remove the duplicate vertices and combine the chains. + Preconditions.checkState(lower.get(0).equals(Iterables.getLast(upper))); + Preconditions.checkState(Iterables.getLast(lower).equals(upper.get(0))); + lower.remove(lower.size() - 1); + upper.remove(upper.size() - 1); + lower.addAll(upper); + return new S2Loop(lower); + } + + /** A comparator for sorting points in CCW around a central point "center". */ + private static final class OrderedCcwAround implements Comparator { + private final S2Point center; + + OrderedCcwAround(S2Point center) { + this.center = center; + } + + @Override + public int compare(S2Point x, S2Point y) { + if (lessThan(x, y)) { + return -1; + } else if (lessThan(y, x)) { + return 1; + } else { + return 0; + } + } + + private boolean lessThan(S2Point x, S2Point y) { + // If X and Y are equal, this will return false (as desired). + return sign(center, x, y) > 0; + } + } + + private static List getMonotoneChain(List points) { + List output = new ArrayList<>(); + for (S2Point p : points) { + // Remove any points that would cause the chain to make a clockwise turn. + while (output.size() >= 2 + && sign(output.get(output.size() - 2), Iterables.getLast(output), p) <= 0) { + output.remove(output.size() - 1); + } + output.add(p); + } + return output; + } + + /** + * Constructs a 3-vertex polygon consisting of "p" and two nearby vertices. Note that contains(p) + * may be false for the resulting loop (see comments at top of file). + */ + private static S2Loop getSinglePointLoop(S2Point p) { + S2Point d0 = S2.ortho(p); + S2Point d1 = S2Point.crossProd(p, d0); + return new S2Loop( + ImmutableList.of( + p, + S2Point.normalize(S2Point.add(p, S2Point.mul(d0, OFFSET_FOR_SINGLE_POINT_LOOP))), + S2Point.normalize(S2Point.add(p, S2Point.mul(d1, OFFSET_FOR_SINGLE_POINT_LOOP))))); + } + + /** Construct a loop consisting of the two vertices and their midpoint. */ + private static S2Loop getSingleEdgeLoop(S2Point a, S2Point b) { + // If the points are exactly antipodal we return the full loop. + // + // Note that we could use the code below even in this case (which would return a zero-area loop + // that follows the edge AB), except that (1) the direction of AB is defined using symbolic + // perturbations and therefore is not predictable by ordinary users, and (2) S2Loop disallows + // anitpodal adjacent vertices and so we would need to use 4 vertices to define the degenerate + // loop. Note that the S2Loop antipodal vertex restriction is historical and now could easily + // be removed, however it would still have the problem that the edge direction is not easily + // predictable. + if (a.equalsPoint(b.neg())) { + return S2Loop.full(); + } + + // Construct a loop consisting of the two vertices and their midpoint. We + // use S2::Interpolate() to ensure that the midpoint is very close to + // the edge even when its endpoints nearly antipodal. + S2Loop loop = new S2Loop(ImmutableList.of(a, b, S2EdgeUtil.interpolate(0.5, a, b))); + // The resulting loop may be clockwise, so invert it if necessary. + loop.normalize(); + return loop; + } +} diff --git a/core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/S2Edge.java b/core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/S2Edge.java new file mode 100644 index 0000000000..e9b8ae4700 --- /dev/null +++ b/core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/S2Edge.java @@ -0,0 +1,119 @@ +/* + * Copyright 2011 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.mogo.eagle.core.utilcode.geometry; + +import com.google.common.annotations.GwtCompatible; +import com.google.common.base.Preconditions; +import java.io.Serializable; + +/** + * An abstract directed edge from one S2Point to another S2Point. + * + * @author kirilll@google.com (Kirill Levin) + */ +@GwtCompatible(serializable = true) +public final class S2Edge implements Serializable, S2Shape { + + private final S2Point start; + private final S2Point end; + + public S2Edge(S2Point start, S2Point end) { + this.start = start; + this.end = end; + } + + public S2Point getStart() { + return start; + } + + public S2Point getEnd() { + return end; + } + + @Override + public String toString() { + return "Edge: (" + start.toDegreesString() + " -> " + end.toDegreesString() + ")"; + } + + @Override + public int hashCode() { + return getStart().hashCode() - getEnd().hashCode(); + } + + @Override + public boolean equals(Object o) { + if (o == null || !(o instanceof S2Edge)) { + return false; + } + S2Edge other = (S2Edge) o; + return getStart().equalsPoint(other.getStart()) && getEnd().equalsPoint(other.getEnd()); + } + + @Override + public int numEdges() { + return 1; + } + + @Override + public void getEdge(int index, MutableEdge result) { + result.set(start, end); + } + + @Override + public boolean hasInterior() { + return false; + } + + @Override + public boolean containsOrigin() { + return false; + } + + @Override + public int numChains() { + return 1; + } + + @Override + public int getChainStart(int chainId) { + Preconditions.checkElementIndex(chainId, numChains()); + return 0; + } + + @Override + public int getChainLength(int chainId) { + Preconditions.checkElementIndex(chainId, numChains()); + return 1; + } + + @Override + public void getChainEdge(int chainId, int offset, MutableEdge result) { + Preconditions.checkElementIndex(offset, getChainLength(chainId)); + result.set(start, end); + } + + @Override + public S2Point getChainVertex(int chainId, int edgeOffset) { + Preconditions.checkElementIndex(edgeOffset, getChainLength(chainId)); + return edgeOffset == 0 ? start : end; + } + + @Override + public int dimension() { + return 1; + } +} diff --git a/core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/S2EdgeIndex.java b/core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/S2EdgeIndex.java new file mode 100644 index 0000000000..144079e34a --- /dev/null +++ b/core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/S2EdgeIndex.java @@ -0,0 +1,637 @@ +/* + * Copyright 2006 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.mogo.eagle.core.utilcode.geometry; + +import static com.mogo.eagle.core.utilcode.geometry.S2Projections.PROJ; + +import com.google.common.annotations.GwtCompatible; +import com.google.common.base.Preconditions; +import com.google.common.collect.Lists; +import com.google.common.collect.Sets; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Comparator; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +@GwtCompatible +public abstract strictfp class S2EdgeIndex { + /** + * Thicken the edge in all directions by roughly 1% of the edge length when thickenEdge is true. + */ + private static final double THICKENING = 0.01; + + /** The cell containing each edge, as given in the parallel array edges. */ + private long[] cells; + + /** The edge contained by each cell, as given in the parallel array cells. */ + private int[] edges; + + /** + * No cell strictly below this level appears in mapping. Initially leaf level, that's the minimum + * level at which we will ever look for test edges. + */ + private int minimumS2LevelUsed; + + /** Has the index been computed already? */ + private boolean indexComputed; + + /** Number of queries so far */ + private int queryCount; + + /** Empties the index in case it already contained something. */ + public void reset() { + minimumS2LevelUsed = S2CellId.MAX_LEVEL; + indexComputed = false; + queryCount = 0; + cells = null; + edges = null; + } + + /** + * Compares [cell1, edge1] to [cell2, edge2], by cell first and edge second. + * + * @return -1 if [cell1, edge1] is less than [cell2, edge2], 1 if [cell1, edge1] is greater than + * [cell2, edge2], 0 otherwise. + */ + private static final int compare(long cell1, int edge1, long cell2, int edge2) { + if (cell1 < cell2) { + return -1; + } else if (cell1 > cell2) { + return 1; + } else if (edge1 < edge2) { + return -1; + } else if (edge1 > edge2) { + return 1; + } else { + return 0; + } + } + + /** Computes the index (if it has not been previously done). */ + public final void computeIndex() { + if (indexComputed) { + return; + } + List cellList = Lists.newArrayList(); + List edgeList = Lists.newArrayList(); + for (int i = 0; i < getNumEdges(); ++i) { + S2Point from = edgeFrom(i); + S2Point to = edgeTo(i); + ArrayList cover = Lists.newArrayList(); + int level = getCovering(from, to, true, cover); + minimumS2LevelUsed = Math.min(minimumS2LevelUsed, level); + for (S2CellId cellId : cover) { + cellList.add(cellId.id()); + edgeList.add(i); + } + } + cells = new long[cellList.size()]; + edges = new int[edgeList.size()]; + for (int i = 0; i < cells.length; i++) { + cells[i] = cellList.get(i); + edges[i] = edgeList.get(i); + } + sortIndex(); + indexComputed = true; + } + + /** Sorts the parallel cells and edges arrays. */ + private void sortIndex() { + // create an array of indices and sort based on the values in the parallel + // arrays at each index + Integer[] indices = new Integer[cells.length]; + for (int i = 0; i < indices.length; i++) { + indices[i] = i; + } + Arrays.sort( + indices, + new Comparator() { + @Override + public int compare(Integer index1, Integer index2) { + return S2EdgeIndex.compare(cells[index1], edges[index1], cells[index2], edges[index2]); + } + }); + // copy the cells and edges in the order given by the sorted list of indices + long[] newCells = new long[cells.length]; + int[] newEdges = new int[edges.length]; + for (int i = 0; i < indices.length; i++) { + newCells[i] = cells[indices[i]]; + newEdges[i] = edges[indices[i]]; + } + // replace the cells and edges with the sorted arrays + cells = newCells; + edges = newEdges; + } + + public final boolean isIndexComputed() { + return indexComputed; + } + + /** + * Tell the index that we just received a new request for candidates. Useful to compute when to + * switch to quad tree. + */ + protected final void incrementQueryCount() { + ++queryCount; + } + + /** + * If the index hasn't been computed yet, looks at how much work has gone into iterating using the + * brute force method, and how much more work is planned as defined by 'cost'. If it were to have + * been cheaper to use a quad tree from the beginning, then compute it now. This guarantees that + * we will never use more than twice the time we would have used had we known in advance exactly + * how many edges we would have wanted to test. It is the theoretical best. + * + *

The value 'n' is the number of iterators we expect to request from this edge index. + * + *

If we have m data edges and n query edges, then the brute force cost is m * n * testCost + * where testCost is taken to be the cost of EdgeCrosser.robustCrossing, measured to be about 30ns + * at the time of this writing. + * + *

If we compute the index, the cost becomes: m * costInsert + n * costFind(m) + * + *

    + *
  • costInsert can be expected to be reasonably stable, and was measured at 1200ns with the + * BM_QuadEdgeInsertionCost benchmark. + *
  • costFind depends on the length of the edge . For m=1000 edges, we got timings ranging + * from 1ms (edge the length of the polygon) to 40ms. The latter is for very long query + * edges, and needs to be optimized. We will assume for the rest of the discussion that + * costFind is roughly 3ms. + *
+ * + *

When doing one additional query, the differential cost is m * testCost - costFind(m) With + * the numbers above, it is better to use the quad tree (if we have it) if m >= 100. + * + *

If m = 100, 30 queries will give m*n*testCost = m * costInsert = 100ms, while the marginal + * cost to find is 3ms. Thus, this is a reasonable thing to do. + */ + public final void predictAdditionalCalls(int n) { + if (indexComputed) { + return; + } + if (getNumEdges() > 100 && (queryCount + n) > 30) { + computeIndex(); + } + } + + /** Returns the number of edges in this index. */ + public abstract int getNumEdges(); + + /** Returns the starting vertex of the edge at offset {@code index}. */ + public abstract S2Point edgeFrom(int index); + + /** Returns the ending vertex of the edge at offset {@code index}. */ + public abstract S2Point edgeTo(int index); + + /** + * Return both vertices of the given {@code index} in one call. Can be overridden by some + * subclasses to more efficiently retrieve both edge points at once, which makes a difference in + * performance, especially for small loops. + */ + public S2Edge edgeFromTo(int index) { + return new S2Edge(edgeFrom(index), edgeTo(index)); + } + + /** + * Appends to "candidateCrossings" all edge references which may cross the given edge. This is + * done by covering the edge and then finding all references of edges whose coverings overlap this + * covering. Parent cells are checked level by level. Child cells are checked all at once by + * taking advantage of the natural ordering of S2CellIds. + */ + protected void findCandidateCrossings(S2Point a, S2Point b, List candidateCrossings) { + Preconditions.checkState(indexComputed); + ArrayList cover = Lists.newArrayList(); + getCovering(a, b, false, cover); + + // Edge references are inserted into the map once for each covering cell, so + // absorb duplicates here + Set uniqueSet = new HashSet(); + getEdgesInParentCells(cover, uniqueSet); + + // TODO(user): An important optimization for long query + // edges (Contains queries): keep a bounding cap and clip the query + // edge to the cap before starting the descent. + getEdgesInChildrenCells(a, b, cover, uniqueSet); + + candidateCrossings.clear(); + candidateCrossings.addAll(uniqueSet); + } + + /** + * Returns the smallest cell containing all four points, or {@link S2CellId#sentinel()} if they + * are not all on the same face. The points don't need to be normalized. + */ + private static S2CellId containingCell(S2Point pa, S2Point pb, S2Point pc, S2Point pd) { + S2CellId a = S2CellId.fromPoint(pa); + S2CellId b = S2CellId.fromPoint(pb); + S2CellId c = S2CellId.fromPoint(pc); + S2CellId d = S2CellId.fromPoint(pd); + + if (a.face() != b.face() || a.face() != c.face() || a.face() != d.face()) { + return S2CellId.sentinel(); + } + + while (!a.equals(b) || !a.equals(c) || !a.equals(d)) { + a = a.parent(); + b = b.parent(); + c = c.parent(); + d = d.parent(); + } + return a; + } + + /** + * Returns the smallest cell containing both points, or Sentinel if they are not all on the same + * face. The points don't need to be normalized. + */ + private static S2CellId containingCell(S2Point pa, S2Point pb) { + S2CellId a = S2CellId.fromPoint(pa); + S2CellId b = S2CellId.fromPoint(pb); + + if (a.face() != b.face()) { + return S2CellId.sentinel(); + } + + while (!a.equals(b)) { + a = a.parent(); + b = b.parent(); + } + return a; + } + + /** + * Computes a cell covering of an edge. Clears edgeCovering and returns the level of the s2 cells + * used in the covering (only one level is ever used for each call). + * + *

If thickenEdge is true, the edge is thickened and extended by 1% of its length. + * + *

It is guaranteed that no child of a covering cell will fully contain the covered edge. + */ + private int getCovering( + S2Point a, S2Point b, boolean thickenEdge, ArrayList edgeCovering) { + edgeCovering.clear(); + + // Selects the ideal s2 level at which to cover the edge, this will be the + // level whose S2 cells have a width roughly commensurate to the length of + // the edge. We multiply the edge length by 2*THICKENING to guarantee the + // thickening is honored (it's not a big deal if we honor it when we don't + // request it) when doing the covering-by-cap trick. + double edgeLength = a.angle(b); + int idealLevel = PROJ.minWidth.getMaxLevel(edgeLength * (1 + 2 * THICKENING)); + + S2CellId containingCellId; + if (!thickenEdge) { + containingCellId = containingCell(a, b); + } else { + if (idealLevel == S2CellId.MAX_LEVEL) { + // If the edge is tiny, instabilities are more likely, so we + // want to limit the number of operations. + // We pretend we are in a cell much larger so as to trigger the + // 'needs covering' case, so we won't try to thicken the edge. + containingCellId = (new S2CellId(0xFFF0)).parent(3); + } else { + S2Point pq = S2Point.mul(S2Point.minus(b, a), THICKENING); + S2Point ortho = + S2Point.mul(S2Point.normalize(S2Point.crossProd(pq, a)), edgeLength * THICKENING); + S2Point p = S2Point.minus(a, pq); + S2Point q = S2Point.add(b, pq); + // If p and q were antipodal, the edge wouldn't be lengthened, + // and it could even flip! This is not a problem because + // idealLevel != 0 here. The farther p and q can be is roughly + // a quarter Earth away from each other, so we remain + // Theta(THICKENING). + containingCellId = + containingCell( + S2Point.minus(p, ortho), + S2Point.add(p, ortho), + S2Point.minus(q, ortho), + S2Point.add(q, ortho)); + } + } + + // Best case: edge is fully contained in a cell that's not too big. + if (!containingCellId.equals(S2CellId.sentinel()) + && containingCellId.level() >= idealLevel - 2) { + edgeCovering.add(containingCellId); + return containingCellId.level(); + } + + if (idealLevel == 0) { + // Edge is very long, maybe even longer than a face width, so the + // trick below doesn't work. For now, we will add the whole S2 sphere. + // TODO(user): Do something a tad smarter (and beware of the + // antipodal case). + for (S2CellId cellid = S2CellId.begin(0); + !cellid.equals(S2CellId.end(0)); + cellid = cellid.next()) { + edgeCovering.add(cellid); + } + return 0; + } + // TODO(user): Check trick below works even when vertex is at + // interface + // between three faces. + + // Use trick as in S2PolygonBuilder.PointIndex.findNearbyPoint: + // Cover the edge by a cap centered at the edge midpoint, then cover + // the cap by four big-enough cells around the cell vertex closest to the + // cap center. + S2Point middle = S2Point.normalize(S2Point.div(S2Point.add(a, b), 2)); + int actualLevel = Math.min(idealLevel, S2CellId.MAX_LEVEL - 1); + S2CellId.fromPoint(middle).getVertexNeighbors(actualLevel, edgeCovering); + return actualLevel; + } + + /** + * Filters a list of entries down to the inclusive range defined by the given cells, in + * O(log N) time. + * + * @param cell1 One side of the inclusive query range. + * @param cell2 The other side of the inclusive query range. + * @return An array of length 2, containing the start/end indices. + */ + private int[] getEdges(long cell1, long cell2) { + // ensure cell1 <= cell2 + if (cell1 > cell2) { + long temp = cell1; + cell1 = cell2; + cell2 = temp; + } + // The binary search returns -N-1 to indicate an insertion point at index N, + // if an exact match cannot be found. Since the edge indices queried for are + // not valid edge indices, we will always get -N-1, so we immediately + // convert to N. + return new int[] { + -1 - binarySearch(cell1, Integer.MIN_VALUE), -1 - binarySearch(cell2, Integer.MAX_VALUE) + }; + } + + private int binarySearch(long cell, int edge) { + int low = 0; + int high = cells.length - 1; + while (low <= high) { + int mid = (low + high) >>> 1; + int cmp = compare(cells[mid], edges[mid], cell, edge); + if (cmp < 0) { + low = mid + 1; + } else if (cmp > 0) { + high = mid - 1; + } else { + return mid; + } + } + return -(low + 1); + } + + /** + * Adds to candidateCrossings all the edges present in any ancestor of any cell of cover, down to + * minimumS2LevelUsed. The cell->edge map is in the variable mapping. + */ + private void getEdgesInParentCells(List cover, Set candidateCrossings) { + // Find all parent cells of covering cells. + Set parentCells = Sets.newHashSet(); + for (S2CellId coverCell : cover) { + for (int parentLevel = coverCell.level() - 1; + parentLevel >= minimumS2LevelUsed; + --parentLevel) { + if (!parentCells.add(coverCell.parent(parentLevel))) { + break; // cell is already in => parents are too. + } + } + } + + // Put parent cell edge references into result. + for (S2CellId parentCell : parentCells) { + int[] bounds = getEdges(parentCell.id(), parentCell.id()); + for (int i = bounds[0]; i < bounds[1]; i++) { + candidateCrossings.add(edges[i]); + } + } + } + + /** Returns true if the edge and the cell (including boundary) intersect. */ + private static boolean edgeIntersectsCellBoundary(S2Point a, S2Point b, S2Cell cell) { + S2Point[] vertices = new S2Point[4]; + for (int i = 0; i < 4; ++i) { + vertices[i] = cell.getVertex(i); + } + for (int i = 0; i < 4; ++i) { + S2Point fromPoint = vertices[i]; + S2Point toPoint = vertices[(i + 1) % 4]; + if (S2EdgeUtil.lenientCrossing(a, b, fromPoint, toPoint)) { + return true; + } + } + return false; + } + + /** + * Appends to candidateCrossings the edges that are fully contained in an S2 covering of edge. The + * covering of edge used is initially cover, but is refined to eliminate quickly subcells that + * contain many edges but do not intersect with edge. + */ + private void getEdgesInChildrenCells( + S2Point a, S2Point b, List cover, Set candidateCrossings) { + // Put all edge references of (covering cells + descendant cells) into + // result. + // This relies on the natural ordering of S2CellIds. + S2Cell[] children = null; + while (!cover.isEmpty()) { + S2CellId cell = cover.remove(cover.size() - 1); + int[] bounds = getEdges(cell.rangeMin().id(), cell.rangeMax().id()); + if (bounds[1] - bounds[0] <= 16) { + for (int i = bounds[0]; i < bounds[1]; i++) { + candidateCrossings.add(edges[i]); + } + } else { + // Add cells at this level + bounds = getEdges(cell.id(), cell.id()); + for (int i = bounds[0]; i < bounds[1]; i++) { + candidateCrossings.add(edges[i]); + } + // Recurse on the children -- hopefully some will be empty. + if (children == null) { + children = new S2Cell[4]; + for (int i = 0; i < 4; ++i) { + children[i] = new S2Cell(); + } + } + new S2Cell(cell).subdivide(children); + for (S2Cell child : children) { + // TODO(user): Do the check for the four cells at once, + // as it is enough to check the four edges between the cells. At + // this time, we are checking 16 edges, 4 times too many. + // + // Note that given the guarantee of AppendCovering, it is enough + // to check that the edge intersect with the cell boundary as it + // cannot be fully contained in a cell. + if (edgeIntersectsCellBoundary(a, b, child)) { + cover.add(child.id()); + } + } + } + } + } + + /** + * Adds points where the edge index intersects the edge {@code [a0, a1]} to {@code intersections}. + * Each intersection is paired with a {@code t}-value indicating the fractional geodesic rotation + * of the intersection from 0 (at {@code a0}) to 1 (at {@code a1}). + * + * @param a0 First vertex of the edge to clip. + * @param a1 Second vertex of the edge to clip. + * @param addSharedEdges Whether an exact duplicate of {@code [a0, a1]} in the index should count + * as an intersection or not. + * @param intersections The resulting list of intersections. + */ + public void clipEdge( + final S2Point a0, + final S2Point a1, + boolean addSharedEdges, + Collection intersections) { + DataEdgeIterator it = new DataEdgeIterator(this); + S2EdgeUtil.EdgeCrosser crosser = new S2EdgeUtil.EdgeCrosser(a0, a1, a0); + S2Point b0 = null; + S2Point b1 = null; + for (it.getCandidates(a0, a1); it.hasNext(); it.next()) { + S2Point previous = b1; + S2Edge bEdge = edgeFromTo(it.index()); + b0 = bEdge.getStart(); + b1 = bEdge.getEnd(); + if (previous == null || !previous.equals(b0)) { + crosser.restartAt(b0); + } + int crossing = crosser.robustCrossing(b1); + if (crossing < 0) { + continue; + } + if (crossing > 0) { + // There is a proper edge crossing. + S2Point x = S2EdgeUtil.getIntersection(a0, a1, b0, b1); + double t = S2EdgeUtil.getDistanceFraction(x, a0, a1); + intersections.add(new ParametrizedS2Point(t, x)); + } else if (S2EdgeUtil.vertexCrossing(a0, a1, b0, b1)) { + // There is a crossing at one of the vertices. The basic rule is simple: + // if a0 equals one of the "b" vertices, the crossing occurs at t=0; + // otherwise, it occurs at t=1. + // + // This has the effect that when two symmetric edges are encountered (an + // edge an its reverse), neither one is included in the output. When two + // duplicate edges are encountered, both are included in the output. The + // "addSharedEdges" flag allows one of these two copies to be removed by + // changing its intersection parameter from 0 to 1. + double t = (a0.equals(b0) || a0.equals(b1)) ? 0 : 1; + if (!addSharedEdges && a1.equals(b1)) { + t = 1; + } + intersections.add(new ParametrizedS2Point(t, t == 0 ? a0 : a1)); + } + } + } + + /** + * An iterator on data edges that may cross a query edge (a,b). Create the iterator, call + * getCandidates(), then hasNext()/next() repeatedly. + * + *

The current edge in the iteration has index index(), goes between from() and to(). + */ + public static class DataEdgeIterator { + /** The structure containing the data edges. */ + private final S2EdgeIndex edgeIndex; + + /** + * Tells whether getCandidates() obtained the candidates through brute force iteration or using + * the quad tree structure. + */ + private boolean isBruteForce; + + /** Index of the current edge and of the edge before the last next() call. */ + private int currentIndex; + + /** Cache of edgeIndex.getNumEdges() so that hasNext() doesn't make an extra call */ + private int numEdges; + + /** + * All the candidates obtained by getCandidates() when we are using a quad-tree (i.e. + * isBruteForce = false). + */ + ArrayList candidates; + + /** + * Index within array above. We have: currentIndex = candidates.get(currentIndexInCandidates). + */ + private int currentIndexInCandidates; + + public DataEdgeIterator(S2EdgeIndex edgeIndex) { + this.edgeIndex = edgeIndex; + candidates = Lists.newArrayList(); + } + + /** + * Initializes the iterator to iterate over a set of candidates that may cross the edge (a,b). + */ + // TODO(user): Get a better API without the clumsy getCandidates(). + // Maybe edgeIndex.GetIterator()? + public void getCandidates(S2Point a, S2Point b) { + edgeIndex.predictAdditionalCalls(1); + isBruteForce = !edgeIndex.isIndexComputed(); + if (isBruteForce) { + edgeIndex.incrementQueryCount(); + currentIndex = 0; + numEdges = edgeIndex.getNumEdges(); + } else { + candidates.clear(); + edgeIndex.findCandidateCrossings(a, b, candidates); + currentIndexInCandidates = 0; + if (!candidates.isEmpty()) { + currentIndex = candidates.get(0); + } + } + } + + /** Index of the current edge in the iteration. */ + public int index() { + Preconditions.checkState(hasNext()); + return currentIndex; + } + + /** False if there are no more candidates; true otherwise. */ + public boolean hasNext() { + if (isBruteForce) { + return (currentIndex < numEdges); + } else { + return currentIndexInCandidates < candidates.size(); + } + } + + /** Iterate to the next available candidate. */ + public void next() { + Preconditions.checkState(hasNext()); + if (isBruteForce) { + ++currentIndex; + } else { + ++currentIndexInCandidates; + if (currentIndexInCandidates < candidates.size()) { + currentIndex = candidates.get(currentIndexInCandidates); + } + } + } + } +} diff --git a/core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/S2EdgeQuery.java b/core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/S2EdgeQuery.java new file mode 100644 index 0000000000..ce4c405631 --- /dev/null +++ b/core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/S2EdgeQuery.java @@ -0,0 +1,628 @@ +/* + * Copyright 2014 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.mogo.eagle.core.utilcode.geometry; + +import com.google.common.annotations.GwtCompatible; +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Preconditions; +import com.google.common.collect.Lists; +import com.google.common.collect.Maps; +import com.mogo.eagle.core.utilcode.geometry.S2EdgeUtil.FaceSegment; +import com.mogo.eagle.core.utilcode.geometry.S2Shape.MutableEdge; +import com.mogo.eagle.core.utilcode.geometry.S2ShapeIndex.S2ClippedShape; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.PriorityQueue; + +/** + * S2EdgeQuery is used to find edges or shapes that are crossed by an edge. If you need to query + * many edges, it is more efficient to declare a single S2EdgeQuery object and reuse it so that + * temporary storage does not need to be reallocated each time. + * + *

Here is an example showing how to index a set of polylines, and then find the polylines that + * are crossed by a given edge AB: + * + *

+ * void test(Collection polylines, S2Point a0, S2Point a1) {
+ * S2ShapeIndex index = new S2ShapeIndex();
+ * for (int i = 0; i < polylines.size(); ++i) {
+ * index.add(polylines[i]);
+ * }
+ * S2EdgeQuery query = new S2EdgeQuery(index);
+ * Map results = query.getCrossings(a, b);
+ * for (Map.Entry entry : results.entrySet()) {
+ * S2Polyline polyline = (S2Polyline) entry.getKey();
+ * for (Edges edges = entry.getValue(); !edges.isEmpty(); ) {
+ * int edge = edges.getNext();
+ * S2Point b0 = polyline.vertex(edge);
+ * S2Point b1 = polyline.vertex(edge + 1);
+ * // Guaranteed that each resulting edge is either a crossing or a degenerate crossing.
+ * assertTrue(S2EdgeUtil.robustCrossing(a0, a1, b0, b1) >= 0);
+ * }
+ * }
+ * }
+ * 
+ * + *

Note that if you need to query many edges, it is more efficient to declare a single + * S2EdgeQuery object and reuse it so that temporary storage does not need to be reallocated each + * time. + * + *

This class is not thread-safe. + */ +@GwtCompatible +public class S2EdgeQuery { + // TODO(eengle): Make this class public once getCandidates has been optimized to avoid boxing edge + // IDs. + private final S2ShapeIndex index; + /** Temporary list of cells that intersect the query edge AB. Used while processing a query. */ + private final List cells; + /** The following vectors are temporary storage used while processing a query. */ + private final S2Iterator iter; + /** An {@code Edges} implementation that contains no edges. */ + private static final Edges EMPTY_EDGE_LIST = + new Edges() { + @Override + public int nextEdge() { + return -1; + } + + @Override + public boolean isEmpty() { + return true; + } + }; + + /** Constructor from an {@link S2ShapeIndex}. */ + public S2EdgeQuery(S2ShapeIndex index) { + this.index = index; + this.iter = index.iterator(); + cells = Lists.newArrayList(); + } + + /** Returns edges from a given shape that either cross AB or share a vertex with AB. */ + public Edges getCrossings(S2Point a, S2Point b, S2Shape shape) { + return new CrossingFilter(shape, getCandidates(a, b, shape), a, b); + } + + /** Returns edges for each shape that either crosses AB or shares a vertex with AB. */ + public Map getCrossings(final S2Point a, final S2Point b) { + Map candidates = getCandidates(a, b); + Map results = Maps.newIdentityHashMap(); + for (Map.Entry entry : candidates.entrySet()) { + S2Shape shape = entry.getKey(); + Edges edges = entry.getValue(); + CrossingFilter filtered = new CrossingFilter(shape, edges, a, b); + if (!filtered.isEmpty()) { + results.put(entry.getKey(), filtered); + } + } + return results; + } + + /** + * Given a query edge AB and a shape {@code shape}, returns a superset of the edges of {@code + * shape} that intersect AB. Consider using {@link ShapeEdges} instead, if the shape has few + * enough edges. + */ + public Edges getCandidates(S2Point a, S2Point b, S2Shape shape) { + // For small loops it is faster to use brute force. The threshold below was determined using the + // benchmarks in the C++ S2Loop unit test. + // TODO(eengle) Update this value based on benchmarking in Java. + int maxBruteForceEdges = 27; + int maxEdges = shape.numEdges(); + if (maxEdges <= maxBruteForceEdges) { + return new ShapeEdges(shape.numEdges()); + } + + getCells(a, b); + + // Compute and return the 'Edges', using different 'Edges' implementations based on how many + // cells the query edge covers. + if (cells.isEmpty()) { + return EMPTY_EDGE_LIST; + } else if (cells.size() == 1) { + S2ClippedShape clippedShape = cells.get(0).findClipped(shape); + if (clippedShape == null || clippedShape.numEdges() == 0) { + return EMPTY_EDGE_LIST; + } else { + return new SimpleEdges(clippedShape); + } + } else { + MergedEdges edges = new MergedEdges(); + for (int i = 0; i < cells.size(); ++i) { + S2ClippedShape clippedShape = cells.get(i).findClipped(shape); + if (clippedShape != null && clippedShape.numEdges() != 0) { + edges.add(clippedShape); + } + } + return edges; + } + } + + /** + * Given a query edge AB, returns a map from the indexed shapes to a superset of the edges for + * each shape that intersect AB. Consider using {@link ShapeEdges} instead, if there is just one + * indexed shape with few enough edges. + * + *

CAVEAT: This method may return shapes that have an empty set of candidate edges, i.e. {@code + * result.get(shape).isEmpty() == true}. + */ + public Map getCandidates(S2Point a, S2Point b) { + // If there are only a few edges then it's faster to use brute force. We only bother with this + // optimization when there is a single shape, since then we can also use some tricks to avoid + // reallocating the edge map. + if (index.shapes.size() == 1) { + S2Shape shape = index.shapes.get(0); + Edges edges = getCandidates(a, b, shape); + if (edges.isEmpty()) { + return Collections.emptyMap(); + } else { + return Collections.singletonMap(shape, edges); + } + } + + getCells(a, b); + + // Compute and return the map. If the map is nonempty, use different 'Edges' implementations + // based on how many cells the query edge covers. + if (cells.isEmpty()) { + return Collections.emptyMap(); + } else if (cells.size() == 1) { + S2ShapeIndex.Cell cell = cells.get(0); + if (cell.numShapes() == 1) { + S2ClippedShape clippedShape = cell.clipped(0); + if (clippedShape.numEdges() == 0) { + return Collections.emptyMap(); + } else { + S2Shape shape = cell.clipped(0).shape(); + return Collections.singletonMap(shape, new SimpleEdges(clippedShape)); + } + } + Map edgeMap = Maps.newIdentityHashMap(); + for (int j = 0; j < cell.numShapes(); ++j) { + S2ClippedShape clippedShape = cell.clipped(j); + if (clippedShape.numEdges() > 0) { + S2Shape shape = clippedShape.shape(); + edgeMap.put(shape, new SimpleEdges(clippedShape)); + } + } + return edgeMap; + } else { + Map edgeMap = Maps.newIdentityHashMap(); + for (int i = 0; i < cells.size(); ++i) { + S2ShapeIndex.Cell cell = cells.get(i); + for (int j = 0; j < cell.numShapes(); ++j) { + S2ClippedShape clippedShape = cell.clipped(j); + if (clippedShape.numEdges() == 0) { + continue; + } + S2Shape shape = clippedShape.shape(); + MergedEdges edges = (MergedEdges) edgeMap.get(shape); + if (edges == null) { + edges = new MergedEdges(); + edgeMap.put(shape, edges); + } + edges.add(clippedShape); + } + } + return edgeMap; + } + } + + /** Sets cells to the set of index cells intersected by an edge AB. */ + private void getCells(S2Point a, S2Point b) { + cells.clear(); + FaceSegment[] segments = FaceSegment.allFaces(); + int numSegments = S2EdgeUtil.getFaceSegments(a, b, segments); + for (int i = 0; i < numSegments; ++i) { + // Optimization: rather than always starting the recursive subdivision at the top level face + // cell, we start at the smallest S2CellId that contains the edge (the "edge root cell"). This + // typically lets us skip quite a few levels of recursion, since most edges are short. + R2Rect edgeBound = R2Rect.fromPointPair(segments[i].a, segments[i].b); + S2PaddedCell pCell = new S2PaddedCell(S2CellId.fromFace(segments[i].face), 0); + S2CellId edgeRoot = pCell.shrinkToFit(edgeBound); + + // Now we need to determine how the edge root cell is related to the cells in the spatial + // index ('cells' in S2ShapeIndex.java). There are three cases: + // + // 1. edgeRoot is an index cell or is contained within an index cell. In this case, we only + // need to look at the contents of that cell. + // 2. edgeRoot is subdivided into one or more index cells. In this case we recursively + // subdivide to find the cells intersected by AB. + // 3. edgeRoot does not intersect any index cells. In this case there is nothing to do. + S2ShapeIndex.CellRelation relation = iter.locate(edgeRoot); + if (relation == S2ShapeIndex.CellRelation.INDEXED) { + // edgeRoot is an index cell or is contained by an index cell (case 1). + // assert (iter.id().contains(edgeRoot)); + cells.add(iter.entry()); + } else if (relation == S2ShapeIndex.CellRelation.SUBDIVIDED) { + // edgeRoot is subdivided into one or more index cells (case 2). We find the cells + // intersected by AB using recursive subdivision. + if (!edgeRoot.isFace()) { + pCell = new S2PaddedCell(edgeRoot, 0); + } + getCells(pCell, edgeBound, segments[i].a, segments[i].b); + } + } + } + + /** + * Convenience method for calling {@link #getCells(S2Point, R2Vector, S2Point, R2Vector, + * S2PaddedCell, List)}. + */ + public boolean getCells(S2Point a, S2Point b, S2PaddedCell root, List cells) { + R2Vector aVector = new R2Vector(); + R2Vector bVector = new R2Vector(); + return getCells(a, aVector, b, bVector, root, cells); + } + + /** + * Adds all cells to {@code cells} that might intersect the query edge from {@code a} to {@code b} + * and the cell {@code root}. The {@code aVector} and {@code bVector} parameters are cached R2 + * versions of the [A, B] edge projected onto the same cube face as {@code root}. + */ + @VisibleForTesting + boolean getCells( + S2Point a, + R2Vector aVector, + S2Point b, + R2Vector bVector, + S2PaddedCell root, + List cells) { + this.cells.clear(); + if (S2EdgeUtil.clipToFace(a, b, root.id().face(), aVector, bVector)) { + R2Rect edgeBound = R2Rect.fromPointPair(aVector, bVector); + if (root.bound().intersects(edgeBound)) { + getCells(root, edgeBound, aVector, bVector); + } + } + if (this.cells.isEmpty()) { + return false; + } + cells.addAll(this.cells); + return true; + } + + /** + * Computes the index cells intersected by the current edge that are descendants of {@code pCell}, + * and adds them to {@code cells}. + * + *

WARNING: This function is recursive with a maximum depth of 30. + */ + private void getCells(S2PaddedCell pCell, R2Rect edgeBound, R2Vector aVector, R2Vector bVector) { + S2CellId id = pCell.id(); + iter.seek(id.rangeMin()); + if (iter.done() || iter.compareTo(id.rangeMax()) > 0) { + // The index does not contain 'pCell' or any of its descendants. + return; + } + if (iter.compareTo(id) == 0) { + // The index contains this cell exactly. + cells.add(iter.entry()); + return; + } + + // Otherwise, split the edge among the four children of 'pCell'. + R2Vector center = pCell.middle().lo(); + if (edgeBound.x().hi() < center.x()) { + // Edge is entirely contained in the two left children. + clipVAxis(edgeBound, center.y(), 0, pCell, aVector, bVector); + } else if (edgeBound.x().lo() >= center.x()) { + // Edge is entirely contained in the two right children. + clipVAxis(edgeBound, center.y(), 1, pCell, aVector, bVector); + } else { + R2Rect[] childBounds = new R2Rect[2]; + splitUBound(edgeBound, center.x(), childBounds, aVector, bVector); + if (edgeBound.y().hi() < center.y()) { + // Edge is entirely contained in the two lower children. + getCells(pCell.childAtIJ(0, 0), childBounds[0], aVector, bVector); + getCells(pCell.childAtIJ(1, 0), childBounds[1], aVector, bVector); + } else if (edgeBound.y().lo() >= center.y()) { + // Edge is entirely contained in the two upper children. + getCells(pCell.childAtIJ(0, 1), childBounds[0], aVector, bVector); + getCells(pCell.childAtIJ(1, 1), childBounds[1], aVector, bVector); + } else { + // The edge bound spans all four children. The edge itself intersects at most three children + // (since no padding is being used). + clipVAxis(childBounds[0], center.y(), 0, pCell, aVector, bVector); + clipVAxis(childBounds[1], center.y(), 1, pCell, aVector, bVector); + } + } + } + + /** + * Given either the left (i = 0) or right (i = 1) side of a padded cell {@code pCell}, determines + * whether the current edge intersects the lower child, upper child, or both children, and calls + * getCells() recursively on those children. {@code center} is the v-coordinate at the center of + * {@code pCell}. + */ + private void clipVAxis( + R2Rect edgeBound, + double center, + int i, + S2PaddedCell pCell, + R2Vector aVector, + R2Vector bVector) { + if (edgeBound.y().hi() < center) { + // Edge is entirely contained in the lower child. + getCells(pCell.childAtIJ(i, 0), edgeBound, aVector, bVector); + } else if (edgeBound.y().lo() >= center) { + // Edge is entirely contained in the upper child. + getCells(pCell.childAtIJ(i, 1), edgeBound, aVector, bVector); + } else { + // The edge intersects both children. + R2Rect[] childBounds = new R2Rect[2]; + splitVBound(edgeBound, center, childBounds, aVector, bVector); + getCells(pCell.childAtIJ(i, 0), childBounds[0], aVector, bVector); + getCells(pCell.childAtIJ(i, 1), childBounds[1], aVector, bVector); + } + } + + /** + * Splits the current edge into two child edges at {@code u} and returns the bound for each child. + */ + private void splitUBound( + R2Rect edgeBound, double u, R2Rect[] childBounds, R2Vector aVector, R2Vector bVector) { + // See comments in S2ShapeIndex.clipUBound. + double v = + edgeBound + .y() + .clampPoint( + S2EdgeUtil.interpolateDouble(u, aVector.x, bVector.x, aVector.y, bVector.y)); + + // 'diag' indicates which diagonal of the bounding box is spanned by AB: It is 0 if AB has + // positive slope, and 1 if AB has negative slope. + int diag = ((aVector.x > bVector.x) != (aVector.y > bVector.y)) ? 1 : 0; + splitBound(edgeBound, 0, u, diag, v, childBounds); + } + + /** + * Splits the current edge into two child edges at {@code v} and returns the bound for each child. + */ + private void splitVBound( + R2Rect edgeBound, double v, R2Rect[] childBounds, R2Vector aVector, R2Vector bVector) { + double u = + edgeBound + .x() + .clampPoint( + S2EdgeUtil.interpolateDouble(v, aVector.y, bVector.y, aVector.x, bVector.x)); + int diag = ((aVector.x > bVector.x) != (aVector.y > bVector.y)) ? 1 : 0; + splitBound(edgeBound, diag, u, 0, v, childBounds); + } + + /** + * Splits the current edge into two child edges at the given point (u, v) and returns the bound + * for each child. {@code uEnd} and {@code vEnd} indicate which bound endpoints of child 1 will be + * updated. + */ + private void splitBound( + R2Rect edgeBound, int uEnd, double u, int vEnd, double v, R2Rect[] childBounds) { + childBounds[0] = new R2Rect(edgeBound); + childBounds[1] = new R2Rect(edgeBound); + if (uEnd == 0) { + childBounds[0].x().setHi(u); + childBounds[1].x().setLo(u); + } else { + childBounds[0].x().setLo(u); + childBounds[1].x().setHi(u); + } + if (vEnd == 0) { + childBounds[0].y().setHi(v); + childBounds[1].y().setLo(v); + } else { + childBounds[0].y().setLo(v); + childBounds[1].y().setHi(v); + } + // assert (!childBounds[0].isEmpty()); + // assert (edgeBound.contains(childBounds[0])); + // assert (!childBounds[1].isEmpty()); + // assert (edgeBound.contains(childBounds[1])); + } + + /** An iterator over the sorted unique edge IDs of a shape that may intersect some query edge. */ + public interface Edges { + /** Returns the next edge ID, or throws an exception if empty. */ + int nextEdge(); + + /** Returns true if there are no more edges. */ + boolean isEmpty(); + } + + /** An {@code Edges} implementation that includes all the edges of a clipped shape. */ + private static final class SimpleEdges implements Edges { + int index; + final S2ClippedShape shape; + + SimpleEdges(S2ClippedShape shape) { + index = 0; + this.shape = shape; + } + + @Override + public int nextEdge() { + Preconditions.checkState(!isEmpty(), "Cannot call nextEdge() on empty Edges."); + if (index == shape.numEdges()) { + return -1; + } else { + return shape.edge(index++); + } + } + + @Override + public boolean isEmpty() { + return index == shape.numEdges(); + } + } + + /** + * An {@code Edges} implementation optimized for merging edges from multiple S2ClippedShapes + * already in sorted order. + */ + private static final class MergedEdges implements Edges { + final PriorityQueue steppers = new PriorityQueue(); + /** + * The top of the priority queue (the stepper which currently has the least value for {@code + * currentEdge}). It is stored separately as an optimization, to avoid repeatedly adding and + * polling it from the top of the queue. + */ + Stepper top; + + /** Note: {@code shape} should have at least one edge. */ + public void add(S2ClippedShape shape) { + Stepper stepper = new Stepper(shape); + + if (top == null) { + top = stepper; + } else if (top.currentEdge() <= stepper.currentEdge()) { + steppers.add(stepper); + } else { + steppers.add(top); + top = stepper; + } + } + + @Override + public int nextEdge() { + Preconditions.checkState(!isEmpty(), "Cannot call nextEdge() on empty Edges."); + + // Store the value to be returned. + int nextEdge = top.currentEdge(); + removeFromPriorityQueue(nextEdge); + + top.index++; + if (top.index == top.clipped.numEdges()) { + // Exhausted current stepper. Get the next one. + top = steppers.isEmpty() ? null : steppers.poll(); + } else if (!steppers.isEmpty() && top.currentEdge() > steppers.peek().currentEdge()) { + // New top.currentEdge() is no longer the next sorted edge, so swap top with the head of the + // queue. + steppers.add(top); + top = steppers.poll(); + } + + return nextEdge; + } + + /** + * Updates the priority queue {@code steppers} so that no stepper in the queue will return + * {@code n} if {@code currentEdge()} is called on it. + */ + private void removeFromPriorityQueue(int n) { + while (!steppers.isEmpty() && steppers.peek().currentEdge() == n) { + Stepper stepper = steppers.poll(); + stepper.index++; + if (stepper.index != stepper.clipped.numEdges()) { + steppers.add(stepper); + } + } + } + + @Override + public boolean isEmpty() { + return top == null; + } + } + + /** An {@code Edges} that contains all the edges of a shape with the given number of edges. */ + public static final class ShapeEdges implements Edges { + private int edgeIndex = 0; + private final int numEdges; + + ShapeEdges(int numEdges) { + this.numEdges = numEdges; + } + + @Override + public int nextEdge() { + Preconditions.checkState(!isEmpty(), "Cannot call nextEdge() on empty Edges."); + return edgeIndex < numEdges ? edgeIndex++ : -1; + } + + @Override + public boolean isEmpty() { + return edgeIndex == numEdges; + } + } + + /** + * An Edges implementation that filters edges of a shape to those that intersect the edge AB or + * have an endpoint on either A or B. + */ + private static final class CrossingFilter implements Edges { + private final S2Shape shape; + private final Edges edges; + private final S2EdgeUtil.EdgeCrosser crosser; + private final MutableEdge edge = new MutableEdge(); + private int nextEdge = -1; + + CrossingFilter(S2Shape shape, Edges edges, S2Point a0, S2Point a1) { + this.shape = shape; + this.edges = edges; + this.crosser = new S2EdgeUtil.EdgeCrosser(a0, a1); + } + + @Override + public int nextEdge() { + checkPosition(); + int edge = nextEdge; + nextEdge = -1; + return edge; + } + + @Override + public boolean isEmpty() { + checkPosition(); + return nextEdge < 0; + } + + private void checkPosition() { + if (nextEdge >= 0) { + // Already at a valid position. + return; + } + // Advance until we discover a valid (robust crossing) position, or we run out of edges. + while (!edges.isEmpty()) { + int index = edges.nextEdge(); + shape.getEdge(index, edge); + if (crosser.robustCrossing(edge.a, edge.b) >= 0) { + nextEdge = index; + break; + } + } + } + } + + /** Tracks the current edge index within a clipped shape. */ + private static final class Stepper implements Comparable { + int index; + final S2ClippedShape clipped; + + Stepper(S2ClippedShape shape) { + this.index = 0; + this.clipped = shape; + } + + int currentEdge() { + return clipped.edge(index); + } + + @Override + public int compareTo(Stepper that) { + return Integer.compare(this.currentEdge(), that.currentEdge()); + } + } +} diff --git a/core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/S2EdgeUtil.java b/core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/S2EdgeUtil.java new file mode 100644 index 0000000000..730a2c1eaa --- /dev/null +++ b/core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/S2EdgeUtil.java @@ -0,0 +1,2576 @@ +/* + * Copyright 2006 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.mogo.eagle.core.utilcode.geometry; + +import static com.mogo.eagle.core.utilcode.geometry.S2Predicates.orderedCCW; +import static java.lang.Math.min; +import static java.lang.Math.sqrt; + +import com.google.common.annotations.GwtCompatible; +import com.google.common.base.Preconditions; + +/** + * This class contains various utility functions related to edges. It collects together common code + * that is needed to implement polygonal geometry such as polylines, loops, and general polygons. + * + */ +@GwtCompatible(serializable = false) +public strictfp class S2EdgeUtil { + /** + * IEEE floating-point operations have a maximum error of 0.5 ULPS (units in the last place). For + * double-precision numbers, this works out to 2**-53 (about 1.11e-16) times the magnitude of the + * result. It is possible to analyze the calculation done by getIntersection() and work out the + * worst-case rounding error. I have done a rough version of this, and my estimate is that the + * worst case distance from the intersection point X to the great circle through (a0, a1) is about + * 12 ULPS, or about 1.3e-15. This needs to be increased by a factor of (1/0.866) to account for + * the edgeSpliceFraction() in S2PolygonBuilder. Note that the maximum error measured by the + * unittest in 1,000,000 trials is less than 3e-16. + */ + public static final S1Angle DEFAULT_INTERSECTION_TOLERANCE = S1Angle.radians(1.5e-15); + + /** + * Threshold for small angles, that help lenientCrossing to determine whether two edges are likely + * to intersect. + */ + private static final double MAX_DET_ERROR = 1e-14; + + /** + * 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 {@link S2#robustCrossProd}, plus the error when + * projecting points onto a cube face. + */ + public static final double FACE_CLIP_ERROR_RADIANS = 3 * S2.DBL_EPSILON; + + /** + * The same angle as {@link #FACE_CLIP_ERROR_RADIANS}, 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. + */ + public static final double FACE_CLIP_ERROR_UV_DIST = 9 * S2.DBL_EPSILON; + + /** + * The same angle as {@link #FACE_CLIP_ERROR_RADIANS}, 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. + */ + public static final double FACE_CLIP_ERROR_UV_COORD = 9 * S2.M_SQRT1_2 * S2.DBL_EPSILON; + + /** + * The maximum error in IntersectRect. 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). + */ + public static final double INTERSECTS_RECT_ERROR_UV_DIST = 3 * S2.M_SQRT2 * S2.DBL_EPSILON; + + /** + * The maximum error in a clipped point's 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). + */ + public static final double EDGE_CLIP_ERROR_UV_COORD = 2.25 * S2.DBL_EPSILON; + + /** + * The maximum error between a clipped edge or boundary point and the corresponding exact result. + * It is equal to the error in a single coordinate because at most one coordinate is subject to + * error. + */ + public static final double EDGE_CLIP_ERROR_UV_DIST = 2.25 * S2.DBL_EPSILON; + + /** Max error allowed when checking if a loop boundary approximately intersects a target cell */ + public static final double MAX_CELL_EDGE_ERROR = + FACE_CLIP_ERROR_UV_COORD + INTERSECTS_RECT_ERROR_UV_DIST; + + /** + * INTERSECTION_ERROR can be set somewhat arbitrarily, because the algorithm uses more precision + * than necessary in order to achieve the specified error. The only strict requirement is that + * INTERSECTION_ERROR >= 2 * S2.DBL_EPSILON radians. However, using a larger error tolerance makes + * the algorithm more efficient because it reduces the number of cases where exact arithmetic is + * needed. + */ + public static final double INTERSECTION_ERROR = 8 * S2.DBL_EPSILON; + + /** Used to denote which point should be used when finding distances/points. */ + private enum ClosestPoint { + A0, + A1, + B0, + B1, + NONE + } + + /** + * Used to efficiently test a fixed edge AB against an edge chain. To use it, {@link + * #init(S2Point, S2Point) initialize} with the edge AB, and call {@link #robustCrossing(S2Point, + * S2Point)} or {@link #edgeOrVertexCrossing(S2Point, S2Point)} with each edge of the chain. + * + *

This class is not thread-safe. + */ + public static final class EdgeCrosser { + private S2Point a; + private S2Point b; + private S2Point aCrossB; + + /** Previous vertex in the vertex chain. */ + private S2Point c; + + /** The orientation of the triangle ACB, i.e. the orientation around the current vertex. */ + private int acb; + + /** + * The orientation of triangle BDA. This is used to return an extra value from + * robustCrossingInternal(). + */ + int bdaReturn; + + /** + * True if the tangents have been computed. To reduce the number of calls to {@link + * S2Predicates.Sign#expensive}, 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. + */ + private boolean hasTangents; + + /** Outward-facing tangent at A. */ + private S2Point aTangent; + + /** Outward-facing tangent at B. */ + private S2Point bTangent; + + /** + * Constructs an uninitialized edge crosser. Invoke {@link #init(S2Point, S2Point)} before + * calling the other methods. + */ + public EdgeCrosser() {} + + /** Convenience constructor that calls init() with the given fixed edge AB. */ + public EdgeCrosser(S2Point a, S2Point b) { + init(a, b); + } + + public void init(S2Point a, S2Point b) { + this.a = a; + this.b = b; + this.c = null; + this.aCrossB = S2Point.crossProd(a, b); + this.hasTangents = false; + } + + /** + * AB is the given fixed edge, and C is the first vertex of the vertex chain. Equivalent to + * using the two-arg constructor and calling restartAt(c). + */ + public EdgeCrosser(S2Point a, S2Point b, S2Point c) { + this(a, b); + restartAt(c); + } + + /** Call this method when your chain 'jumps' to a new place. */ + public void restartAt(S2Point c) { + this.c = c; + acb = -triage(aCrossB, c); + } + + /** + * Returns the sign of the determinant of the column matrix ABC, given the precomputed cross + * product AB. + */ + static int triage(S2Point ab, S2Point c) { + // maxDetError 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 = 0.5*DBL_EPSILON. Similarly, + // + // fl(B.C) = B.C + d where |d| <= (1.5*|B.C| + 1.5*|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| <= (2.5 + 2/sqrt(3)) * e + // + // which is about 3.6548 * e, or 1.8274 * DBL_EPSILON. + final double maxDetError = 1.8274 * S2.DBL_EPSILON; + // assert S2.isUnitLength(c); + + double det = ab.dotProd(c); + + // Double-check borderline cases in debug mode. + // assert Math.abs(det) <= maxDetError + // || Math.abs(det) >= 100 * maxDetError + // || det * expensiveSign(a, b, c) > 0; + + if (det >= maxDetError) { + return 1; + } + if (det <= -maxDetError) { + return -1; + } + return 0; + } + + /** + * This method is equivalent to calling the {@link #robustCrossing} function (defined below) on + * the edges AB and CD. It returns +1 if there is a crossing, -1 if there is no crossing, and 0 + * if two points from different edges are the same. Returns 0 or -1 if either edge is + * degenerate. As a side effect, it saves vertex D to be used as the next vertex C. + */ + public int robustCrossing(S2Point d) { + // 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. + int bda = triage(aCrossB, d); + if (this.acb == -bda && bda != 0) { + // 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). + this.c = d; + this.acb = -bda; + return -1; + } + this.bdaReturn = bda; + return robustCrossingInternal(d); + } + + /** + * As {@link #robustCrossing(S2Point)}, but restarts at {@code c} if that is not the previous + * endpoint. + */ + public int robustCrossing(S2Point c, S2Point d) { + if (this.c != c) { + // Comparison by reference may sometimes cause us to do slightly extra + // work, but the vast majority of the time if the points are equal by + // value, they are exactly the same reference as well. + restartAt(c); + } + return robustCrossing(d); + } + + /** + * This method is equivalent to the {@link #edgeOrVertexCrossing} method defined below. It is + * similar to {@link #robustCrossing}, but handles cases where two vertices are identical in a + * way that makes it easy to implement point-in-polygon containment tests. + */ + public boolean edgeOrVertexCrossing(S2Point d) { + // Copy c, since the reference may be replaced by robustCrossing(). + S2Point c2 = c; + + int crossing = robustCrossing(d); + if (crossing < 0) { + return false; + } + if (crossing > 0) { + return true; + } + + return vertexCrossing(a, b, c2, d); + } + + /** + * As {@link #edgeOrVertexCrossing(S2Point)}, but restarts at {@code c} if that is not the + * previous endpoint. + */ + public boolean edgeOrVertexCrossing(S2Point c, S2Point d) { + if (this.c != c) { + // Test by reference since the same value in different references is very rare. + restartAt(c); + } + return edgeOrVertexCrossing(d); + } + + /** + * 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). + */ + private int robustCrossingInternal(S2Point d) { + int result = robustCrossingInternal2(d); + this.c = d; + this.acb = -bdaReturn; + return result; + } + + private int robustCrossingInternal2(S2Point d) { + // 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 by computing 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 S2Predicates.expensiveSign(). + if (!hasTangents) { + S2Point norm = S2Point.normalize(S2.robustCrossProd(a, b)); + aTangent = S2Point.crossProd(a, norm); + bTangent = S2Point.crossProd(norm, b); + hasTangents = true; + } + + // 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)) * S2.DBL_EPSILON. The maximum error in each call to dotProd() below is + // S2.DBL_EPSILON. (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.) + final double kError = (1.5 + 1 / Math.sqrt(3)) * S2.DBL_EPSILON; + if ((c.dotProd(aTangent) > kError && d.dotProd(aTangent) > kError) + || (c.dotProd(bTangent) > kError && d.dotProd(bTangent) > kError)) { + return -1; + } + + // Otherwise, eliminate the cases where any two vertices are equal. (These cases could be + // handled in the code below, but since expensiveSign lives up to its name we would rather + // avoid calling it if possible.) + // + // These are the cases where two vertices from different edges are equal. + if (a.equalsPoint(c) || a.equalsPoint(d) || b.equalsPoint(c) || b.equalsPoint(d)) { + return 0; + } + + // These are the cases where an input edge is degenerate. Note that in most cases, if CD is + // degenerate then this method is not even called because acb and bda have different signs. + // That's why this method is documented to return either 0 or -1 when an input edge is + // degenerate. + if (a.equalsPoint(b) || c.equalsPoint(d)) { + return 0; + } + + // Otherwise it's time to break out the big guns. + if (acb == 0) { + acb = -S2Predicates.Sign.expensive(a, b, c, true); + assert acb != 0; + } + if (bdaReturn == 0) { + bdaReturn = S2Predicates.Sign.expensive(a, b, d, true); + assert bdaReturn != 0; + } + if (bdaReturn != acb) { + return -1; + } + + S2Point cCrossD = S2Point.crossProd(c, d); + int cbd = -sign(c, d, b, cCrossD); + assert cbd != 0; + if (cbd != acb) { + return -1; + } + + int dac = sign(c, d, a, cCrossD); + assert dac != 0; + return (dac == acb) ? 1 : -1; + } + + /** Helper that checks the sign of ABC, using a precomputed cross product for AxB. */ + private static int sign(S2Point a, S2Point b, S2Point c, S2Point aCrossB) { + // assert (isUnitLength(a) && isUnitLength(b) && isUnitLength(c)); + int ccw = triage(aCrossB, c); + if (ccw == 0) { + ccw = S2Predicates.Sign.expensive(a, b, c, true); + } + return ccw; + } + } + + /** + * This class computes 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 S2Points are + * converted to S2LatLngs. For example, this class guarantees that if L is a closed edge chain (a + * loop) such that the interior of the loop does not contain either pole, and P is any point such + * that L contains P, then the RectBounder of all edges in L will contain S2LatLng(P). + */ + public static class RectBounder { + /** The accumulated bounds, initially empty. */ + private S2LatLngRect.Builder builder = S2LatLngRect.Builder.empty(); + + /** The previous vertex in the chain. */ + private S2Point a; + + /** The corresponding latitude-longitude. */ + private S2LatLng aLatLng; + + /** Temporary storage for the longitude range spanned by AB. */ + private final S1Interval lngAB = new S1Interval(); + + /** Temporary storage for the latitude range spanned by AB. */ + private final R1Interval latAB = new R1Interval(); + + public RectBounder() {} + + /** + * This method is called to add each vertex to the chain. This method is much faster than {@link + * #addPoint(S2Point)}, since converting S2LatLng to an S2Point is much faster than the other + * way around.. + */ + public void addPoint(S2LatLng b) { + addPoint(b.toPoint(), b); + } + + /** + * This method is called to add each vertex to the chain. Prefer calling {@link + * #addPoint(S2LatLng)} if you have that type available. The point must be unit length. + */ + public void addPoint(S2Point b) { + addPoint(b, new S2LatLng(b)); + } + + /** + * Internal implementation of addPoint that takes both the point and latLng representation, by + * whichever path provided them, and expands the bounds accordingly. + */ + private void addPoint(S2Point b, S2LatLng bLatLng) { + // assert (S2.isUnitLength(b)); + if (builder.isEmpty()) { + builder.addPoint(bLatLng); + } else { + // First compute the cross product N = A x B robustly. This is the normal + // to the great circle through A and B. We don't use S2.RobustCrossProd() + // since that method returns an arbitrary vector orthogonal to A if the two + // vectors are proportional, and we want the zero vector in that case. + // N = 2 * (A x B) + S2Point n = S2Point.crossProd(S2Point.sub(a, b), S2Point.add(a, 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 * DBL_EPSILON, and it + // is desirable to have the total error be a multiple of DBL_EPSILON, we + // have chosen the maximum error threshold here to be 3.84 * DBL_EPSILON. + // It is possible to show that the error is less than this when + // + // n.norm() >= 8 * sqrt(3) / (3.84 - 0.5 - sqrt(3)) * DBL_EPSILON + // = 1.91346e-15 (about 8.618 * DBL_EPSILON) + double nNorm = n.norm(); + if (nNorm < 1.91346e-15) { + // A and B are either nearly identical or nearly antipodal (to within + // 4.309 * DBL_EPSILON, or about 6 nanometers on the earth's surface). + if (a.dotProd(b) < 0) { + // The two points are nearly antipodal. The easiest solution is to + // assume that the edge between A and B could go in any direction + // around the sphere. + builder.setFull(); + } else { + // The two points are nearly identical (to within 4.309 * DBL_EPSILON). + // In this case we can just use the bounding rectangle of the points, + // since after the expansion done by GetBound() this rectangle is + // guaranteed to include the (lat,lng) values of all points along AB. + builder.union(S2LatLngRect.fromPointPair(aLatLng, bLatLng)); + } + } else { + // Compute the longitude range spanned by AB. + lngAB.initFromPointPair(aLatLng.lng().radians(), bLatLng.lng().radians()); + if (lngAB.getLength() >= S2.M_PI - 2 * S2.DBL_EPSILON) { + // The points lie on nearly opposite lines of longitude to within the + // maximum error of the calculation. (Note that this test relies on + // the fact that M_PI is slightly less than the true value of Pi, and + // that representable values near M_PI are 2 * DBL_EPSILON apart.) + // The easiest solution is to assume that AB could go on either side + // of the pole. + lngAB.setFull(); + } + + // Next we compute the latitude range spanned by the edge AB. We start + // with the range spanning the two endpoints of the edge: + latAB.initFromPointPair(aLatLng.lat().radians(), bLatLng.lat().radians()); + + // 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. + S2Point m = S2Point.crossProd(n, S2Point.Z_POS); + double mDotA = m.dotProd(a); + double mDotB = m.dotProd(b); + + // We want to test the signs of "mDotA" and "mDotB", 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)) * DBL_EPSILON * nNorm + 8 * sqrt(3) * (DBL_EPSILON**2) + // = 6.06638e-16 * nNorm + 6.83174e-31 + + double mError = 6.06638e-16 * nNorm + 6.83174e-31; + if (mDotA * mDotB < 0 || Math.abs(mDotA) <= mError || Math.abs(mDotB) <= 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 * DBL_EPSILON) + // - 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 * DBL_EPSILON + // individually, but it is possible to show by a more complex analysis + // that together they can add up to at most 1.16 * DBL_EPSILON, for a + // total error of 5 * DBL_EPSILON. + // + // We add 3 * DBL_EPSILON to the bound here, and getBound() will pad + // the bound by another 2 * DBL_EPSILON. + double maxLat = + Math.min( + S2.M_PI_2, + 3 * S2.DBL_EPSILON + + Math.atan2( + Math.sqrt(n.getX() * n.getX() + n.getY() * n.getY()), + Math.abs(n.getZ()))); + + // 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. + double latBudget = 2 * Math.asin(0.5 * S2Point.sub(a, b).norm() * Math.sin(maxLat)); + double maxDelta = 0.5 * (latBudget - latAB.getLength()) + S2.DBL_EPSILON; + + // 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 (mDotA <= mError && mDotB >= -mError) { + latAB.setHi(Math.min(maxLat, latAB.hi() + maxDelta)); + } + if (mDotB <= mError && mDotA >= -mError) { + latAB.setLo(Math.max(-maxLat, latAB.lo() - maxDelta)); + } + } + builder.union(new S2LatLngRect(latAB, lngAB)); + } + } + a = b; + aLatLng = bLatLng; + } + + /** + * 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 S2LatLng coordinates of all S2Points contained by the loop. + */ + public S2LatLngRect getBound() { + // To save time, we ignore numerical errors in the computed S2LatLngs while + // accumulating the bounds and then account for them here. + // + // S2LatLng(S2Point) has a maximum error of 0.955 * S2.DBL_EPSILON in latitude. + // In the worst case, we might have rounded "inwards" when computing the + // bound and "outwards" when computing the latitude of a contained point P, + // therefore we expand the latitude bounds by 2 * S2.DBL_EPSILON in each + // direction. (A more complex analysis shows that 1.5 * S2.DBL_EPSILON is + // enough, but the expansion amount should be a multiple of S2.DBL_EPSILON in + // order to avoid rounding errors during the expansion itself.) + // + // S2LatLng(S2Point) has a maximum error of S2.DBL_EPSILON in longitude, which + // is simply the maximum rounding error for results in the range [-Pi, Pi]. + // This is true because the Gnu implementation of atan2() comes from the IBM + // Accurate Mathematical Library, which implements correct rounding for this + // intrinsic (i.e., it returns the infinite precision result rounded to the + // nearest representable value, with ties rounded to even values). This + // implies that we don't need to expand the longitude bounds at all, since + // we only guarantee that the bound contains the *rounded* latitudes of + // contained points. The *true* latitudes of contained points may lie up to + // S2.DBL_EPSILON outside of the returned bound. + + S2LatLng expansion = S2LatLng.fromRadians(2 * S2.DBL_EPSILON, 0); + return builder.build().expanded(expansion).polarClosure(); + } + + /** + * Returns the maximum error in getBound() provided that the result does not include either + * pole. It is only to be used for testing purposes (e.g., by passing it to {@link + * S2LatLngRect#approxEquals}). + */ + static S2LatLng maxErrorForTests() { + // The maximum error in the latitude calculation is + // 3.84 * DBL_EPSILON for the robustCrossProd calculation + // 0.96 * DBL_EPSILON for the latitude() calculation + // 5 * DBL_EPSILON added by AddPoint/GetBound to compensate for error + // ------------------ + // 9.80 * DBL_EPSILON maximum error in result + // + // The maximum error in the longitude calculation is DBL_EPSILON. GetBound + // does not do any expansion because this isn't necessary in order to + // bound the *rounded* longitudes of contained points. + return S2LatLng.fromRadians(10 * S2.DBL_EPSILON, 1 * S2.DBL_EPSILON); + } + + /** + * Expand a bound returned by getBound() so that it is guaranteed to contain the bounds of any + * subregion whose bounds are computed using this class. For example, consider a loop L that + * defines a square. GetBound() ensures that if a point P is contained by this square, then + * S2LatLng(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 {@code L.contains(S)}, then {@code + * expandForSubregions(RectBound(L)).contains(RectBound(S))}. + */ + static S2LatLngRect expandForSubregions(S2LatLngRect bound) { + // Empty bounds don't need expansion. + if (bound.isEmpty()) { + return bound; + } + + // First we need to check whether the bound B contains any nearly-antipodal + // points (to within 4.309 * S2.DBL_EPSILON). If so then we need to return + // S2LatLngRect.full(), 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 * DBL_EPSILON. + + // "lngGap" is a lower bound on the longitudinal distance between B and its + // reflection B'. (2.5 * S2.DBL_EPSILON is the maximum combined error of the + // endpoint longitude calculations and the GetLength() call.) + double lngGap = Math.max(0.0, S2.M_PI - bound.lng().getLength() - 2.5 * S2.DBL_EPSILON); + + // "minAbsLat" is the minimum distance from B to the equator (if zero or + // negative, then B straddles the equator). + double minAbsLat = Math.max(bound.lat().lo(), -bound.lat().hi()); + + // "latGap1" and "latGap2" measure the minimum distance from B to the + // south and north poles respectively. + double latGap1 = S2.M_PI_2 + bound.lat().lo(); + double latGap2 = S2.M_PI_2 - bound.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 * S2.DBL_EPSILON), 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 * S2.DBL_EPSILON + // ~= 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 S2LatLngRect.full(); + } + } else if (lngGap >= S2.M_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 = latGap1 + // and y = latGap2, and angle Z >= Pi/2 between them. We then have + // + // z >= sqrt(x^2 + y^2) >= (x + y) / sqrt(2) + // + // Unlike the case above, "latGap1" and "latGap2" are not lower bounds + // (because of the extra addition operation, and because M_PI_2 is not + // exactly equal to Pi/2); they can exceed their true values by up to + // 0.75 * S2.DBL_EPSILON. Putting this all together, the region may + // contain nearly antipodal points only if + // + // latGap1 + latGap2 < (sqrt(2) * 4.309 + 1.5) * S2.DBL_EPSILON + // ~= 1.687e-15 + if (latGap1 + latGap2 < 1.687e-15) { + return S2LatLngRect.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(d_min) / sin(lngGap) + // sin(d_min) = sin(maxLatGap) * sin(lngGap) + // + // where "maxLatGap" = max(latGap1, latGap2) and "d_min" 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 * S2.DBL_EPSILON, we want to test whether + // + // maxLatGap * lngGap < (4.309 + 0.75) * (Pi/2) * S2.DBL_EPSILON + // ~= 1.765e-15 + if (Math.max(latGap1, latGap2) * lngGap < 1.765e-15) { + return S2LatLngRect.full(); + } + } + + // Next we need to check whether the subregion might contain any edges that + // span (M_PI - 2 * S2.DBL_EPSILON) radians or more in longitude, since AddPoint + // sets the longitude bound to Full() in that case. This corresponds to + // testing whether (lngGap <= 0) in "lng_expansion" below. + + // Otherwise, the maximum latitude error in AddPoint is 4.8 * S2.DBL_EPSILON. + // 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 + // S2.DBL_EPSILON.) + // + // For longitude, we rely on the fact that atan2 is correctly rounded and + // therefore no additional bounds expansion is necessary. + + double latExpansion = 9 * S2.DBL_EPSILON; + double lngExpansion = (lngGap <= 0) ? S2.M_PI : 0; + return bound.expanded(S2LatLng.fromRadians(latExpansion, lngExpansion)).polarClosure(); + } + } + + /** + * The purpose of this class is to find edges that intersect a given XYZ bounding box. It can be + * used as an efficient rejection test when attempting to find edges that intersect a given + * region. It accepts a vertex chain v0, v1, v2, ... and returns a boolean value indicating + * whether each edge intersects the specified bounding box. + * + *

We use XYZ intervals instead of something like longitude intervals because it is cheap to + * collect from S2Point lists and any slicing strategy should give essentially equivalent results. + * See S2Loop for an example of use. + */ + public static class XYZPruner { + private S2Point lastVertex; + + // The region to be tested against. + private boolean boundSet; + private double xmin; + private double ymin; + private double zmin; + private double xmax; + private double ymax; + private double zmax; + private double maxDeformation; + + public XYZPruner() { + boundSet = false; + } + + /** + * Accumulate a bounding rectangle from provided edges. + * + * @param from start of edge + * @param to end of edge. + */ + public void addEdgeToBounds(S2Point from, S2Point to) { + if (!boundSet) { + boundSet = true; + xmin = xmax = from.x; + ymin = ymax = from.y; + zmin = zmax = from.z; + } + xmin = Math.min(xmin, Math.min(to.x, from.x)); + ymin = Math.min(ymin, Math.min(to.y, from.y)); + zmin = Math.min(zmin, Math.min(to.z, from.z)); + xmax = Math.max(xmax, Math.max(to.x, from.x)); + ymax = Math.max(ymax, Math.max(to.y, from.y)); + zmax = Math.max(zmax, Math.max(to.z, from.z)); + + // Because our arcs are really geodesics on the surface of the earth + // an edge can have intermediate points outside the xyz bounds implicit + // in the end points. Based on the length of the arc we compute a + // generous bound for the maximum amount of deformation. For small edges + // it will be very small but for some large arcs (ie. from (1N,90W) to + // (1N,90E) the path can be wildly deformed. I did a bunch of + // experiments with geodesics to get safe bounds for the deformation. + double approxArcLen = + Math.abs(from.x - to.x) + Math.abs(from.y - to.y) + Math.abs(from.z - to.z); + if (approxArcLen < 0.025) { // less than 2 degrees + maxDeformation = Math.max(maxDeformation, approxArcLen * 0.0025); + } else if (approxArcLen < 1.0) { // less than 90 degrees + maxDeformation = Math.max(maxDeformation, approxArcLen * 0.11); + } else { + maxDeformation = approxArcLen * 0.5; + } + } + + public void setFirstIntersectPoint(S2Point v0) { + xmin = xmin - maxDeformation; + ymin = ymin - maxDeformation; + zmin = zmin - maxDeformation; + xmax = xmax + maxDeformation; + ymax = ymax + maxDeformation; + zmax = zmax + maxDeformation; + this.lastVertex = v0; + } + + /** + * Returns true if the edge going from the last point to this point passes through the pruner + * bounding box, otherwise returns false. So the method returns false if we are certain there is + * no intersection, but it may return true when there turns out to be no intersection. + */ + public boolean intersects(S2Point v1) { + boolean result = true; + + if ((v1.x < xmin && lastVertex.x < xmin) || (v1.x > xmax && lastVertex.x > xmax)) { + result = false; + } else if ((v1.y < ymin && lastVertex.y < ymin) || (v1.y > ymax && lastVertex.y > ymax)) { + result = false; + } else if ((v1.z < zmin && lastVertex.z < zmin) || (v1.z > zmax && lastVertex.z > zmax)) { + result = false; + } + + lastVertex = v1; + return result; + } + } + + /** + * The purpose of this class is to find edges that intersect a given longitude interval. It can be + * used as an efficient rejection test when attempting to find edges that intersect a given + * region. It accepts a vertex chain v0, v1, v2, ... and returns a boolean value indicating + * whether each edge intersects the specified longitude interval. + * + *

This class is not currently used as the XYZPruner is preferred for S2Loop, but this should + * be usable in similar circumstances. Be wary of the cost of atan2() in conversions from S2Point + * to longitude! + */ + public static class LongitudePruner { + // The interval to be tested against. + private S1Interval interval; + + // The longitude of the next v0. + private double lng0; + + /** + * 'interval' is the longitude interval to be tested against, and 'v0' is the first vertex of + * edge chain. + */ + public LongitudePruner(S1Interval interval, S2Point v0) { + this.interval = interval; + this.lng0 = S2LatLng.longitude(v0).radians(); + } + + /** + * Returns true if the edge (v0, v1) intersects the given longitude interval, and then saves + * 'v1' to be used as the next 'v0'. + */ + public boolean intersects(S2Point v1) { + double lng1 = S2LatLng.longitude(v1).radians(); + boolean result = interval.intersects(S1Interval.fromPointPair(lng0, lng1)); + lng0 = lng1; + return result; + } + } + + /** Spatial containment relationships between a wedge A to another wedge B. */ + enum WedgeRelation { + /** A and B are equal. */ + WEDGE_EQUALS, + /** A is a strict superset of B. */ + WEDGE_PROPERLY_CONTAINS, + /** A is a strict subset of B. */ + WEDGE_IS_PROPERLY_CONTAINED, + /** A-B, B-A, and A intersect B are non-empty. */ + WEDGE_PROPERLY_OVERLAPS, + /** A and B are disjoint. */ + WEDGE_IS_DISJOINT, + } + + /** Returns the relation from wedge A to B. */ + public static WedgeRelation getWedgeRelation( + S2Point a0, S2Point ab1, S2Point a2, S2Point b0, S2Point b2) { + // There are 6 possible edge orderings at a shared vertex (all + // of these orderings are circular, i.e. abcd == bcda): + // + // (1) a2 b2 b0 a0: A contains B + // (2) a2 a0 b0 b2: B contains A + // (3) a2 a0 b2 b0: A and B are disjoint + // (4) a2 b0 a0 b2: A and B intersect in one wedge + // (5) a2 b2 a0 b0: A and B intersect in one wedge + // (6) a2 b0 b2 a0: A and B intersect in two wedges + // + // We do not distinguish between 4, 5, and 6. + // We pay extra attention when some of the edges overlap. When edges + // overlap, several of these orderings can be satisfied, and we take + // the most specific. + if (a0.equalsPoint(b0) && a2.equalsPoint(b2)) { + return WedgeRelation.WEDGE_EQUALS; + } + + if (orderedCCW(a0, a2, b2, ab1)) { + // The cases with this vertex ordering are 1, 5, and 6, + // although case 2 is also possible if a2 == b2. + if (orderedCCW(b2, b0, a0, ab1)) { + return WedgeRelation.WEDGE_PROPERLY_CONTAINS; + } + + // We are in case 5 or 6, or case 2 if a2 == b2. + if (a2.equalsPoint(b2)) { + return WedgeRelation.WEDGE_IS_PROPERLY_CONTAINED; + } else { + return WedgeRelation.WEDGE_PROPERLY_OVERLAPS; + } + } + + // We are in case 2, 3, or 4. + if (orderedCCW(a0, b0, b2, ab1)) { + return WedgeRelation.WEDGE_IS_PROPERLY_CONTAINED; + } + if (orderedCCW(a0, b0, a2, ab1)) { + return WedgeRelation.WEDGE_IS_DISJOINT; + } else { + return WedgeRelation.WEDGE_PROPERLY_OVERLAPS; + } + } + + /** + * Wedge processors are used to determine the local relationship between two polygons that share a + * common vertex. + * + *

Given an edge chain (x0, x1, x2), the wedge at x1 is the region to the left of the edges. + * More precisely, it is the set of all rays from x1x0 (inclusive) to x1x2 (exclusive) in the + * *clockwise* direction. + * + *

Implementations compare two *non-empty* wedges that share the same middle vertex: A=(a0, + * ab1, a2) and B=(b0, ab1, b2). + * + *

All wedge processors require that a0 != a2 and b0 != b2. Other degenerate cases (such as a0 + * == b2) are handled as expected. The parameter "ab1" denotes the common vertex a1 == b1. + */ + public interface WedgeProcessor { + /** + * A wedge processor's test method accepts two edge chains A=(a0,a1,a2) and B=(b0,b1,b2) where + * a1==b1, and returns either -1, 0, or 1 to indicate the relationship between the region to the + * left of A and the region to the left of B. + */ + int test(S2Point a0, S2Point ab1, S2Point a2, S2Point b0, S2Point b2); + } + + /** + * Returns true if wedge A contains wedge B. Equivalent to but faster than {@code + * getWedgeRelation() == WEDGE_PROPERLY_CONTAINS || WEDGE_EQUALS}. + */ + public static class WedgeContains implements WedgeProcessor { + /** + * Given two edge chains, this function returns +1 if the region to the left of A contains the + * region to the left of B, and 0 otherwise. + */ + @Override + public int test(S2Point a0, S2Point ab1, S2Point a2, S2Point b0, S2Point b2) { + // For A to contain B (where each loop interior is defined to be its left + // side), the CCW edge order around ab1 must be a2 b2 b0 a0. We split + // this test into two parts that test three vertices each. + return orderedCCW(a2, b2, b0, ab1) && orderedCCW(b0, a0, a2, ab1) ? 1 : 0; + } + } + + /** + * Returns true if wedge A intersects wedge B. Equivalent to but faster than {@code + * getWedgeRelation() != WEDGE_IS_DISJOINT}. + */ + public static class WedgeIntersects implements WedgeProcessor { + /** + * Given two edge chains (see WedgeRelation above), this function returns -1 if the region to + * the left of A intersects the region to the left of B, and 0 otherwise. Note that regions are + * defined such that points along a boundary are contained by one side or the other, not both. + * So for example, if A,B,C are distinct points ordered CCW around a vertex O, then the wedges + * BOA, AOC, and COB do not intersect. + */ + @Override + public int test(S2Point a0, S2Point ab1, S2Point a2, S2Point b0, S2Point b2) { + // For A not to intersect B (where each loop interior is defined to be + // its left side), the CCW edge order around ab1 must be a0 b2 b0 a2. + // Note that it's important to write these conditions as negatives + // (!OrderedCCW(a,b,c,o) rather than Ordered(c,b,a,o)) to get correct + // results when two vertices are the same. + return (orderedCCW(a0, b2, b0, ab1) && orderedCCW(b0, a2, a0, ab1) ? 0 : -1); + } + } + + public static class WedgeContainsOrIntersects implements WedgeProcessor { + /** + * Given two edge chains (see WedgeRelation above), this function returns +1 if A contains B, 0 + * if A and B are disjoint, and -1 if A intersects but does not contain B. + */ + @Override + public int test(S2Point a0, S2Point ab1, S2Point a2, S2Point b0, S2Point b2) { + // This is similar to WedgeContainsOrCrosses, except that we want to + // distinguish cases (1) [A contains B], (3) [A and B are disjoint], + // and (2,4,5,6) [A intersects but does not contain B]. + + if (orderedCCW(a0, a2, b2, ab1)) { + // We are in case 1, 5, or 6, or case 2 if a2 == b2. + return orderedCCW(b2, b0, a0, ab1) ? 1 : -1; // Case 1 vs. 2,5,6. + } + // We are in cases 2, 3, or 4. + if (!orderedCCW(a2, b0, b2, ab1)) { + return 0; // Case 3. + } + + // We are in case 2 or 4, or case 3 if a2 == b0. + return (a2.equalsPoint(b0)) ? 0 : -1; // Case 3 vs. 2,4. + } + } + + public static class WedgeContainsOrCrosses implements WedgeProcessor { + /** + * Given two edge chains (see WedgeRelation above), this function returns +1 if A contains B, 0 + * if B contains A or the two wedges do not intersect, and -1 if the edge chains A and B cross + * each other (i.e. if A intersects both the interior and exterior of the region to the left of + * B). In degenerate cases where more than one of these conditions is satisfied, the maximum + * possible result is returned. For example, if A == B then the result is +1. + */ + @Override + public int test(S2Point a0, S2Point ab1, S2Point a2, S2Point b0, S2Point b2) { + // There are 6 possible edge orderings at a shared vertex (all + // of these orderings are circular, i.e. abcd == bcda): + // + // (1) a2 b2 b0 a0: A contains B + // (2) a2 a0 b0 b2: B contains A + // (3) a2 a0 b2 b0: A and B are disjoint + // (4) a2 b0 a0 b2: A and B intersect in one wedge + // (5) a2 b2 a0 b0: A and B intersect in one wedge + // (6) a2 b0 b2 a0: A and B intersect in two wedges + // + // In cases (4-6), the boundaries of A and B cross (i.e. the boundary + // of A intersects the interior and exterior of B and vice versa). + // Thus we want to distinguish cases (1), (2-3), and (4-6). + // + // Note that the vertices may satisfy more than one of the edge + // orderings above if two or more vertices are the same. The tests + // below are written so that we take the most favorable + // interpretation, i.e. preferring (1) over (2-3) over (4-6). In + // particular note that if orderedCCW(a,b,c,o) returns true, it may be + // possible that orderedCCW(c,b,a,o) is also true (if a == b or b == c). + + if (orderedCCW(a0, a2, b2, ab1)) { + // The cases with this vertex ordering are 1, 5, and 6, + // although case 2 is also possible if a2 == b2. + if (orderedCCW(b2, b0, a0, ab1)) { + return 1; // Case 1 (A contains B) + } + + // We are in case 5 or 6, or case 2 if a2 == b2. + return (a2.equalsPoint(b2)) ? 0 : -1; // Case 2 vs. 5,6. + } + // We are in case 2, 3, or 4. + return orderedCCW(a0, b0, a2, ab1) ? 0 : -1; // Case 2,3 vs. 4. + } + } + + /** + * FaceSegment represents an edge AB clipped to an S2 cube face. It is represented by a face index + * and a pair of (u,v) coordinates. + */ + static final class FaceSegment { + int face; + final R2Vector a = new R2Vector(); + final R2Vector b = new R2Vector(); + + /** Returns an array of newly created FaceSegments. */ + static FaceSegment[] allFaces() { + FaceSegment[] faces = new FaceSegment[6]; + for (int i = 0; i < faces.length; i++) { + faces[i] = new FaceSegment(); + } + return faces; + } + } + + // The three functions below all compare a sum (u + v) to a third value w. + // They are implemented in such a way that they produce an exact result even + // though all calculations are done with ordinary floating-point operations. + // Here are the principles on which these functions are based: + // + // A. If u + v < w in floating-point, then u + v < w in exact arithmetic. + // + // B. If u + v < w in exact arithmetic, then at least one of the following + // expressions is true in floating-point: + // u + v < w + // u < w - v + // v < w - u + // + // Proof: By rearranging terms and substituting ">" for "<", we can assume + // that all values are non-negative. Now clearly "w" is not the smallest + // value, so assume that "u" is the smallest. We want to show that + // u < w - v in floating-point. If v >= w/2, the calculation of w - v is + // exact since the result is smaller in magnitude than either input value, + // so the result holds. Otherwise we have u <= v < w/2 and w - v >= w/2 + // (even in floating point), so the result also holds. + + /** Returns true if u + v == w exactly. */ + static boolean sumEquals(double u, double v, double w) { + return (u + v == w) && (u == w - v) && (v == w - u); + } + + /** + * Returns true if 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. + */ + static boolean intersectsFace(S2Point n) { + // 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 + // (see comments above). + double u = Math.abs(n.x); + double v = Math.abs(n.y); + double w = Math.abs(n.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); + } + + /** + * Given a directed line L intersecting a cube face F, return true if L intersects two opposite + * edges of F (including the case where L passes exactly through a corner vertex of F). The line L + * is defined by its normal N in the (u,v,w) coordinates of F. + */ + static boolean intersectsOppositeEdges(S2Point n) { + // 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 (see comments above). + double u = Math.abs(n.x); + double v = Math.abs(n.y); + double w = Math.abs(n.z); + // If w is the smallest, the following line returns an exact result. + if (Math.abs(u - v) != w) { + return Math.abs(u - v) >= w; + } + // Otherwise u - v = w exactly, or w is not the smallest value. In either + // case the following line returns the correct result. + return (u >= v) ? (u - w >= v) : (v - w >= u); + } + + /** + * Given cube face F and a directed line L (represented by its CCW normal N in the (u,v,w) + * coordinates of F), compute the axis of the cube face edge where L exits the face: return 0 if L + * exits through the u=-1 or u=+1 edge, and 1 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. + */ + static int getExitAxis(S2Point n) { + // assert (intersectsFace(n)); + if (intersectsOppositeEdges(n)) { + // The line passes 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. + return (Math.abs(n.x) >= Math.abs(n.y)) ? 1 : 0; + } else { + // The line passes 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. + // assert(n.x != 0 && n.y != 0 && n.z != 0); + return ((n.x < 0) ^ (n.y < 0) ^ (n.z < 0)) ? 0 : 1; + } + } + + /** + * Given a cube face F, a directed line L (represented by its CCW normal N in the (u,v,w) + * coordinates of F), and result of {@link #getExitAxis(S2Point)}, set {@code result} to the (u,v) + * coordinates of the point where L exits the cube face. + */ + static void getExitPoint(S2Point n, int axis, R2Vector result) { + if (axis == 0) { + result.x = (n.y > 0) ? 1.0 : -1.0; + result.y = (-result.x * n.x - n.z) / n.y; + } else { + result.y = (n.x < 0) ? 1.0 : -1.0; + result.x = (-result.y * n.y - n.z) / n.x; + } + } + + /** + * Given a line segment AB whose origin A has been projected onto a given cube face, determine + * whether it is necessary to project A onto a different face instead. This can happen because the + * normal of the line AB is not computed exactly, so that the line AB (defined as the set of + * points perpendicular to the normal) may not intersect the cube face containing A. Even if it + * does intersect the face, the "exit point" of the line from that face may be on the wrong side + * of A (i.e., in the direction away from B). If this happens, we reproject A onto the adjacent + * face where the line AB approaches A most closely. This moves the origin by a small amount, but + * never more than the error tolerances documented in the header file. + */ + static int moveOriginToValidFace(int face, S2Point a, S2Point ab, R2Vector aUv) { + // Fast path: if the origin is sufficiently far inside the face, it is + // always safe to use it. + final double kMaxSafeUVCoord = 1 - FACE_CLIP_ERROR_UV_COORD; + double au = aUv.x; + double av = aUv.y; + if (Math.max(Math.abs(au), Math.abs(av)) <= kMaxSafeUVCoord) { + return face; + } + + // Otherwise check whether the normal AB even intersects this face. + S2Point n = S2Projections.faceXyzToUvw(face, ab); + if (intersectsFace(n)) { + // Check whether the point where the line AB exits this face is on the + // wrong side of A (by more than the acceptable error tolerance). + getExitPoint(n, getExitAxis(n), aUv); + S2Point exit = S2Projections.faceUvToXyz(face, aUv); + S2Point aTangent = S2Point.crossProd(S2Point.normalize(ab), a); + if (S2Point.sub(exit, a).dotProd(aTangent) >= -FACE_CLIP_ERROR_RADIANS) { + // We can use the given face, but first put the original values back. + aUv.x = au; + aUv.y = av; + return face; + } + } + + // Otherwise we reproject A to the nearest adjacent face. (If line AB does + // not pass through a given face, it must pass through all adjacent faces.) + if (Math.abs(au) >= Math.abs(av)) { + face = S2Projections.getUVWFace(face, 0 /*U axis*/, au > 0 ? 1 : 0); + } else { + face = S2Projections.getUVWFace(face, 1 /*V axis*/, av > 0 ? 1 : 0); + } + // assert(intersectsFace(S2Projections.faceXyzToUvw(face, ab))); + S2Projections.validFaceXyzToUv(face, a, aUv); + aUv.set(Math.max(-1.0, Math.min(1.0, aUv.x)), Math.max(-1.0, Math.min(1.0, aUv.y))); + return face; + } + + /** + * Return the next face that should be visited by getFaceSegments, given that we have just visited + * "face" and we are following the line AB (represented by its normal N in the (u,v,w) coordinates + * of that face). The other arguments include the point where AB exits "face", the corresponding + * exit axis, and the "target face" containing the destination point B. + */ + static int getNextFace(int face, R2Vector exit, int axis, S2Point n, int targetFace) { + // We return the face that is adjacent to the exit point along the given + // axis. If line AB exits *exactly* through a corner of the face, there are + // two possible next faces. If one is the "target face" containing B, then + // we guarantee that we advance to that face directly. + // + // The three conditions below check that (1) AB exits approximately through + // a corner, (2) the adjacent face along the non-exit axis is the target + // face, and (3) AB exits *exactly* through the corner. (The sumEquals() + // code checks whether the dot product of (u,v,1) and "n" is exactly zero.) + if (Math.abs(exit.get(1 - axis)) == 1 + && S2Projections.getUVWFace(face, 1 - axis, exit.get(1 - axis) > 0 ? 1 : 0) == targetFace + && sumEquals(exit.x * n.x, exit.y * n.y, -n.z)) { + return targetFace; + } + + // Otherwise return the face that is adjacent to the exit point in the + // direction of the exit axis. + return S2Projections.getUVWFace(face, axis, exit.get(axis) > 0 ? 1 : 0); + } + + /** + * Subdivide the given edge AB at every point where it crosses the boundary between two S2 cube + * faces, returning the number of FaceSegments entries used (all entries must be prefilled). The + * segments are returned in order from A toward B. The input points must be unit length. + * + *

This method guarantees that the returned segments form a continuous path from A to B, and + * that all vertices are within kFaceClipErrorUVDist of the line AB. All vertices lie within the + * [-1,1]x[-1,1] cube face rectangles. The results are consistent with {@link + * S2Predicates.Sign#expensive}, i.e. the edge is well-defined even if its endpoints are + * antipodal. + */ + // TODO(user): Extend the implementation of S2.robustCrossProd so that this statement is true. + static int getFaceSegments(S2Point a, S2Point b, FaceSegment[] segments) { + // assert(S2.IsUnitLength(a)); + // assert(S2.IsUnitLength(b)); + + // Fast path: both endpoints are on the same face. + FaceSegment seg = segments[0]; + seg.face = S2Projections.xyzToFace(a); + S2Projections.validFaceXyzToUv(seg.face, a, seg.a); + int bFace = S2Projections.xyzToFace(b); + S2Projections.validFaceXyzToUv(bFace, b, seg.b); + if (seg.face == bFace) { + return 1; + } else { + // Starting at A, we follow AB from face to face until we reach the face + // containing B. The following code is designed to ensure that we always + // reach B, even in the presence of numerical errors. + // + // First we compute the normal to the plane containing A and B. This normal + // becomes the ultimate definition of the line AB; it is used to resolve all + // questions regarding where exactly the line goes. Unfortunately due to + // numerical errors, the line may not quite intersect the faces containing + // the original endpoints. We handle this by moving A and/or B slightly if + // necessary so that they are on faces intersected by the line AB. + S2Point ab = S2.robustCrossProd(a, b); + seg.face = moveOriginToValidFace(seg.face, a, ab, seg.a); + bFace = moveOriginToValidFace(bFace, b, S2Point.neg(ab), seg.b); + + // Save b in the last possible segment. + segments[5].b.set(seg.b); + + // Now we simply follow AB from face to face until we reach B. + int size = 1; + while (seg.face != bFace) { + // Complete the current segment by finding the point where AB exits the + // current face. + S2Point n = S2Projections.faceXyzToUvw(seg.face, ab); + int exitAxis = getExitAxis(n); + getExitPoint(n, exitAxis, seg.b); + + // Compute the next face intersected by AB, and translate the exit point + // of the current segment into the (u,v) coordinates of the next face. + // This becomes the first point of the next segment. + int newFace = getNextFace(seg.face, seg.b, exitAxis, n, bFace); + S2Point oldExitXyz = S2Projections.faceUvToXyz(seg.face, seg.b); + S2Point newExitUvw = S2Projections.faceXyzToUvw(newFace, oldExitXyz); + + // Set up the first half of the next segment. + seg = segments[size++]; + seg.face = newFace; + seg.a.set(newExitUvw.x, newExitUvw.y); + } + + // Finish the last segment. + seg.b.set(segments[5].b); + return size; + } + } + + /** + * This helper function does two things. First, it clips the line segment AB to find the clipped + * destination B' on a given face. (The face is specified implicitly by expressing *all arguments* + * in the (u,v,w) coordinates of that face.) Second, it partially computes whether the segment AB + * intersects this face at all. The actual condition is fairly complicated, but it turns out that + * it can be expressed as a "score" that can be computed independently when clipping the two + * endpoints A and B. This function returns the score for the given endpoint, which is an integer + * ranging from 0 to 3. If the sum of the two scores is 3 or more, then AB does not intersect this + * face. See the calling function for the meaning of the various parameters. + */ + static int clipDestination( + S2Point a, + S2Point b, + S2Point nScaled, + S2Point aTangent, + S2Point bTangent, + double uvScale, + R2Vector uv) { + // assert(intersectsFace(nScaled)); + + // Optimization: if B is within the safe region of the face, use it. + final double kMaxSafeUVCoord = 1 - FACE_CLIP_ERROR_UV_COORD; + if (b.z > 0) { + uv.set(b.x / b.z, b.y / b.z); + if (Math.max(Math.abs(uv.x), Math.abs(uv.y)) <= kMaxSafeUVCoord) { + return 0; + } + } + + // Otherwise find the point B' where the line AB exits the face. + getExitPoint(nScaled, getExitAxis(nScaled), uv); + uv.x *= uvScale; + uv.y *= uvScale; + S2Point p = new S2Point(uv.x, uv.y, 1); + + // Determine if the exit point B' is contained within the segment. We do this + // by computing the dot products with two inward-facing tangent vectors at A + // and B. If either dot product is negative, we say that B' is on the "wrong + // side" of that point. As the point B' moves around the great circle AB past + // the segment endpoint B, it is initially on the wrong side of B only; as it + // moves further it is on the wrong side of both endpoints; and then it is on + // the wrong side of A only. If the exit point B' is on the wrong side of + // either endpoint, we can't use it; instead the segment is clipped at the + // original endpoint B. + // + // We reject the segment if the sum of the scores of the two endpoints is 3 + // or more. Here is what that rule encodes: + // - If B' is on the wrong side of A, then the other clipped endpoint A' + // must be in the interior of AB (otherwise AB' would go the wrong way + // around the circle). There is a similar rule for A'. + // - If B' is on the wrong side of either endpoint (and therefore we must + // use the original endpoint B instead), then it must be possible to + // project B onto this face (i.e., its w-coordinate must be positive). + // This rule is only necessary to handle certain zero-length edges (A=B). + int score = 0; + if (S2Point.sub(p, a).dotProd(aTangent) < 0) { + score = 2; // B' is on wrong side of A. + } else if (S2Point.sub(p, b).dotProd(bTangent) < 0) { + score = 1; // B' is on wrong side of B. + } + if (score > 0) { // B' is not in the interior of AB. + if (b.z <= 0) { + score = 3; // B cannot be projected onto this face. + } else { + uv.set(b.x / b.z, b.y / b.z); + } + } + return score; + } + + /** + * As {@link #clipToFace(S2Point, S2Point, int, R2Vector, R2Vector)}, but rather than clipping to + * the square [-1,1]x[-1,1] in (u,v) space, this method clips to [-R,R]x[-R,R] where + * R=(1+padding). + */ + public static boolean clipToPaddedFace( + S2Point aXyz, S2Point bXyz, int face, double padding, R2Vector aUv, R2Vector bUv) { + // assert (padding >= 0); + // Fast path: both endpoints are on the given face. + if (S2Projections.xyzToFace(aXyz) == face && S2Projections.xyzToFace(bXyz) == face) { + S2Projections.validFaceXyzToUv(face, aXyz, aUv); + S2Projections.validFaceXyzToUv(face, bXyz, bUv); + return true; + } + + // Convert everything into the (u,v,w) coordinates of the given face. Note + // that the cross product *must* be computed in the original (x,y,z) + // coordinate system because RobustCrossProd (unlike the mathematical cross + // product) can produce different results in different coordinate systems + // when one argument is a linear multiple of the other, due to the use of + // symbolic perturbations. + S2Point n = S2Projections.faceXyzToUvw(face, S2.robustCrossProd(aXyz, bXyz)); + S2Point a = S2Projections.faceXyzToUvw(face, aXyz); + S2Point b = S2Projections.faceXyzToUvw(face, bXyz); + + // Padding is handled by scaling the u- and v-components of the normal. + // Letting R=1+padding, this means that when we compute the dot product of + // the normal with a cube face vertex (such as (-1,-1,1)), we will actually + // compute the dot product with the scaled vertex (-R,-R,1). This allows + // methods such as intersectsFace(), getExitAxis(), etc, to handle padding + // with no further modifications. + final double uvScale = 1 + padding; + S2Point nScaled = new S2Point(uvScale * n.x, uvScale * n.y, n.z); + if (!intersectsFace(nScaled)) { + return false; + } + + // TODO(user): This is a temporary hack until I rewrite S2.RobustCrossProd; + // it avoids loss of precision in normalize() when the vector is so small + // that it underflows. + if (Math.max(Math.abs(n.x), Math.max(Math.abs(n.y), Math.abs(n.z))) < Math.scalb(1d, -511)) { + n = S2Point.mul(n, Math.scalb(1d, 563)); + } // END OF HACK + + n = S2Point.normalize(n); + S2Point aTangent = S2Point.crossProd(n, a); + S2Point bTangent = S2Point.crossProd(b, n); + // As described above, if the sum of the scores from clipping the two + // endpoints is 3 or more, then the segment does not intersect this face. + int aScore = clipDestination(b, a, S2Point.neg(nScaled), bTangent, aTangent, uvScale, aUv); + int bScore = clipDestination(a, b, nScaled, aTangent, bTangent, uvScale, bUv); + return aScore + bScore < 3; + } + + /** + * Returns true if the edge AB intersects the given (closed) rectangle to within the error bound + * below. + */ + static boolean intersectsRect(R2Vector a, R2Vector b, R2Rect rect) { + // First check whether the bound of AB intersects "rect". + R2Rect bound = R2Rect.fromPointPair(a, b); + if (!rect.intersects(bound)) { + return false; + } + + // Otherwise AB intersects "rect" if and only if all four vertices of "rect" + // do not lie on the same side of the extended line AB. We test this by + // finding the two vertices of "rect" with minimum and maximum projections + // onto the normal of AB, and computing their dot products with the edge + // normal. + R2Vector n = R2Vector.sub(b, a).ortho(); + int i = (n.x >= 0) ? 1 : 0; + int j = (n.y >= 0) ? 1 : 0; + double max = n.dotProd(R2Vector.sub(rect.getVertex(i, j), a)); + double min = n.dotProd(R2Vector.sub(rect.getVertex(1 - i, 1 - j), a)); + return (max >= 0) && (min <= 0); + } + + /** Moves an endpoint of the given bound to the given value. */ + static boolean updateEndpoint(R1Interval bound, boolean slopeNegative, double value) { + if (!slopeNegative) { + if (bound.hi() < value) { + return false; + } + if (bound.lo() < value) { + bound.setLo(value); + } + } else { + if (bound.lo() > value) { + return false; + } + if (bound.hi() > value) { + bound.setHi(value); + } + } + return true; + } + + /** + * Given a line segment from (a0,a1) to (b0,b1) and a bounding interval for each axis, clip the + * segment further if necessary so that "bound0" does not extend outside the given interval + * "clip". "diag" is a a precomputed helper variable that indicates which diagonal of the bounding + * box is spanned by AB: it is 0 if AB has positive slope, and 1 if AB has negative slope. + */ + static boolean clipBoundAxis( + double a0, + double b0, + R1Interval bound0, + double a1, + double b1, + R1Interval bound1, + boolean slopeNegative, + R1Interval clip0) { + if (bound0.lo() < clip0.lo()) { + if (bound0.hi() < clip0.lo()) { + return false; + } + bound0.setLo(clip0.lo()); + if (!updateEndpoint(bound1, slopeNegative, interpolateDouble(clip0.lo(), a0, b0, a1, b1))) { + return false; + } + } + if (bound0.hi() > clip0.hi()) { + if (bound0.lo() > clip0.hi()) { + return false; + } + bound0.setHi(clip0.hi()); + if (!updateEndpoint(bound1, !slopeNegative, interpolateDouble(clip0.hi(), a0, b0, a1, b1))) { + return false; + } + } + return true; + } + + /** + * Given an edge AB and a rectangle "clip", return the bounding rectangle of the portion of AB + * intersected by "clip". The resulting bound may be empty. This is a convenience function built + * on top of clipEdgeBound. + */ + static R2Rect getClippedEdgeBound(R2Vector a, R2Vector b, R2Rect clip) { + R2Rect bound = R2Rect.fromPointPair(a, b); + if (clipEdgeBound(a, b, clip, bound)) { + return bound; + } + return R2Rect.empty(); + } + + /** + * This function can be used to clip an edge AB to sequence of rectangles efficiently. It + * represents the clipped edges by their bounding boxes rather than as a pair of endpoints. + * Specifically, let A'B' be some portion of an edge AB, and let "bound" be a tight bound of A'B'. + * This function updates "bound" (in place) to be a tight bound of A'B' intersected with a given + * rectangle "clip". If A'B' does not intersect "clip", returns false and does not necessarily + * update "bound". + * + *

The given bound must be a tight bounding rectangle for some portion of AB. (This condition + * is automatically satisfied if you start with the bounding box of AB and clip to a sequence of + * rectangles, stopping when the method returns false.) + */ + static boolean clipEdgeBound(R2Vector a, R2Vector b, R2Rect clip, R2Rect bound) { + // "slopeNegative" indicates which diagonal of the bounding box is spanned by AB: it + // is false if AB has positive slope, and true if AB has negative slope. This is + // used to determine which interval endpoints need to be updated each time + // the edge is clipped. + boolean slopeNegative = (a.x > b.x) != (a.y > b.y); + return clipBoundAxis(a.x, b.x, bound.x(), a.y, b.y, bound.y(), slopeNegative, clip.x()) + && clipBoundAxis(a.y, b.y, bound.y(), a.x, b.x, bound.x(), slopeNegative, clip.y()); + } + + /** + * Given an edge AB, assigns the portion of AB that is contained by the given rectangle "clip" to + * the aClipped and bClipped arguments, and returns true if there is an intersection. + */ + static boolean clipEdge( + R2Vector a, R2Vector b, R2Rect clip, R2Vector aClipped, R2Vector bClipped) { + // Compute the bounding rectangle of AB, clip it, and then extract the new + // endpoints from the clipped bound. + R2Rect bound = R2Rect.fromPointPair(a, b); + if (clipEdgeBound(a, b, clip, bound)) { + int iEnd = a.x > b.x ? 1 : 0; + int jEnd = a.y > b.y ? 1 : 0; + aClipped.set(bound.getVertex(iEnd, jEnd)); + bClipped.set(bound.getVertex(1 - iEnd, 1 - jEnd)); + return true; + } + return false; + } + + /** + * Given a value x that is some linear combination of a and b, return the value x1 that is the + * same linear combination of a1 and b1. This function makes the following guarantees: + * + *

    + *
  1. If x == a, then x1 = a1 (exactly). + *
  2. If x == b, then x1 = b1 (exactly). + *
  3. If a <= x <= b, then a1 <= x1 <= b1 (even if a1 == b1). + *
+ * + *

Results are undefined if a==b. + */ + static double interpolateDouble(double x, double a, double b, double a1, double b1) { + // assertTrue(a != b); + // To get results that are accurate near both A and B, we interpolate starting from the closer + // of the two points. + if (Math.abs(a - x) <= Math.abs(b - x)) { + return a1 + (b1 - a1) * (x - a) / (b - a); + } else { + return b1 + (a1 - b1) * (x - b) / (a - b); + } + } + + /** + * Given an edge AB and a face, return the (u,v) coordinates for the portion of AB that intersects + * that face. This method guarantees that the clipped vertices lie within the [-1,1]x[-1,1] cube + * face rectangle and are within kFaceClipErrorUVDist of the line AB, but the results may differ + * from those produced by getFaceSegments. Returns false if AB does not intersect the given face. + */ + public static boolean clipToFace(S2Point a, S2Point b, int face, R2Vector aUv, R2Vector bUv) { + return clipToPaddedFace(a, b, face, 0.0, aUv, bUv); + } + + /** + * Return true if edge AB crosses CD at a point that is interior to both edges. Properties: + * + *

    + *
  • simpleCrossing(b,a,c,d) == simpleCrossing(a,b,c,d) + *
  • simpleCrossing(c,d,a,b) == simpleCrossing(a,b,c,d) + *
+ */ + public static boolean simpleCrossing(S2Point a, S2Point b, S2Point c, S2Point d) { + // We compute simpleCCW() 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. Note that this is slightly more + // restrictive than the corresponding definition for planar edges, + // since we need to exclude pairs of line segments that would + // otherwise "intersect" by crossing two antipodal points. + + S2Point ab = S2Point.crossProd(a, b); + double acb = -(ab.dotProd(c)); + double bda = ab.dotProd(d); + if (acb * bda <= 0) { + return false; + } + + S2Point cd = S2Point.crossProd(c, d); + double cbd = -(cd.dotProd(b)); + double dac = cd.dotProd(a); + return (acb * cbd > 0) && (acb * dac > 0); + } + + /** + * Like SimpleCrossing, except that points that lie exactly on a line are arbitrarily classified + * as being on one side or the other (according to the rules of sign). It returns +1 if there is a + * crossing, -1 if there is no crossing, and 0 if any two vertices from different edges are the + * same. Returns 0 or -1 if either edge is degenerate. Properties of robustCrossing: + * + *
    + *
  • robustCrossing(b,a,c,d) == robustCrossing(a,b,c,d) + *
  • robustCrossing(c,d,a,b) == robustCrossing(a,b,c,d) + *
  • robustCrossing(a,b,c,d) == 0 if a==c, a==d, b==c, b==d + *
  • robustCrossing(a,b,c,d) <= 0 if a==b or c==d + *
+ * + *

Note that if you want to check an edge against a *chain* of other edges, it is much more + * efficient to use an EdgeCrosser (above). + */ + public static int robustCrossing(S2Point a, S2Point b, S2Point c, S2Point d) { + EdgeCrosser crosser = new EdgeCrosser(a, b, c); + return crosser.robustCrossing(d); + } + + /** + * Given two edges AB and CD where at least two vertices are identical (i.e. + * robustCrossing(a,b,c,d) == 0), this function defines whether the two edges "cross" in a such a + * way that point-in-polygon containment tests can be implemented by counting the number of edge + * crossings. 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): + * + *

    + *
  • VC(a,a,c,d) == VC(a,b,c,c) == false + *
  • VC(a,b,a,b) == VC(a,b,b,a) == true + *
  • VC(a,b,c,d) == VC(a,b,d,c) == VC(b,a,c,d) == VC(b,a,d,c) + *
  • 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. + */ + public static boolean vertexCrossing(S2Point a, S2Point b, S2Point c, S2Point d) { + // 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.equalsPoint(b) || c.equalsPoint(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 than the edge CD. + if (a.equalsPoint(d)) { + return orderedCCW(S2.ortho(a), c, b, a); + } + if (b.equalsPoint(c)) { + return orderedCCW(S2.ortho(b), d, a, b); + } + if (a.equalsPoint(c)) { + return orderedCCW(S2.ortho(a), d, b, a); + } + if (b.equalsPoint(d)) { + return orderedCCW(S2.ortho(b), c, a, b); + } + + // assert (false); + return false; + } + + /** + * A convenience function that calls robustCrossing() to handle cases where all four vertices are + * distinct, and VertexCrossing() to handle cases where two or more vertices are the same. This + * defines a crossing function such that point-in-polygon containment tests can be implemented by + * simply counting edge crossings. + */ + public static boolean edgeOrVertexCrossing(S2Point a, S2Point b, S2Point c, S2Point d) { + int crossing = robustCrossing(a, b, c, d); + if (crossing < 0) { + return false; + } + if (crossing > 0) { + return true; + } + return vertexCrossing(a, b, c, d); + } + + /** + * Finds the closest acceptable endpoint to a given point. An endpoint is acceptable if it lies + * between the endpoints of the other line segment. + */ + static S2Point closestAcceptableEndpoint( + S2Point a0, S2Point a1, S2Point aNorm, S2Point b0, S2Point b1, S2Point bNorm, S2Point x) { + CloserResult r = new CloserResult(Double.POSITIVE_INFINITY, x); + if (orderedCCW(b0, a0, b1, bNorm)) { + r.replaceIfCloser(x, a0); + } + if (orderedCCW(b0, a1, b1, bNorm)) { + r.replaceIfCloser(x, a1); + } + if (orderedCCW(a0, b0, a1, aNorm)) { + r.replaceIfCloser(x, b0); + } + if (orderedCCW(a0, b1, a1, aNorm)) { + r.replaceIfCloser(x, b1); + } + return r.getVmin(); + } + + static class CloserResult { + private double dmin2; + private S2Point vmin; + + public double getDmin2() { + return dmin2; + } + + public S2Point getVmin() { + return vmin; + } + + public CloserResult(double dmin2, S2Point vmin) { + this.dmin2 = dmin2; + this.vmin = vmin; + } + + public void replaceIfCloser(S2Point x, S2Point y) { + // If the squared distance from x to y is less than dmin2, then replace + // vmin by y and update dmin2 accordingly. + double d2 = S2Point.minus(x, y).norm2(); + if (d2 < dmin2 || (d2 == dmin2 && y.lessThan(vmin))) { + dmin2 = d2; + vmin = y; + } + } + } + + /** Returns true if ab possibly crosses cd, by clipping tiny angles to zero. */ + public static final boolean lenientCrossing(S2Point a, S2Point b, S2Point c, S2Point d) { + // assert (S2.isUnitLength(a)); + // assert (S2.isUnitLength(b)); + // assert (S2.isUnitLength(c)); + + double acb = S2Point.scalarTripleProduct(b, a, c); + if (Math.abs(acb) < MAX_DET_ERROR) { + return true; + } + double bda = S2Point.scalarTripleProduct(a, b, d); + if (Math.abs(bda) < MAX_DET_ERROR) { + return true; + } + if (acb * bda < 0) { + return false; + } + double cbd = S2Point.scalarTripleProduct(d, c, b); + if (Math.abs(cbd) < MAX_DET_ERROR) { + return true; + } + double dac = S2Point.scalarTripleProduct(c, d, a); + if (Math.abs(dac) < MAX_DET_ERROR) { + return true; + } + return (acb * cbd >= 0) && (acb * dac >= 0); + } + + /** + * Given two edges AB and CD such that robustCrossing() is true, return their intersection point. + * Useful properties of getIntersection (GI): + * + *

    + *
  • GI(b,a,c,d) == GI(a,b,d,c) == GI(a,b,c,d) + *
  • GI(c,d,a,b) == GI(a,b,c,d) + *
+ * + * The returned intersection point X is guaranteed to be very close to the true intersection point + * of AB and CD, even if the edges intersect at a very small angle. See "INTERSECTION_ERROR" above + * for details. + */ + public static S2Point getIntersection(S2Point a0, S2Point a1, S2Point b0, S2Point b1) { + return getIntersection(a0, a1, b0, b1, new ResultError()); + } + + /** + * Helper for {@link getIntersection(S2Point, S2Point, S2Point, S2Point)} with provided result + * error parameter for testing and benchmarking purposes. + */ + static S2Point getIntersection( + S2Point a0, S2Point a1, S2Point b0, S2Point b1, ResultError resultError) { + Preconditions.checkArgument( + robustCrossing(a0, a1, b0, b1) > 0, + "Input edges a0a1 and b0b1 must have a true robustCrossing."); + + // It is difficult to compute the intersection point of two edges accurately when the angle + // between the edges is very small. Previously we handled this by only guaranteeing that the + // returned intersection point is within INTERSECTION_ERROR of each edge. However, this means + // that when the edges cross at a very small angle, the computed result may be very far from the + // true intersection point. + // + // Instead this function now guarantees that the result is always within INTERSECTION_ERROR of + // the true intersection. This requires using more sophisticated techniques and in some cases + // extended precision. + // + // Two different techniques are used: + // + //
    + //
  • getIntersectionStable() computes the intersection point using projection and + // interpolation, taking care to minimize cancellation error. + //
  • getIntersectionExact() computes the intersection point using exact arithmetic and + // converts the final result back to an S2Point. + //
+ // + // Our strategy is to first call getIntersectionStable(). If the result has an error bound + // greater than INTERSECTION_ERROR, we fall back to exact arithmetic. + S2Point result = getIntersectionApprox(a0, a1, b0, b1, resultError); + if (resultError.error > INTERSECTION_ERROR) { + result = getIntersectionExact(a0, a1, b0, b1); + } + return correctIntersectionSign(a0, a1, b0, b1, result); + } + + /** Returns intersection result with sign corrected (if necessary). */ + static S2Point correctIntersectionSign( + S2Point a0, S2Point a1, S2Point b0, S2Point b1, S2Point intersectionResult) { + // Make sure the intersection point is on the correct side of the sphere. Since all vertices are + // unit length, and both edge lengths are less than 180 degrees, (a0 + a1) and (b0 + b1) both + // have positive dot product with the intersection point. We use the sum of all vertices to + // make sure that the result is unchanged when the edges are swapped or reversed. + if (intersectionResult.dotProd(a0.add(a1).add(b0.add(b1))) < 0) { + intersectionResult = intersectionResult.neg(); + } + return intersectionResult; + } + + /** + * Given a point X and an edge AB, return the distance ratio AX / (AX + BX). If X happens to be on + * the line segment AB, this is the fraction "t" such that X == Interpolate(A, B, t). Requires + * that A and B are distinct. + */ + public static double getDistanceFraction(S2Point x, S2Point a0, S2Point a1) { + Preconditions.checkArgument(!a0.equalsPoint(a1)); + double d0 = x.angle(a0); + double d1 = x.angle(a1); + return d0 / (d0 + d1); + } + + /** + * Return the minimum distance from X to any point on the edge AB. The result is very accurate for + * small distances but may have some numerical error if the distance is large (approximately Pi/2 + * or greater). The case A == B is handled correctly. + * + * @throws IllegalArgumentException Thrown if the parameters are not all unit length. + */ + public static S1Angle getDistance(S2Point x, S2Point a, S2Point b) { + Preconditions.checkArgument(S2.isUnitLength(x), "S2Point not normalized: %s", x); + Preconditions.checkArgument(S2.isUnitLength(a), "S2Point not normalized: %s", a); + Preconditions.checkArgument(S2.isUnitLength(b), "S2Point not normalized: %s", b); + return S1Angle.radians(getDistanceRadians(x, a, b, S2.robustCrossProd(a, b))); + } + + /** Gets the distance from {@code p} to {@code e}. */ + public static S1ChordAngle getDistance(S2Point p, S2Edge e) { + return updateMinDistance(p, e, S1ChordAngle.INFINITY); + } + + /** Gets the minimum of the distance from {@code a} to {@code e} and {@code minDistance}. */ + public static S1ChordAngle updateMinDistance(S2Point p, S2Edge e, S1ChordAngle minDistance) { + return updateMinDistance(p, e.getStart(), e.getEnd(), minDistance); + } + + /** + * Return the minimum of the distance from {@code x} to any point on edge ab and the given {@code + * minDistance}. The case {@code a.equals(b)} is handled correctly. + * + * @throws IllegalArgumentException Thrown if the parameters are not all unit length. + */ + // TODO(blakewall): Update this method to be named getMinDistance. + public static S1ChordAngle updateMinDistance( + S2Point x, S2Point a, S2Point b, S1ChordAngle minDistance) { + Preconditions.checkArgument(S2.isUnitLength(x), "S2Point not normalized: %s", x); + Preconditions.checkArgument(S2.isUnitLength(a), "S2Point not normalized: %s", a); + Preconditions.checkArgument(S2.isUnitLength(b), "S2Point not normalized: %s", b); + + // We divide the problem into two cases, based on whether the closest point + // on AB is one of the two vertices (the "vertex case") or in the interior + // (the "interior case"). Let C = A x B. If X is in the spherical wedge + // extending from A to B around the axis through C, then we are in the + // interior case. Otherwise we are in the vertex case. + + // Check whether we might be in the interior case. For this to be true, XAB and XBA must both be + // acute angles. Checking this condition exactly is expensive, so instead we consider the 3D + // Euclidian triangle ABX (which passes through the sphere's interior). As can be observed from + // the law of spherical excess, the planar angles XAB and XBA are always less than the + // corresponding spherical angles, so if we are in the interior case then both of these angles + // must be acute. + // + // We check this by computing the squared edge lengths of the 3D Euclidean triangle ABX, and + // testing acuteness using the law of cosines: + // + // max(XA^2, XB^2) < AB^2 + min(XA^2, XB^2) + // or equivalently: XA^2 + XB^2 < AB^2 + 2 * min(XA^2, XB^2) + // + double xa2 = x.getDistance2(a); + double xb2 = x.getDistance2(b); + double ab2 = a.getDistance2(b); + double dist2 = min(xa2, xb2); + if (xa2 + xb2 < ab2 + 2 * dist2) { + // The minimum distance might be to a point on the edge interior. Let R be the closest point + // to X that lies on the great circle through AB. Rather than computing the geodesic distance + // along the surface of the sphere, instead we compute the "chord length", the 3D Euclidian + // length of the line passing through the sphere's interior. If the squared chord length + // exceeds minDistance.getLength2() then we can return "false" immediately. + // + // The squared chord length XR^2 can be expressed as XQ^2 + QR^2, where Q is the point X + // projected onto the plane through the great circle AB. + // The distance XQ^2 can be written as (X.C)^2 / |C|^2 where C = A x B. + // We ignore the QR^2 term and instead use XQ^2 as a lower bound, since it is faster and the + // corresponding distance on the Earth's surface is accurate to within 1% for distances up to + // about 1800km. + S2Point c = S2.robustCrossProd(a, b); + double c2 = c.norm2(); + double xDotC = x.dotProd(c); + double xDotC2 = xDotC * xDotC; + if (xDotC2 >= c2 * minDistance.getLength2()) { + // The closest point on the great circle AB is too far away. + return minDistance; + } + // Otherwise we do the exact, more expensive test for the interior case. + // This test is very likely to succeed because of the conservative planar test we did + // initially. + S2Point cx = S2Point.crossProd(c, x); + if (a.dotProd(cx) < 0 && b.dotProd(cx) > 0) { + // Compute the squared chord length XR^2 = XQ^2 + QR^2 (see above). + // This calculation has good accuracy for all chord lengths since it is based on both the + // dot product and cross product (rather than deriving one from the other). However, note + // that the chord length representation itself loses accuracy as the angle approaches Pi. + double qr = 1 - sqrt(cx.norm2() / c2); + dist2 = (xDotC2 / c2) + (qr * qr); + } + } + if (dist2 >= minDistance.getLength2()) { + return minDistance; + } + return S1ChordAngle.fromLength2(dist2); + } + + /** + * Returns the maximum of the distance from {@code x} to any point on edge AB and the given {@code + * maxDistance}. The case {@code a.equals(b)} is handled correctly. + */ + public static S1ChordAngle updateMaxDistance( + S2Point x, S2Point a, S2Point b, S1ChordAngle maxDistance) { + S1ChordAngle dist = S1ChordAngle.max(new S1ChordAngle(x, a), new S1ChordAngle(x, b)); + if (dist.compareTo(S1ChordAngle.RIGHT) > 0) { + dist = updateMinDistance(x.neg(), a, b, S1ChordAngle.INFINITY); + dist = S1ChordAngle.sub(S1ChordAngle.STRAIGHT, dist); + } + if (maxDistance.compareTo(dist) >= 0) { + return maxDistance; + } + return dist; + } + + /** + * Like {@link #updateMinDistance}, but computes the minimum distance between the given pair of + * edges. (If the two edges cross, the distance is zero.) The cases {@code a0.equals(a1)} and + * {@code b0.equals(b1)} are handled correctly. + */ + public static S1ChordAngle getEdgePairMinDistance( + final S2Point a0, + final S2Point a1, + final S2Point b0, + final S2Point b1, + S1ChordAngle minDist) { + if (minDist.equals(S1ChordAngle.ZERO)) { + return minDist; + } + + // If they cross, distance is 0 and no end point is closest. + if (robustCrossing(a0, a1, b0, b1) > 0) { + return S1ChordAngle.ZERO; + } + + minDist = updateMinDistance(a0, b0, b1, minDist); + minDist = updateMinDistance(a1, b0, b1, minDist); + minDist = updateMinDistance(b0, a0, a1, minDist); + minDist = updateMinDistance(b1, a0, a1, minDist); + return minDist; + } + + /** Gets distance between edges with no minimum distance. */ + public static S1ChordAngle getEdgePairDistance( + final S2Point a0, final S2Point a1, final S2Point b0, final S2Point b1) { + return getEdgePairMinDistance(a0, a1, b0, b1, S1ChordAngle.INFINITY); + } + + /** + * Updates the {@code results} with points that achieve the minimum distance between edges a0a1 + * and b0b1, where {@code a} is a point on a0a1 and {@code b} is a point on b0b1. If the two edges + * intersect, {@code a} and {@code b} are both equal to the intersection point. Handles {@code + * a0.equals(a1)} and {@code b0.equals(b1)} correctly. + */ + static void getEdgePairClosestPoints( + final S2Point a0, final S2Point a1, final S2Point b0, final S2Point b1, S2Point[] result) { + + // If they cross, distance is 0 and no end point is closest. + if (robustCrossing(a0, a1, b0, b1) > 0) { + S2Point intersection = getIntersection(a0, a1, b0, b1); + result[0] = intersection; + result[1] = intersection; + return; + } + + S1ChordAngle actualMin = S1ChordAngle.INFINITY; + ClosestPoint closest = ClosestPoint.NONE; + S1ChordAngle newMin = updateMinDistance(a0, b0, b1, actualMin); + if (newMin != actualMin) { + closest = ClosestPoint.A0; + actualMin = newMin; + } + newMin = updateMinDistance(a1, b0, b1, actualMin); + if (newMin != actualMin) { + closest = ClosestPoint.A1; + actualMin = newMin; + } + newMin = updateMinDistance(b0, a0, a1, actualMin); + if (newMin != actualMin) { + closest = ClosestPoint.B0; + actualMin = newMin; + } + newMin = updateMinDistance(b1, a0, a1, actualMin); + if (newMin != actualMin) { + closest = ClosestPoint.B1; + } + + switch (closest) { + case A0: + result[0] = a0; + result[1] = getClosestPoint(a0, b0, b1); + return; + case A1: + result[0] = a1; + result[1] = getClosestPoint(a1, b0, b1); + return; + case B0: + result[0] = getClosestPoint(b0, a0, a1); + result[1] = b0; + return; + case B1: + result[0] = getClosestPoint(b1, a0, a1); + result[1] = b1; + return; + default: + Preconditions.checkArgument( + false, + "Unknown ClosestPoint case when finding closest points of %s:%s and %s:%s", + a0, + a1, + b0, + b1); + } + } + + /** + * Like {@link #updateMaxDistance}, but computes the maximum distance between the given pair of + * edges. If the two edges cross, the distance is zero. The cases {@code a0.equals(a1)} and + * {@code b0.equals(b1)} are handled correctly. + */ + public static S1ChordAngle getEdgePairMaxDistance( + S2Point a0, S2Point a1, + S2Point b0, S2Point b1, + S1ChordAngle maxDist) { + if (S1ChordAngle.STRAIGHT.equals(maxDist)) { + return maxDist; + } + + if (S2EdgeUtil.robustCrossing(a0, a1, b0.neg(), b1.neg()) > 0) { + return S1ChordAngle.STRAIGHT; + } + + // Otherwise, the maximum distance is achieved at an endpoint of at least one of the two edges. + // The calculation below computes all six distances twice (this could be optimized). + maxDist = updateMaxDistance(a0, b0, b1, maxDist); + maxDist = updateMaxDistance(a1, b0, b1, maxDist); + maxDist = updateMaxDistance(b0, a0, a1, maxDist); + maxDist = updateMaxDistance(b1, a0, a1, maxDist); + return maxDist; + } + + /** + * A slightly more efficient version of getDistance() where the cross product of the two endpoints + * has been precomputed. The cross product does not need to be normalized, but should be computed + * using S2.robustCrossProd() for the most accurate results. + * + * @throws IllegalArgumentException Thrown if the parameters are not all unit length. + */ + public static S1Angle getDistance(S2Point x, S2Point a, S2Point b, S2Point aCrossB) { + Preconditions.checkArgument(S2.isUnitLength(x), "S2Point not normalized: %s", x); + Preconditions.checkArgument(S2.isUnitLength(a), "S2Point not normalized: %s", a); + Preconditions.checkArgument(S2.isUnitLength(b), "S2Point not normalized: %s", b); + return S1Angle.radians(getDistanceRadians(x, a, b, aCrossB)); + } + + /** @deprecated Temporary bridge for refactoring */ + @Deprecated + private static boolean ccw(S2Point a, S2Point b, S2Point c) { + return S2Point.scalarTripleProduct(b, c, a) > 0; + } + + /** + * A more efficient version of getDistance() where the cross product of the endpoints has been + * precomputed and the result is returned as a direct radian measure rather than wrapping it in an + * S1Angle. This is the recommended method for making large numbers of back-to-back edge distance + * tests, since it allocates no objects. The inputs are assumed to be unit length; results are + * undefined if they are not. + */ + public static double getDistanceRadians(S2Point x, S2Point a, S2Point b, S2Point aCrossB) { + // There are three cases. If X is located in the spherical wedge defined by + // A, B, and the axis A x B, then the closest point is on the segment AB. + // Otherwise the closest point is either A or B; the dividing line between + // these two cases is the great circle passing through (A x B) and the + // midpoint of AB. + if (ccw(aCrossB, a, x) && ccw(x, b, aCrossB)) { + // The closest point to X lies on the segment AB. We compute the distance + // to the corresponding great circle. The result is accurate for small + // distances but not necessarily for large distances (approaching Pi/2). + double sinDist = Math.abs(x.dotProd(aCrossB)) / aCrossB.norm(); + return Math.asin(Math.min(1.0, sinDist)); + } + + // Otherwise, the closest point is either A or B. The cheapest method is + // just to compute the minimum of the two linear (as opposed to spherical) + // distances and convert the result to an angle. Again, this method is + // accurate for small but not large distances (approaching Pi). + double linearDist2 = Math.min(diffMag2(x, a), diffMag2(x, b)); + return 2 * Math.asin(Math.min(1.0, 0.5 * Math.sqrt(linearDist2))); + } + + /** Returns the squared distance from {@code a} to {@code b}. */ + private static final double diffMag2(S2Point a, S2Point b) { + double dx = a.getX() - b.getX(); + double dy = a.getY() - b.getY(); + double dz = a.getZ() - b.getZ(); + return dx * dx + dy * dy + dz * dz; + } + + /** + * As {@link #getClosestPoint(S2Point, S2Point, S2Point)}, but faster if the cross product between + * a and b has already been computed. All points must be unit length; results are undefined if + * that is not the case. + */ + public static S2Point getClosestPoint(S2Point x, S2Point a, S2Point b, S2Point aCrossB) { + // assert (S2.isUnitLength(a)); + // assert (S2.isUnitLength(b)); + // assert (S2.isUnitLength(x)); + + // Find the closest point to X along the great circle through AB. + S2Point p = S2Point.sub(x, S2Point.mul(aCrossB, x.dotProd(aCrossB) / aCrossB.norm2())); + + // If this point is on the edge AB, then it's the closest point. + if (ccw(aCrossB, a, p) && ccw(p, b, aCrossB)) { + return S2Point.normalize(p); + } + + // Otherwise, the closest point is either A or B. + return x.getDistance2(a) <= x.getDistance2(b) ? a : b; + } + + /** + * Returns the point on edge AB closest to X. All points must be unit length; results are + * undefined if that is not the case. + */ + public static S2Point getClosestPoint(S2Point x, S2Point a, S2Point b) { + return getClosestPoint(x, a, b, S2.robustCrossProd(a, b)); + } + + /** + * A slightly more efficient version of {@link #interpolateAtDistance} that can be used when the + * distance AB is already known. Requires that all vectors have unit length. + */ + public static S2Point interpolateAtDistance(S1Angle ax, S2Point a, S2Point b, S1Angle ab) { + // assert S2.isUnitLength(a); + // assert S2.isUnitLength(b); + + double axRadians = ax.radians(); + double abRadians = ab.radians(); + + // The result X is some linear combination X = e*A + f*B of the input + // points. The fractions "e" and "f" can be derived by looking at the + // components of this equation that are parallel and perpendicular to A. + // Let E = e*A and F = f*B. Then OEXF is a parallelogram. You can obtain + // the distance f = OF by considering the similar triangles produced by + // dropping perpendiculars from the segments OF and OB to OA. + double f = Math.sin(axRadians) / Math.sin(abRadians); + + // Form the dot product of the first equation with A to obtain + // A.X = e*A.A + f*A.B. Since A, B, and X are all unit vectors, + // cos(ax) = e*1 + f*cos(ab), so + double e = Math.cos(axRadians) - f * Math.cos(abRadians); + + // Mathematically speaking, if "a" and "b" are unit length then the result + // is unit length as well. But we normalize it anyway to prevent points + // from drifting away from unit length when multiple interpolations are done + // in succession (i.e. the result of one interpolation is fed into another). + return S2Point.normalize(S2Point.add(S2Point.mul(a, e), S2Point.mul(b, f))); + } + + /** + * Like {@link #interpolate}, except that the parameter "ax" represents the desired distance from + * A to the result X rather than a fraction between 0 and 1. Requires that {@code a} and {@code b} + * are unit length. + */ + public static S2Point interpolateAtDistance(S1Angle ax, S2Point a, S2Point b) { + return interpolateAtDistance(ax, a, b, new S1Angle(a, b)); + } + + /** + * Return 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. + */ + public static S2Point interpolate(double t, S2Point a, S2Point b) { + if (t == 0) { + return a; + } + if (t == 1) { + return b; + } + S1Angle ab = new S1Angle(a, b); + return interpolateAtDistance(S1Angle.radians(t * ab.radians()), a, b, ab); + } + + /** + * Returns the maximum error in the result of {@link #updateMinDistance}, assuming that all input + * points are normalized to within the bounds guaranteed by {@link S2Point#normalize}. The error + * can be added or subtracted from an S1ChordAngle "x" using {@code x.plusError(error)}. + */ + static double getMinInteriorDistanceMaxError(S1ChordAngle distance) { + // If a point is more than 90 degrees from an edge, then the minimum + // distance is always to one of the endpoints, not to the edge interior. + if (distance.compareTo(S1ChordAngle.RIGHT) > 0) { + return 0.0; + } + + // This bound includes all source of error, assuming that the input points + // are normalized to within the bounds guaranteed to S2Point::Normalize(). + // "a" and "b" are components of chord length that are perpendicular and + // parallel to plane containing the edge respectively. + double x = distance.getLength2(); + double b = 0.5 * x * x; + double a = x * sqrt(1 - 0.5 * b); + return ((2.5 + 2 * sqrt(3) + 8.5 * a) * a + + (2 + 2 * sqrt(3) / 3 + 6.5 * (1 - b)) * b + + (23 + 16 / sqrt(3)) * S2.DBL_EPSILON) + * S2.DBL_EPSILON; + } + + /** + * Returns the maximum error in the result of {@link #updateMinDistance} (and associated + * functions), assuming that all input points are normalized to within the bounds guaranteed by + * {@link S2Point#normalize}. The error can be added or subtracted from an S1ChordAngle "x" using + * {@code x.plusError(error)}. + * + *

Note that accuracy goes down as the distance approaches 0 degrees or 180 degrees (for + * different reasons). Near 0 degrees the error is acceptable for all practical purposes (about + * 1.2e-15 radians ~= 8 nanometers). For exactly antipodal points the maximum error is quite high + * (0.5 meters), but this error drops rapidly as the points move away from antipodality + * (approximately 1 millimeter for points that are 50 meters from antipodal, and 1 micrometer for + * points that are 50km from antipodal). + */ + static double getMinDistanceMaxError(S1ChordAngle distance) { + // There are two max errors, depending on whether the closest point is interior to the edge. + return Math.max( + getMinInteriorDistanceMaxError(distance), distance.getS2PointConstructorMaxError()); + } + + /** + * Compute the intersection point of (a0, a1) and (b0, b1) using exact arithmetic. Note that the + * result is not exact because it is rounded to double precision. Also, the intersection point is + * not guaranteed to have the correct sign (i.e., the return value may need to be negated). + */ + static S2Point getIntersectionExact(S2Point a0, S2Point a1, S2Point b0, S2Point b1) { + // Since we are using exact arithmetic, we don't need to worry about numerical stability. + BigPoint aNormBp = (new BigPoint(a0)).crossProd(new BigPoint(a1)); + BigPoint bNormBp = (new BigPoint(b0)).crossProd(new BigPoint(b1)); + BigPoint xBp = aNormBp.crossProd(bNormBp); + + // The last two operations are done in double precision, which creates a directional error of + // up to 2 * S2.DBL_EPSILON. (BigPoint.toS2Point() and S2Point.normalize() each contribute up to + // S2.DBL_EPSILON of directional error.) + S2Point x = xBp.toS2Point().normalize(); + + if (x.equals(S2Point.ORIGIN)) { + // The two edges are exactly collinear, but we still consider them to be "crossing" because of + // simulation of simplicity. Out of the four endpoints, exactly two lie in the interior of + // the other edge. Of those two we return the one that is lexicographically smallest. + x = new S2Point(10, 10, 10); // Greater than any valid S2Point + S2Point aNorm = aNormBp.toS2Point().normalize(); + S2Point bNorm = bNormBp.toS2Point().normalize(); + // Note: To support antipodal edges properly, we would need to add a crossProd() function that + // computes the cross product using simulation of simplicity and rounds the result to the + // nearest floating-point representation. + Preconditions.checkArgument( + !(aNorm.equals(S2Point.ORIGIN) || bNorm.equals(S2Point.ORIGIN)), + "Exactly antipodal edges not supported by getIntersectionExact"); + x = closestAcceptableEndpoint(a0, a1, aNorm, b0, b1, bNorm, x); + } + // assert (S2.isUnitLength(x)); + return x; + } + + /** + * Returns the approximate intersection point of the edges (a0,a1) and (b0,b1), and writes to + * resultError a bound on its error. + * + *

The intersection point is not guaranteed to have the correct sign, i.e., it may need to be + * negated. + */ + static S2Point getIntersectionApprox( + S2Point a0, S2Point a1, S2Point b0, S2Point b1, ResultError resultError) { + // Sort the two edges so that (a0,a1) is longer, breaking ties in a deterministic way that does + // not depend on the ordering of the endpoints. This is desirable for two reasons: + // - So that the result doesn't change when edges are swapped or reversed. + // - It reduces error, since the first edge is used to compute the edge normal (where a longer + // edge means less error), and the second edge is used for interpolation (where a shorter + // edge means less error). + double aLen2 = a1.getDistance2(a0); + double bLen2 = b1.getDistance2(b0); + if ((aLen2 < bLen2) || ((aLen2 == bLen2) && compareEdges(a0, a1, b0, b1))) { + return getIntersectionApproxSorted(b0, b1, a0, a1, resultError); + } else { + return getIntersectionApproxSorted(a0, a1, b0, b1, resultError); + } + } + + /** + * Returns true if (a0,a1) is less than (b0,b1) with respect to a total ordering on edges that is + * invariant under edge reversals. + */ + private static boolean compareEdges(S2Point a0, S2Point a1, S2Point b0, S2Point b1) { + if (a1.lessThan(a0)) { + S2Point temp = a0; + a0 = a1; + a1 = temp; + } + if (b1.lessThan(b0)) { + S2Point temp = b0; + b0 = b1; + b1 = temp; + } + return a0.lessThan(b0) || (a0.equalsPoint(b0) && b0.lessThan(b1)); + } + + /** + * Returns the approximate intersection point of the edges (a0,a1) and (b0,b1), and writes to + * resultError a bound on its error. + * + *

Expects that the edges (a0,a1) and (b0,b1) have been sorted so that the first edge is + * longer. + * + *

The intersection point is not guaranteed to have the correct sign, i.e., it may need to be + * negated. + */ + private static S2Point getIntersectionApproxSorted( + S2Point a0, S2Point a1, S2Point b0, S2Point b1, ResultError resultError) { + // assert(a1.getDistance2(a0) >= b1.getDistance2(b0)); + + // Compute the normal of the plane through (a0, a1) in a stable way. + S2Point aNormal = S2.robustCrossProd(a0, a1); + double aNormalLen = aNormal.norm(); + double bLen = b1.getDistance(b0); + + // Compute the projection (i.e., signed distance) of b0 and b1 onto the plane through (a0, a1). + // Distances are scaled by the length of aNormal. + ResultError b0ResultError = new ResultError(); + ResultError b1ResultError = new ResultError(); + double b0Dist = getProjection(b0, aNormal, aNormalLen, a0, a1, b0ResultError); + double b1Dist = getProjection(b1, aNormal, aNormalLen, a0, a1, b1ResultError); + + // The total distance from b0 to b1 measured perpendicularly to (a0,a1) is |b0Dist - b1Dist|. + // Note that b0Dist and b1Dist generally have opposite signs because b0 and b1 are on opposite + // sides of (a0, a1). The code below finds the intersection point by interpolating along the + // edge (b0, b1) to a fractional distance of b0Dist / (b0Dist - b1Dist). + // + // It can be shown that the maximum error in the interpolation fraction is + // + // (b0Dist * b1ResultError.error - b1Dist * b0ResultError.error) / + // (distSum * (distSum - errorSum)) + // + // We save ourselves some work by scaling the result and the error bound by "distSum", since the + // result is normalized to be unit length anyway. + double distSum = Math.abs(b0Dist - b1Dist); + double errorSum = b0ResultError.error + b1ResultError.error; + if (distSum <= errorSum) { + // Error is unbounded in this case. Return arbitrary S2Point with infinite error. + resultError.error = Double.POSITIVE_INFINITY; + return S2Point.ORIGIN; + } + S2Point x = b1.mul(b0Dist).sub(b0.mul(b1Dist)); + + // Finally we normalize the result and compute the corresponding error. + double xLen2 = x.norm2(); + if (xLen2 < Double.MIN_NORMAL) { + // If x.norm2() is less than double's minimum norm value, xLen might lose precision and the + // result might fail to satisfy S2.isUnitLength(). Return arbitrary S2Point with infinite + // error. + resultError.error = Double.POSITIVE_INFINITY; + return S2Point.ORIGIN; + } + double xLen = Math.sqrt(xLen2); + double scaledInterpFactor = + Math.abs(b0Dist * b1ResultError.error - b1Dist * b0ResultError.error) + / (distSum - errorSum); + resultError.error = + ((bLen * scaledInterpFactor + 2 * S2.DBL_EPSILON * distSum) / xLen) + S2.DBL_EPSILON; + return x.mul(1 / xLen); + } + + /** + * Returns 2x the dot product of x and aNormal, and writes to resultError a bound on the error + * given that aNormal was calculated using {@link S2#robustCrossProd}. + * + *

The remaining parameters allow this calculation to be computed more accurately and + * efficiently. They include the length of aNormal (aNormalLen) and the edge endpoints a0 and a1. + * + *

Note that the 2x factor mentioned above is the result of an error reducing step. Rescaling + * the result would result in a loss of accuracy and efficiency, and thus is not performed. + */ + static double getProjection( + S2Point x, + S2Point aNormal, + double aNormalLen, + S2Point a0, + S2Point a1, + ResultError resultError) { + // The error in the dot product is proportional to the lengths of the input vectors, so rather + // than using x itself (a unit-length vector) we use the vectors from x to the closer of the + // two edge endpoints. This typically reduces the error by a huge factor. + S2Point x0 = x.sub(a0); + S2Point x1 = x.sub(a1); + double x0Dist2 = x0.norm2(); + double x1Dist2 = x1.norm2(); + + // If both distances are the same, we need to be careful to choose one endpoint + // deterministically so that the result does not change if the order of the endpoints is + // reversed. + double dist; + double result; + if ((x0Dist2 < x1Dist2) || (x0Dist2 == x1Dist2 && x0.lessThan(x1))) { + dist = Math.sqrt(x0Dist2); + result = x0.dotProd(aNormal); + } else { + dist = Math.sqrt(x1Dist2); + result = x1.dotProd(aNormal); + } + // This calculation bounds the error from all sources: the computation of the normal, the + // subtraction of one endpoint, and the dot product itself. + // + // For reference, the bounds that went into this calculation are: + // ||N'-N|| <= ((1 + 2 * sqrt(3))||N|| + 32 * sqrt(3) * S2.DBL_EPSILON) * S2.DBL_EPSILON + // |(A.B)'-(A.B)| <= (1.5 * (A.B) + 1.5 * ||A|| * ||B||) * S2.DBL_EPSILON + // ||(X-Y)'-(X-Y)|| <= ||X-Y|| * S2.DBL_EPSILON + resultError.error = + S2.DBL_EPSILON + * (dist * ((3.5 + 2 * Math.sqrt(3)) * aNormalLen + 32 * Math.sqrt(3) * S2.DBL_EPSILON) + + 1.5 * Math.abs(result)); + return result; + } + + /** + * Encapsulation of a mutable error value. + * + *

Used as an output parameter for methods that calculate double error values for their return + * values. + * + *

TODO(bjj): Reuse elsewhere, e.g., S2Predicates. + */ + static final class ResultError { + double error; + } + + /** Constructor is private so that this class is never instantiated. */ + private S2EdgeUtil() {} +} diff --git a/core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/S2Error.java b/core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/S2Error.java new file mode 100644 index 0000000000..779bdab5a1 --- /dev/null +++ b/core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/S2Error.java @@ -0,0 +1,108 @@ +/* + * Copyright 2014 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.mogo.eagle.core.utilcode.geometry; + +import com.google.common.annotations.GwtCompatible; + +/** + * An error code and text string describing the first error encountered during a validation process. + */ +@GwtCompatible +public class S2Error { + public enum Code { + /** No problems detected. */ + NO_ERROR(0), + + // Generic errors, not specific to geometric objects: + /** Unknown error. */ + UNKNOWN(1000), + /** Operation is not implemented. */ + UNIMPLEMENTED(1001), + /** Argument is out of range. */ + OUT_OF_RANGE(1002), + /** Invalid argument (other than a range error). */ + INVALID_ARGUMENT(1003), + /** Object is not in the required state. */ + FAILED_PRECONDITION(1004), + /** An internal invariant has failed. */ + INTERNAL(1005), + /** Data loss or corruption. */ + DATA_LOSS(1006), + /** A resource has been exhausted. */ + RESOURCE_EXHAUSTED(1007), + + // Error codes that apply to more than one type of geometry: + /** Vertex is not unit length. */ + NOT_UNIT_LENGTH(1), + /** There are two identical vertices. */ + DUPLICATE_VERTICES(2), + /** There are two antipodal vertices. */ + ANTIPODAL_VERTICES(3), + + // Error codes that only apply to certain geometric objects: + /** Loop with fewer than 3 vertices. */ + LOOP_NOT_ENOUGH_VERTICES(100), + /** Loop has a self-intersection. */ + LOOP_SELF_INTERSECTION(101), + + /** Two polygon loops share an edge. */ + POLYGON_LOOPS_SHARE_EDGE(200), + /** Two polygon loops cross. */ + POLYGON_LOOPS_CROSS(201), + /** Polygon has an empty loop. */ + POLYGON_EMPTY_LOOP(202), + /** Non-full polygon has a full loop. */ + POLYGON_EXCESS_FULL_LOOP(203), + /** Loop depths don't correspond to any valid nesting hierarchy. */ + POLYGON_INVALID_LOOP_DEPTH(205), + /** Actual polygon nesting does not correspond to the nesting given in the loop depths. */ + POLYGON_INVALID_LOOP_NESTING(206); + + private int code; + + private Code(int code) { + this.code = code; + } + + /** Returns the numeric value of this error code. */ + public int code() { + return code; + } + } + + private Code code = Code.NO_ERROR; + private String text = ""; + + /** + * Sets the error code and text description; the description is formatted according to the rules + * defined in {@link String#format(String, Object...)}. This method may be called more than once, + * so that various layers may surround + */ + public void init(Code code, String format, Object... args) { + this.code = code; + this.text = Platform.formatString(format, args); + } + + /** Returns the code of this error. */ + public Code code() { + return code; + } + + /** Returns the text string. */ + public String text() { + return text; + } +} diff --git a/core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/S2FractalBuilder.java b/core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/S2FractalBuilder.java new file mode 100644 index 0000000000..08ee285a38 --- /dev/null +++ b/core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/S2FractalBuilder.java @@ -0,0 +1,238 @@ +/* + * Copyright 2014 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.mogo.eagle.core.utilcode.geometry; + +import com.google.common.annotations.GwtCompatible; +import com.google.common.base.Preconditions; +import com.google.common.collect.Lists; +import java.util.List; +import java.util.Random; + +/** + * A simple class that generates "Koch snowflake" fractals (see Wikipedia for an introduction). + * There is an option to control the fractal dimension (between 1.0 and 2.0); values between 1.02 + * and 1.50 are reasonable simulations of various coastlines. The default dimension (about 1.26) + * corresponds to the standard Koch snowflake. (The west coast of Britain has a fractal dimension of + * approximately 1.25) + * + *

The fractal is obtained by starting with an equilateral triangle and recursively subdividing + * each edge into four segments of equal length. Therefore the shape at level 'n' consists of 3 * + * (4^n) edges. Multi-level fractals are also supported: if you set minLevel() to a non-negative + * value, then the recursive subdivision has an equal probability of stopping at any of the levels + * between the given min and max (inclusive). This yields a fractal where the perimeter of the + * original triangle is approximately equally divided between fractals at the various possible + * levels. If there are k distinct levels {min, ..., max}, the expected number of edges at each + * level 'i' is approximately 3 * (4 ^ i) / k. + */ +@GwtCompatible +public class S2FractalBuilder { + private int maxLevel = -1; + + /** Value set by user. */ + private int minLevelArg = -1; + + /** Actual min level (depends on maxLevel.) */ + private int minLevel = -1; + + /** Standard Koch curve */ + private double dimension = (Math.log(4) / Math.log(3)); + + /** The ratio of the sub-edge length to the original edge length at each subdivision step. */ + private double edgeFraction = 0; + + /** + * The distance from the original edge to the middle vertex at each subdivision step, as a + * fraction of the original edge length. + */ + private double offsetFraction = 0; + + private Random rand; + + /** You must call setMaxLevel() or setLevelForApproxMaxMedges() before calling makeLoop(). */ + public S2FractalBuilder(Random rand) { + this.rand = rand; + computeOffsets(); + } + + /** Sets the maximum subdivision level for the fractal (see above). */ + public void setMaxLevel(int maxLevel) { + Preconditions.checkArgument(maxLevel >= 0); + this.maxLevel = maxLevel; + computeMinLevel(); + } + + /** + * Sets the minimum subdivision level for the fractal (see above). The default value of -1 causes + * the min and max levels to be the same. A minLevel of 0 should be avoided since this creates a + * significant chance that none of the three original edges will be subdivided at all. + */ + public void setMinLevel(int minLevelArg) { + Preconditions.checkArgument(minLevelArg >= -1); + this.minLevelArg = minLevelArg; + computeMinLevel(); + } + + private void computeMinLevel() { + if (minLevelArg >= 0 && minLevelArg <= maxLevel) { + minLevel = minLevelArg; + } else { + minLevel = maxLevel; + } + } + + /** + * Sets the fractal dimension. The default value of approximately 1.26 corresponds to the standard + * Koch curve. The value must lie in the range [1.0, 2.0). + */ + public void setFractalDimension(double dimension) { + Preconditions.checkArgument(dimension >= 1.0); + Preconditions.checkArgument(dimension <= 2.0); + this.dimension = dimension; + computeOffsets(); + } + + private void computeOffsets() { + edgeFraction = Math.pow(4.0, -1.0 / dimension); + offsetFraction = Math.sqrt(edgeFraction - 0.25); + } + + /** + * The following two functions set the min and/or max level to produce approximately the given + * number of edges. (The values are rounded to a nearby value of 3 * (4 ^ n).) + */ + public void setLevelForApproxMinEdges(int minEdges) { + setMinLevel(levelFromEdges(minEdges)); + } + + public void setLevelForApproxMaxEdges(int maxEdges) { + setMaxLevel(levelFromEdges(maxEdges)); + } + + /** Returns level from values in the range [1.5 * (4 ^ n), 6 * (4 ^ n)]. */ + private static int levelFromEdges(int edges) { + return (int) Math.ceil(0.5 * Math.log(edges / 3) / Math.log(2)); + } + + /** + * Returns a lower bound on the ratio (Rmin / R), where 'R' is the radius passed to makeLoop() and + * 'Rmin' is the minimum distance from the fractal boundary to its center. This can be used to + * inscribe another geometric figure within the fractal without intersection. + */ + public double minRadiusFactor() { + // The minimum radius is attained at one of the vertices created by the first subdivision + // step as long as the dimension is not too small (at least + // kMinDimensionForMinRadiusAtLevel1, see below). Otherwise we fall back on the incircle + // radius of the original triangle, which is always a lower bound (and is attained when + // dimension = 1). + // + // The value below was obtained by letting AE be an original triangle edge, letting ABCDE be + // the corresponding polyline after one subdivision step, and then letting BC be tangent to + // the inscribed circle at the center of the fractal O. This gives rise to a pair of + // similar triangles whose edge length ratios can be used to solve for the corresponding + // "edge fraction". This method is slightly conservative because it is computed using + // planar rather than spherical geometry. The value below is equal to + // -log(4)/log((2 + cbrt(2) - cbrt(4))/6). + double kMinDimensionForMinRadiusAtLevel1 = 1.0852230903040407; + if (dimension >= kMinDimensionForMinRadiusAtLevel1) { + return Math.sqrt(1 + 3 * edgeFraction * (edgeFraction - 1)); + } + return 0.5; + } + + /** + * Returns the ratio (Rmax / R), where 'R' is the radius passed to makeLoop() and 'Rmax' is the + * maximum distance from the fractal boundary to its center. This can be used to inscribe the + * fractal within some other geometric figure without intersection. + */ + public double maxRadiusFactor() { + // The maximum radius is always attained at either an original triangle vertex or at a + // middle vertex from the first subdivision step. + return Math.max(1.0, offsetFraction * Math.sqrt(3) + 0.5); + } + + private void getR2Vertices(List vertices) { + // The Koch "snowflake" consists of three Koch curves whose initial edges form an + // equilateral triangle. + R2Vector v0 = new R2Vector(1.0, 0.0); + R2Vector v1 = new R2Vector(-0.5, Math.sqrt(3) / 2); + R2Vector v2 = new R2Vector(-0.5, -Math.sqrt(3) / 2); + getR2VerticesHelper(v0, v1, 0, vertices); + getR2VerticesHelper(v1, v2, 0, vertices); + getR2VerticesHelper(v2, v0, 0, vertices); + } + + /** + * Given the two endpoints (v0, v4) of an edge, recursively subdivide the edge to the desired + * level, and insert all vertices of the resulting curve up to but not including the endpoint + * "v4". + */ + private void getR2VerticesHelper(R2Vector v0, R2Vector v4, int level, List vertices) { + // The second expression should return 'true' once every (maxLevel - level + 1) calls. + if (level >= minLevel && (rand.nextInt(maxLevel - level + 1) == 0)) { + // Stop subdivision at this level. + vertices.add(v0); + return; + } + // Otherwise compute the intermediate vertices v1, v2, and v3. + R2Vector dir = R2Vector.sub(v4, v0); + // v1 = v0 + edgeFraction * dir + R2Vector v1 = R2Vector.add(v0, R2Vector.mul(dir, edgeFraction)); + // v2 = 0.5 * (v0 + v4) - offsetFraction * dir.ortho() + R2Vector v2 = + R2Vector.sub( + R2Vector.mul(R2Vector.add(v0, v4), 0.5), R2Vector.mul(dir.ortho(), offsetFraction)); + // v3 = v4 - edgeFraction * dir + R2Vector v3 = R2Vector.sub(v4, R2Vector.mul(dir, edgeFraction)); + + // And recurse on the four sub-edges. + getR2VerticesHelper(v0, v1, level + 1, vertices); + getR2VerticesHelper(v1, v2, level + 1, vertices); + getR2VerticesHelper(v2, v3, level + 1, vertices); + getR2VerticesHelper(v3, v4, level + 1, vertices); + } + + /** + * Returns a fractal loop centered around the a-axis of the given coordinate frame, with the first + * vertex in the direction of the positive x-axis, and the given nominal radius. + */ + public S2Loop makeLoop(Matrix3x3 frame, S1Angle nominalRadius) { + return new S2Loop(makeVertices(frame, nominalRadius)); + } + + /** As {@link #makeLoop(Matrix3x3, S1Angle)} except it returns the vertices instead of loop. */ + public List makeVertices(Matrix3x3 frame, S1Angle nominalRadius) { + List r2Vertices = Lists.newArrayList(); + getR2Vertices(r2Vertices); + List vertices = Lists.newArrayList(); + for (int i = 0; i < r2Vertices.size(); ++i) { + // Convert each vertex to polar coordinates. + R2Vector v = r2Vertices.get(i); + double theta = Math.atan2(v.y(), v.x()); + double radius = nominalRadius.radians() * v.norm(); + + // We construct the loop in the given frame coordinates, with the center at (0, 0, 1). For + // a loop of radius 'r', the loop vertices have the form (x, y, z) where x^2 + y^2 = sin(r) + // and z = cos(r). The distance on the sphere (arc length) from each vertex to the center + // is acos(cos(r)) = r. + double z = Math.cos(radius); + double r = Math.sin(radius); + S2Point p = new S2Point(r * Math.cos(theta), r * Math.sin(theta), z); + vertices.add(S2.rotate(p, frame)); + } + return vertices; + } +} diff --git a/core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/S2Iterator.java b/core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/S2Iterator.java new file mode 100644 index 0000000000..684924dc02 --- /dev/null +++ b/core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/S2Iterator.java @@ -0,0 +1,288 @@ +/* + * Copyright 2015 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.mogo.eagle.core.utilcode.geometry; + +import com.google.common.annotations.GwtCompatible; +import com.google.common.base.Function; +import com.mogo.eagle.core.utilcode.geometry.S2ShapeIndex.CellRelation; +import com.google.common.primitives.UnsignedLongs; +import java.util.List; + +/** + * A random access iterator that provides low-level access to entries sorted by cell ID. The + * behavior of this iterator is more like a database cursor, where accessing properties at the + * current position does not alter the position of the cursor. The cursor has a {@link #compareTo} + * method to compare the value at the current position of the iterator with a given S2CellId. + */ +// TODO(eengle): Replace Entry with a Multimap that is space efficient, and supports +// time-efficient inserts, removes, and lookups. +@GwtCompatible +public final class S2Iterator { + /** An interface to provide the cell ID for an element in a sorted list. */ + public interface Entry { + /** Returns the cell ID of this cell as a primitive. */ + long id(); + } + + /** + * Creates an iterator given a list of entries. Package private and not public, since only S2 + * classes guarantee the necessary preconditions on {@code entries} -- that the cell IDs of each + * entry are sorted in ascending order. + */ + static S2Iterator create(List entries) { + return new S2Iterator(entries); + } + + /** + * Same as {@link #create(List)}, but accepts {@code seekFunction}, which is used as the + * implementation of {@link #seek(S2CellId)}. + * + * @param entries the list of entries which back this iterator. + * @param seekFunction a function which takes a target {@link S2CellId} and returns an index to + * which this iterator will be repositioned. + */ + static S2Iterator create( + List entries, Function seekFunction) { + return new S2Iterator(entries, seekFunction); + } + + /** Creates a new iterator with the same entries and position as {@code it}. */ + static S2Iterator copy(S2Iterator it) { + S2Iterator copy = new S2Iterator(it.entries, it.seekFunction); + copy.pos = it.pos; + return copy; + } + + private final List entries; + private final Function seekFunction; + protected int pos; + + /** + * Create a new iterator based on the given list of entries. Results are undefined if the entries + * are not in ascending sorted order. + * + * @param entries the list of entries which back this iterator. + */ + protected S2Iterator(List entries) { + this.entries = entries; + this.seekFunction = + (target) -> { + int start = 0; + int end = entries.size() - 1; + while (start <= end) { + int mid = (start + end) / 2; + long id = entries.get(mid).id(); + int result = UnsignedLongs.compare(id, target.id()); + if (result > 0) { + end = mid - 1; + } else if (result < 0) { + start = mid + 1; + } else if (start != mid) { + end = mid; + } else { + return mid; + } + } + return start; + }; + } + + /** + * Same as {@link #S2Iterator(List)}, but accepts {@code seekFunction}, which is used as the + * implementation of {@link #seek(S2CellId)}. + * + * @param entries the list of entries which back this iterator. + * @param seekFunction a function which takes a target {@link S2CellId} and returns an index to + * which this iterator will be repositioned. + */ + protected S2Iterator(List entries, Function seekFunction) { + this.entries = entries; + this.seekFunction = seekFunction; + } + + /** Returns a copy of this iterator, positioned as this iterator is. */ + public S2Iterator copy() { + S2Iterator it = new S2Iterator(entries, seekFunction); + it.pos = pos; + return it; + } + + /** Positions the iterator so that {@link #atBegin()} is true. */ + public void restart() { + pos = 0; + } + + /** Returns the comparison from the current iterator cell to the given cell ID. */ + public int compareTo(S2CellId cellId) { + return UnsignedLongs.compare(entry().id(), cellId.id()); + } + + /** Returns true if {@code o} is an {@link S2Iterator} with equal entries and position. */ + @Override + public boolean equals(Object o) { + return o instanceof S2Iterator && equalIterators((S2Iterator) o); + } + + @Override + public int hashCode() { + return 31 * pos + entries.hashCode(); + } + + /** Returns true if these iterators have the same entries and position. */ + public boolean equalIterators(S2Iterator it) { + return entries == it.entries && pos == it.pos; + } + + /** Returns the cell id for the current cell. */ + public S2CellId id() { + return new S2CellId(entry().id()); + } + + /** Returns the current entry. */ + public T entry() { + // assert (!done()); + return entries.get(pos); + } + + /** Returns the center of the cell (used as a reference point for shape interiors.) */ + public S2Point center() { + return id().toPoint(); + } + + /** + * Advances the iterator to the next cell in the index. Does not advance the iterator if {@code + * pos} is equal to the number of cells in the index. + */ + public void next() { + if (pos < entries.size()) { + pos++; + } + } + + /** + * Positions the iterator at the previous cell in the index. Does not move the iterator if {@code + * pos} is equal to 0. + */ + public void prev() { + if (pos > 0) { + pos--; + } + } + + /** Returns true if the iterator is positioned past the last index cell. */ + public boolean done() { + return pos == entries.size(); + } + + /** Returns true if the iterator is positioned at the first index cell. */ + public boolean atBegin() { + return pos == 0; + } + + /** + * Positions the iterator at the first cell with id() >= target, or at the end of the index if no + * such cell exists. + */ + public void seek(S2CellId target) { + pos = seekFunction.apply(target); + } + + /** + * Advances the iterator to the next cell with id() >= target. If the iterator is {@link #done()} + * or already satisfies id() >= target, there is no effect. + */ + public void seekForward(S2CellId target) { + if (!done() && compareTo(target) < 0) { + int tmpPos = pos; + seek(target); + pos = Math.max(pos, tmpPos + 1); + } + } + + /** Positions the iterator so that {@link #done()} is true. */ + public void finish() { + pos = entries.size(); + } + + /** + * Positions the iterator at the index cell containing "target" and returns true, or if no such + * cell exists in the index, the iterator is positioned arbitrarily and this method returns false. + * + *

The resulting index position is guaranteed to contain all edges that might intersect the + * line segment between {@code targetPoint} and {@link #center()}. + */ + public boolean locate(S2Point targetPoint) { + // Let I be the first cell not less than T, where T is the leaf cell containing "targetPoint". + // Then if T is contained by an index cell, then the containing cell is either I or I'. We + // test for containment by comparing the ranges of leaf cells spanned by T, I, and I'. + S2CellId target = S2CellId.fromPoint(targetPoint); + seek(target); + if (!done() && id().rangeMin().lessOrEquals(target)) { + return true; + } + if (!atBegin()) { + prev(); + if (id().rangeMax().greaterOrEquals(target)) { + return true; + } + } + return false; + } + + /** + * Positions the iterator at the index cell containing the given cell, if possible, and returns + * the {@link CellRelation} that describes the relationship between the index and the given target + * cell: + * + *

    + *
  • Returns {@link CellRelation#INDEXED} if the iterator was positioned at an index cell that + * is equal to or contains the given cell. I.e. the given target exists in the index as a + * leaf cell. + *
  • Returns {@link CellRelation#SUBDIVIDED} if the iterator was positioned at the first of + * one or more cells contained by the given target cell. I.e. the target does not exist in + * the index, but the first of its descendants was selected. + *
  • Returns {@link CellRelation#DISJOINT} if the iterator had to be positioned arbitrarily + * because the given target cell does not intersect any of the index's cells. + */ + public CellRelation locate(S2CellId target) { + // Let T be the target, let I be the first cell not less than T.rangeMin(), and let I' be the + // predecessor of I. If T contains any index cells, then T contains I. Similarly, if T is + // contained by an index cell, then the containing cell is either I or I'. We test for + // containment by comparing the ranges of leaf cells spanned by T, I, and I'. + seek(target.rangeMin()); + if (!done()) { + if (id().greaterOrEquals(target) && id().rangeMin().lessOrEquals(target)) { + return CellRelation.INDEXED; + } + if (id().lessOrEquals(target.rangeMax())) { + return CellRelation.SUBDIVIDED; + } + } + if (!atBegin()) { + prev(); + if (id().rangeMax().greaterOrEquals(target)) { + return CellRelation.INDEXED; + } + } + return CellRelation.DISJOINT; + } + + /** Set this iterator to the position given by the other iterator. */ + public void position(S2Iterator it) { + // assert index == it.index; + pos = it.pos; + } +} diff --git a/core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/S2LatLng.java b/core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/S2LatLng.java new file mode 100644 index 0000000000..90b4d18680 --- /dev/null +++ b/core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/S2LatLng.java @@ -0,0 +1,282 @@ +/* + * Copyright 2005 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.mogo.eagle.core.utilcode.geometry; + +import com.google.common.annotations.GwtCompatible; +import com.google.errorprone.annotations.Immutable; +import java.io.Serializable; +import javax.annotation.CheckReturnValue; + +/** + * This class represents a point on the unit sphere as a pair of latitude-longitude coordinates. + * Like the rest of the "geometry" package, the intent is to represent spherical geometry as a + * mathematical abstraction, so functions that are specifically related to the Earth's geometry + * (e.g. easting/northing conversions) should be put elsewhere. Note that the serialized form of + * this class is not stable and should not be relied upon for long-term persistence. + * + */ +@Immutable +@GwtCompatible(serializable = true, emulated = true) +public final strictfp class S2LatLng implements Serializable { + /** The center point the lat/lng coordinate system. */ + public static final S2LatLng CENTER = new S2LatLng(0.0, 0.0); + + private final double latRadians; + private final double lngRadians; + + /** Returns a new S2LatLng specified in radians. */ + public static S2LatLng fromRadians(double latRadians, double lngRadians) { + return new S2LatLng(latRadians, lngRadians); + } + + /** Returns a new S2LatLng converted from degrees. */ + public static S2LatLng fromDegrees(double latDegrees, double lngDegrees) { + return new S2LatLng(S1Angle.degrees(latDegrees), S1Angle.degrees(lngDegrees)); + } + + /** Returns a new S2LatLng converted from tens of microdegrees. */ + public static S2LatLng fromE5(int latE5, int lngE5) { + return new S2LatLng(S1Angle.e5(latE5), S1Angle.e5(lngE5)); + } + + /** Returns a new S2LatLng converted from microdegrees. */ + public static S2LatLng fromE6(int latE6, int lngE6) { + return new S2LatLng(S1Angle.e6(latE6), S1Angle.e6(lngE6)); + } + + /** Returns a new S2LatLng converted from tenths of a microdegree. */ + public static S2LatLng fromE7(int latE7, int lngE7) { + return new S2LatLng(S1Angle.e7(latE7), S1Angle.e7(lngE7)); + } + + public static S1Angle latitude(S2Point p) { + // We use atan2 rather than asin because the input vector is not necessarily + // unit length, and atan2 is much more accurate than asin near the poles. + // The "+ 0.0" is to ensure that points with coordinates of -0.0 and +0.0 + // (which compare equal) are converted to identical S2LatLng values, since + // even though -0.0 == +0.0 they can be formatted differently. + return S1Angle.radians(Math.atan2(p.z + 0.0, Math.sqrt(p.x * p.x + p.y * p.y))); + } + + public static S1Angle longitude(S2Point p) { + // The "+ 0.0" is to ensure that points with coordinates of -0.0 and +0.0 + // (which compare equal) are converted to identical S2LatLng values, since + // even though -0.0 == +0.0 and -180 == 180 degrees, they can be formatted + // differently. Also note that atan2(0, 0) is defined to be zero. + return S1Angle.radians(Math.atan2(p.y + 0.0, p.x + 0.0)); + } + + /** This is internal to avoid ambiguity about which units are expected. */ + private S2LatLng(double latRadians, double lngRadians) { + this.latRadians = latRadians; + this.lngRadians = lngRadians; + } + + /** + * Basic constructor. The latitude and longitude must be within the ranges allowed by is_valid() + * below. + */ + public S2LatLng(S1Angle lat, S1Angle lng) { + this(lat.radians(), lng.radians()); + } + + /** Default constructor for convenience when declaring arrays, etc. */ + public S2LatLng() { + this(0, 0); + } + + /** Convert a point (not necessarily normalized) to an S2LatLng. */ + public S2LatLng(S2Point p) { + // The "+ 0.0" is to ensure that points with coordinates of -0.0 and +0.0 + // (which compare equal) are converted to identical S2LatLng values, since + // even though -0.0 == +0.0 they can be formatted differently. + this(Math.atan2(p.z + 0.0, Math.sqrt(p.x * p.x + p.y * p.y)), Math.atan2(p.y + 0.0, p.x + 0.0)); + // The latitude and longitude are already normalized. We use atan2 to + // compute the latitude because the input vector is not necessarily unit + // length, and atan2 is much more accurate than asin near the poles. + // Note that atan2(0, 0) is defined to be zero. + } + + /** Returns the latitude of this point as a new S1Angle. */ + public S1Angle lat() { + return S1Angle.radians(latRadians); + } + + /** Returns the latitude of this point as radians. */ + public double latRadians() { + return latRadians; + } + + /** Returns the latitude of this point as degrees. */ + public double latDegrees() { + return 180.0 / Math.PI * latRadians; + } + + /** Returns the longitude of this point as a new S1Angle. */ + public S1Angle lng() { + return S1Angle.radians(lngRadians); + } + + /** Returns the longitude of this point as radians. */ + public double lngRadians() { + return lngRadians; + } + + /** Returns the longitude of this point as degrees. */ + public double lngDegrees() { + return 180.0 / Math.PI * lngRadians; + } + + /** + * Return true if the latitude is between -90 and 90 degrees inclusive and the longitude is + * between -180 and 180 degrees inclusive. + */ + public boolean isValid() { + return Math.abs(latRadians) <= S2.M_PI_2 && Math.abs(lngRadians) <= S2.M_PI; + } + + /** + * Returns a new S2LatLng based on this instance for which {@link #isValid()} will be {@code + * true}. + * + *
      + *
    • Latitude is clipped to the range {@code [-90, 90]} + *
    • Longitude is normalized to be in the range {@code [-180, 180]} + *
    + * + *

    If the current point is valid then the returned point will have the same coordinates. + */ + @CheckReturnValue + public S2LatLng normalized() { + // drem(x, 2 * S2.M_PI) reduces its argument to the range + // [-S2.M_PI, S2.M_PI] inclusive, which is what we want here. + return new S2LatLng( + Math.max(-S2.M_PI_2, Math.min(S2.M_PI_2, latRadians)), + Platform.IEEEremainder(lngRadians, 2 * S2.M_PI)); + } + + // Clamps the latitude to the range [-90, 90] degrees, and adds or subtracts + // a multiple of 360 degrees to the longitude if necessary to reduce it to + // the range [-180, 180]. + + /** Convert an S2LatLng to the equivalent unit-length vector (S2Point). */ + public S2Point toPoint() { + double phi = latRadians; + double theta = lngRadians; + double cosphi = Math.cos(phi); + return new S2Point(Math.cos(theta) * cosphi, Math.sin(theta) * cosphi, Math.sin(phi)); + } + + /** Return the distance (measured along the surface of the sphere) to the given point. */ + public S1Angle getDistance(final S2LatLng o) { + // This implements the Haversine formula, which is numerically stable for + // small distances but only gets about 8 digits of precision for very large + // distances (e.g. antipodal points). Note that 8 digits is still accurate + // to within about 10cm for a sphere the size of the Earth. + // + // This could be fixed with another sin() and cos() below, but at that point + // you might as well just convert both arguments to S2Points and compute the + // distance that way (which gives about 15 digits of accuracy for all + // distances). + + // assert isValid(); + // assert o.isValid(); + + double lat1 = latRadians; + double lat2 = o.latRadians; + double lng1 = lngRadians; + double lng2 = o.lngRadians; + double dlat = Math.sin(0.5 * (lat2 - lat1)); + double dlng = Math.sin(0.5 * (lng2 - lng1)); + double x = dlat * dlat + dlng * dlng * Math.cos(lat1) * Math.cos(lat2); + return S1Angle.radians(2 * Math.asin(Math.sqrt(Math.min(1.0, x)))); + } + + /** Returns the surface distance to the given point assuming a constant radius. */ + public double getDistance(final S2LatLng o, double radius) { + return getDistance(o).distance(radius); + } + + /** + * Adds the given point to this point. Note that there is no guarantee that the new point will be + * valid. + */ + @CheckReturnValue + public S2LatLng add(final S2LatLng o) { + return new S2LatLng(latRadians + o.latRadians, lngRadians + o.lngRadians); + } + + /** + * Subtracts the given point from this point. Note that there is no guarantee that the new point + * will be valid. + */ + @CheckReturnValue + public S2LatLng sub(final S2LatLng o) { + return new S2LatLng(latRadians - o.latRadians, lngRadians - o.lngRadians); + } + + /** + * Scales this point by the given scaling factor. Note that there is no guarantee that the new + * point will be valid. + */ + @CheckReturnValue + public S2LatLng mul(final double m) { + return new S2LatLng(latRadians * m, lngRadians * m); + } + + @Override + public boolean equals(Object that) { + if (that instanceof S2LatLng) { + S2LatLng o = (S2LatLng) that; + return (latRadians == o.latRadians) && (lngRadians == o.lngRadians); + } + return false; + } + + @Override + public int hashCode() { + long value = 17; + value += 37 * value + Double.doubleToLongBits(latRadians); + value += 37 * value + Double.doubleToLongBits(lngRadians); + return (int) (value ^ (value >>> 32)); + } + + /** + * Returns true if both the latitude and longitude of the given point are within {@code maxError} + * radians of this point. + */ + public boolean approxEquals(S2LatLng o, double maxError) { + return (Math.abs(latRadians - o.latRadians) < maxError) + && (Math.abs(lngRadians - o.lngRadians) < maxError); + } + + /** + * Returns true if the given point is within {@code 1e-9} radians of this point. This corresponds + * to a distance of less than {@code 1cm} at the surface of the Earth. + */ + public boolean approxEquals(S2LatLng o) { + return approxEquals(o, 1e-9); + } + + @Override + public String toString() { + return "(" + latRadians + ", " + lngRadians + ")"; + } + + public String toStringDegrees() { + return "(" + latDegrees() + ", " + lngDegrees() + ")"; + } +} diff --git a/core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/S2LatLngRect.java b/core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/S2LatLngRect.java new file mode 100644 index 0000000000..e1ba8a964b --- /dev/null +++ b/core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/S2LatLngRect.java @@ -0,0 +1,578 @@ +/* + * Copyright 2005 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.mogo.eagle.core.utilcode.geometry; + +import com.google.common.annotations.GwtCompatible; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import javax.annotation.CheckReturnValue; + +/** + * S2LatLngRect represents a latitude-longitude rectangle. It is capable of representing the empty + * and full rectangles as well as single points. Rectangles may be constructed via a {@link + * Builder}, or more commonly via {@link S2Region#getRectBound()}. + * + *

    Note that the latitude-longitude space is considered to have a cylindrical + * topology rather than a spherical one, i.e. the poles have multiple lat/lng representations. An + * S2LatLngRect may be defined so that it includes some representations of a pole but not others. + * Use the polarClosure() method if you want to expand a rectangle so that it contains all possible + * representations of any contained poles. + * + *

    Because S2LatLngRect uses S1Interval to store the longitude range, longitudes of -180 degrees + * are treated specially. Except for empty and full longitude spans, -180 degree longitudes will + * turn into +180 degrees. This sign flip causes lng.lo() to be greater than lng.hi(), indicating + * that the rectangle will wrap around through -180 instead of through +179. Thus the math is + * consistent with the library, but the sign flip can be surprising, especially when working with + * map projections where -180 and +180 are at opposite ends of the flattened map. See {@link + * S1Interval} for more details. + * + *

    S2LatLngRect is immutable, so all methods that change the boundary return a new instance. To + * efficiently make numerous alterations to the bounds, use a {@link Builder} instead, mutate it to + * compute the desired bounds, and then call {@link Builder#build()} to convert it to an immutable + * S2LatLngRect. + * + */ +@GwtCompatible(serializable = true) +public final strictfp class S2LatLngRect extends S2LatLngRectBase { + + /** Version number of the lossless encoding format for S2LatLngRect. */ + private static final byte LOSSLESS_ENCODING_VERSION = 1; + + /** + * The canonical empty rectangle, as derived from the empty R1 and S1 intervals. Empty: lat.lo = + * 1, lat.hi = 0, lng.lo = Pi, lng.hi = -Pi (radians). + */ + public static S2LatLngRect empty() { + return new S2LatLngRect(R1Interval.empty(), S1Interval.empty()); + } + + /** The canonical full rectangle. */ + public static S2LatLngRect full() { + return new S2LatLngRect(fullLat(), fullLng()); + } + + /** The full allowable range of latitudes. */ + public static R1Interval fullLat() { + return new R1Interval(-S2.M_PI_2, S2.M_PI_2); + } + + /** The full allowable range of longitudes. */ + public static S1Interval fullLng() { + return S1Interval.full(); + } + + /** + * Constructs a rectangle of the given size centered around the given point. {@code center} needs + * to be normalized, but {@code size} does not. The latitude interval of the result is clamped to + * [-90, 90] degrees, and the longitude interval of the result is full() 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, 100], 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] + */ + public static S2LatLngRect fromCenterSize(S2LatLng center, S2LatLng size) { + return fromPoint(center).expanded(size.mul(0.5)); + } + + /** Convenience method to construct a rectangle containing a single point. */ + public static S2LatLngRect fromPoint(S2LatLng p) { + // assert (p.isValid()); + return new S2LatLngRect(p, p); + } + + /** + * Convenience method to construct the minimal bounding rectangle containing the two given + * normalized points. This is equivalent to starting with an empty rectangle and calling + * addPoint() twice. Note that it is different than the {@link #S2LatLngRect(S2LatLng, S2LatLng)} + * constructor, where the first point is always used as the lower-left corner of the resulting + * rectangle. + */ + public static S2LatLngRect fromPointPair(S2LatLng p1, S2LatLng p2) { + // assert (p1.isValid() && p2.isValid()); + return new S2LatLngRect( + R1Interval.fromPointPair(p1.lat().radians(), p2.lat().radians()), + S1Interval.fromPointPair(p1.lng().radians(), p2.lng().radians())); + } + + /** + * Returns a latitude-longitude rectangle that contains the edge from "a" to "b". Both points must + * be unit-length. Note that the bounding rectangle of an edge can be larger than the bounding + * rectangle of its endpoints. + */ + public static S2LatLngRect fromEdge(S2Point a, S2Point b) { + // assert (S2.isUnitLength(a) && S2.isUnitLength(b)); + S2LatLngRect r = fromPointPair(new S2LatLng(a), new S2LatLng(b)); + + // Check whether the min/max latitude occurs in the edge interior. We find the normal to the + // plane containing AB, and then a vector "dir" in this plane that also passes through the + // equator. We use RobustCrossProd to ensure that the edge normal is accurate even when the two + // points are very close together. + S2Point ab = S2.robustCrossProd(a, b); + S2Point dir = S2Point.crossProd(ab, S2Point.Z_POS); + double da = dir.dotProd(a); + double db = dir.dotProd(b); + if (da * db >= 0) { + // Minimum and maximum latitude are attained at the vertices. + return r; + } + // Minimum/maximum latitude occurs in the edge interior. This affects the latitude bounds but + // not the longitude bounds. + double absLat = Math.acos(Math.abs(ab.z / ab.norm())); + if (da < 0) { + return new S2LatLngRect(new R1Interval(r.lat().lo(), absLat), r.lng()); + } else { + return new S2LatLngRect(new R1Interval(-absLat, r.lat().hi()), r.lng()); + } + } + + /** + * Constructs a rectangle from minimum and maximum latitudes and longitudes. If {@code lo.lng() > + * hi.lng()}, the rectangle spans the 180 degree longitude line. Both points must be normalized, + * with {@code lo.lat() <= hi.lat()}. The rectangle contains all the points p such that {@code lo + * <= p && p <= hi}, where '<=' is defined in the obvious way. + */ + public S2LatLngRect(final S2LatLng lo, final S2LatLng hi) { + super(lo, hi); + // assert (isValid()); + } + + /** Constructs a rectangle from latitude and longitude intervals. */ + public S2LatLngRect(R1Interval lat, S1Interval lng) { + super(lat, lng); + // assert (isValid()); + } + + /** Creates a new S2LatLngRect as a copy of {@code b}. */ + public S2LatLngRect(S2LatLngRectBase b) { + lat.setLo(b.lat.lo()); + lat.setHi(b.lat.hi()); + lng.set(b.lng.lo(), b.lng.hi(), true); + } + + @Override + public final R1Interval lat() { + // It is OK to return the instance field because S2LatLngRect won't mutate its 'lat' field. + return lat; + } + + @Override + public final S1Interval lng() { + // It is OK to return the instance field because S2LatLngRect won't mutate its 'lng' field. + return lng; + } + + /** Returns a new {@link Builder} initialized as a copy of {@code r}. */ + public Builder toBuilder() { + return new Builder(this); + } + + /** + * Returns a new rectangle that includes this rectangle and the given point, expanding this + * rectangle to include the point by the minimum amount possible. + */ + @CheckReturnValue + public S2LatLngRect addPoint(S2Point p) { + return addPoint(new S2LatLng(p)); + } + + /** + * Returns a new rectangle that includes this rectangle and the given S2LatLng, expanding this + * rectangle to include the point by the minimum amount possible. The S2LatLng argument must be + * normalized. + */ + @CheckReturnValue + public S2LatLngRect addPoint(S2LatLng ll) { + // assert (ll.isValid()); + R1Interval newLat = lat.addPoint(ll.lat().radians()); + S1Interval newLng = lng.addPoint(ll.lng().radians()); + return new S2LatLngRect(newLat, newLng); + } + + /** + * Returns a rectangle that contains all points whose latitude distance from this rectangle is at + * most margin.lat(), and whose longitude distance from this rectangle is at most margin.lng(). In + * particular, latitudes are clamped while longitudes are wrapped. Note that any expansion of an + * empty interval remains empty, and both components of the given margin must be non-negative. + * + *

    Note that if an expanded rectangle contains a pole, it may not contain all possible lat/lng + * representations of that pole. Use polarClosure() if you do not want this behavior. + * + *

    NOTE: If you are trying to grow a rectangle by a certain *distance* on the sphere (e.g. + * 5km), use the convolveWithCap() method instead. + */ + @CheckReturnValue + public S2LatLngRect expanded(S2LatLng margin) { + // assert (margin.lat().radians() >= 0 && margin.lng().radians() >= 0); + return new S2LatLngRect( + lat.expanded(margin.lat().radians()).intersection(fullLat()), + lng.expanded(margin.lng().radians())); + } + + /** + * Expands this rectangle so that it contains all points within the given distance of the + * boundary, and return the smallest such rectangle. If the distance is negative, then instead + * shrinks this rectangle so that it excludes all points within the given absolute distance of the + * boundary, and returns the largest such rectangle. + * + *

    Unlike {@link #expanded}, this method treats the rectangle as a set of points on the sphere, + * and measures distances on the sphere. For example, you can use this method to find a rectangle + * that contains all points within 5km of a given rectangle. Because this method uses the topology + * of the sphere, note the following: + * + *

      + *
    • The full and empty rectangles have no boundary on the sphere. Any expansion (positive or + * negative) of these rectangles leaves them unchanged. + *
    • Any rectangle that covers the full longitude range does not have an east or west + * boundary, therefore no expansion (positive or negative) will occur in that direction. + *
    • Any rectangle that covers the full longitude range and also includes a pole will not be + * expanded or contracted at that pole, because it does not have a boundary there. + *
    • If a rectangle is within the given distance of a pole, the result will include the full + * longitude range (because all longitudes are present at the poles). + *
    + * + *

    Expansion and contraction are defined such that they are inverses whenever possible, i.e. + * + *

    {@code rect.expandedByDistance(x).expandedByDistance(-x) == rect} + * + *

    (approximately), so long as the first operation does not cause a rectangle boundary to + * disappear (i.e., the longitude range newly becomes full or empty, or the latitude range expands + * to include a pole). + */ + @CheckReturnValue + public S2LatLngRect expandedByDistance(S1Angle distance) { + if (distance.radians() >= 0) { + // The most straightforward approach is to build a cap centered on each vertex and take the + // union of all the bounding rectangles (including the original rectangle; this is necessary + // for very large rectangles). + // TODO(user): Update this code to use an algorithm like the one below. + S1ChordAngle radius = S1ChordAngle.fromS1Angle(distance); + Builder r = toBuilder(); + for (int k = 0; k < 4; ++k) { + r.union(S2Cap.fromAxisChord(getVertex(k).toPoint(), radius).getRectBound()); + } + return r.build(); + } else { + // Shrink the latitude interval unless the latitude interval contains a pole and the longitude + // interval is full, in which case the rectangle has no boundary at that pole. + R1Interval full = fullLat(); + R1Interval latResult = new R1Interval( + lat().lo() <= full.lo() && lng().isFull() ? full.lo() : lat().lo() - distance.radians(), + lat().hi() >= full.hi() && lng().isFull() ? full.hi() : lat().hi() + distance.radians()); + if (latResult.isEmpty()) { + return S2LatLngRect.empty(); + } + + // Maximum absolute value of a latitude in lat_result. At this latitude, the cap occupies the + // largest longitude interval. + double maxAbsLat = Math.max(-latResult.lo(), latResult.hi()); + + // Compute the largest longitude interval that the cap occupies. We use the law of sines for + // spherical triangles. For the details, see S2Cap.getRectBound(). + // + // When sin_a >= sin_c, the cap covers all the latitudes. + double aSin = Math.sin(-distance.radians()); + double cSin = Math.cos(maxAbsLat); + double maxLngMargin = aSin < cSin ? Math.asin(aSin / cSin) : S2.M_PI_2; + S1Interval lngResult = lng().expanded(-maxLngMargin); + if (lngResult.isEmpty()) { + return S2LatLngRect.empty(); + } + + return new S2LatLngRect(latResult, lngResult); + } + } + + /** + * If the rectangle does not include either pole, return it unmodified. Otherwise expand the + * longitude range to full() so that the rectangle contains all possible representations of the + * contained pole(s). + */ + @CheckReturnValue + public S2LatLngRect polarClosure() { + if (lat.lo() == -S2.M_PI_2 || lat.hi() == S2.M_PI_2) { + return new S2LatLngRect(lat, S1Interval.full()); + } else { + return this; + } + } + + /** + * Returns the smallest rectangle containing the union of this rectangle and the given rectangle. + */ + @CheckReturnValue + public S2LatLngRect union(S2LatLngRectBase other) { + return new S2LatLngRect(lat.union(other.lat), lng.union(other.lng)); + } + + /** + * 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. + */ + @CheckReturnValue + public S2LatLngRect intersection(S2LatLngRectBase other) { + R1Interval intersectLat = lat.intersection(other.lat); + S1Interval intersectLng = lng.intersection(other.lng); + if (intersectLat.isEmpty() || intersectLng.isEmpty()) { + // The lat/lng ranges must either be both empty or both non-empty. + return empty(); + } + return new S2LatLngRect(intersectLat, intersectLng); + } + + /** + * Returns a rectangle that contains the convolution of this rectangle with a cap of the given + * angle. This expands the rectangle by a fixed distance (as opposed to growing the rectangle in + * latitude-longitude space). The returned rectangle includes all points whose minimum distance to + * the original rectangle is at most the given angle. + */ + @CheckReturnValue + public S2LatLngRect convolveWithCap(S1Angle angle) { + Builder builder = toBuilder(); + builder.convolveWithCap(angle); + return builder.build(); + } + + // NOTE: This should be marked as @Override, but clone() isn't present in GWT's version of Object, + // so we can't mark it as such. + @SuppressWarnings("MissingOverride") + public S2Region clone() { + return new S2LatLngRect(this.lo(), this.hi()); + } + + @Override + public S2LatLngRect getRectBound() { + return this; + } + + /** + * Encodes this {@link S2LatLngRect} into an efficient, lossless binary representation, which can + * be decoded by calling {@link S2LatLngRect#decode}. The encoding is byte-compatible with the C++ + * version of the S2 library. + * + * @param output The output stream into which the encoding should be written. + * @throws IOException if there was a problem writing into the output stream. + */ + public void encode(OutputStream output) throws IOException { + encode(new LittleEndianOutput(output)); + } + + void encode(LittleEndianOutput encoder) throws IOException { + encoder.writeByte(LOSSLESS_ENCODING_VERSION); + encoder.writeDouble(lat().lo()); + encoder.writeDouble(lat().hi()); + encoder.writeDouble(lng().lo()); + encoder.writeDouble(lng().hi()); + } + + /** + * Decodes an {@link S2LatLngRect} that was encoded using {@link S2LatLngRect#encode}. + * + * @param input The input stream containing the encoded rectangle data. + * @return the decoded {@link S2LatLngRect}. + * @throws IOException if there was a problem reading from the input stream, or the contents are + * malformed. + */ + public static S2LatLngRect decode(InputStream input) throws IOException { + return decode(new LittleEndianInput(input)); + } + + static S2LatLngRect decode(LittleEndianInput decoder) throws IOException { + byte version = decoder.readByte(); + if (version != LOSSLESS_ENCODING_VERSION) { + throw new IOException("Unsupported S2LatLngRect encoding version " + version); + } + double latLo = decoder.readDouble(); + double latHi = decoder.readDouble(); + R1Interval lat = new R1Interval(latLo, latHi); + double lngLo = decoder.readDouble(); + double lngHi = decoder.readDouble(); + S1Interval lng = new S1Interval(lngLo, lngHi); + S2LatLngRect bound = new S2LatLngRect(lat, lng); + if (!bound.isValid()) { + throw new IOException("Decoded S2LatLngRect is invalid."); + } + return bound; + } + + /** + * This class is a builder for S2LatLngRect instances. This is much more efficient when creating + * the bounds from numerous operations, as it ensures that the S2LatLngRect is only created once. + * + *

    Example usage: + * + *

    {@code S2LatLngRect union(List points) { S2LatLngRect.Builder builder = new + * S2LatLngRect.Builder(); for (S2LatLng point : points) { builder.addPoint(point); } return + * builder.build(); } } + */ + public static final strictfp class Builder extends S2LatLngRectBase { + public Builder(final S2LatLng lo, final S2LatLng hi) { + super(lo, hi); + } + + public Builder(R1Interval lat, S1Interval lng) { + super(lat, lng); + } + + /** Creates a new S2LatLngRect.Builder as a copy of {@code b}. */ + public Builder(S2LatLngRectBase b) { + lat.setLo(b.lat.lo()); + lat.setHi(b.lat.hi()); + lng.set(b.lng.lo(), b.lng.hi(), true); + } + + @Override + public final R1Interval lat() { + // 'lat' is copied here to avoid further changes in the builder being visible in the returned + // object. + return new R1Interval(lat); + } + + @Override + public final S1Interval lng() { + // 'lng' is copied here to avoid further changes in the builder being visible in the returned + // object. + return new S1Interval(lng); + } + + /** Returns a new immutable S2LatLngRect copied from the current state of this builder. */ + public S2LatLngRect build() { + return new S2LatLngRect(new R1Interval(lat), new S1Interval(lng)); + } + + /** A builder initialized to be empty (such that it doesn't contain anything). */ + public static Builder empty() { + return new Builder(R1Interval.empty(), S1Interval.empty()); + } + + /** Sets the rectangle to the full rectangle. */ + public Builder setFull() { + lat.set(-S2.M_PI_2, S2.M_PI_2); + lng.setFull(); + return this; + } + + public Builder addPoint(S2Point p) { + addPoint(new S2LatLng(p)); + return this; + } + + /** + * Increases the size of the bounding rectangle to include the given point. The rectangle is + * expanded by the minimum amount possible. + */ + public Builder addPoint(S2LatLng ll) { + // assert (ll.isValid()); + lat.unionInternal(ll.lat().radians()); + lng.unionInternal(S1Interval.fromPoint(ll.lng().radians())); + return this; + } + + /** + * Mutates the rectangle to contain all points whose latitude distance from this rectangle is at + * most margin.lat(), and whose longitude distance from this rectangle is at most margin.lng(). + * In particular, latitudes are clamped while longitudes are wrapped. Note that any expansion of + * an empty interval remains empty, and both components of the given margin must be + * non-negative. + * + *

    NOTE: If you are trying to grow a rectangle by a certain *distance* on the sphere (e.g. + * 5km), use the convolveWithCap() method instead. + */ + public Builder expanded(S2LatLng margin) { + // assert (margin.lat().radians() >= 0 && margin.lng().radians() >= 0); + lat.expandedInternal(margin.lat().radians()); + lat.intersectionInternal(fullLat()); + lng.expandedInternal(margin.lng().radians()); + return this; + } + + /** + * If the rectangle does not include either pole, leave it unmodified. Otherwise expand the + * longitude range to full() so that the rectangle contains all possible representations of the + * contained pole(s). + */ + public Builder polarClosure() { + if (lat.lo() == -S2.M_PI_2 || lat.hi() == S2.M_PI_2) { + lng.setFull(); + } + return this; + } + + /** + * Mutates this rectangle to be the smallest rectangle containing the union of the current and + * given rectangles. + */ + public Builder union(S2LatLngRect other) { + lat.unionInternal(other.lat); + lng.unionInternal(other.lng); + return this; + } + + /** + * Mutates this rectangle to be the smallest rectangle containing the intersection of the + * current and given rectangles. Note that the region of intersection may consist of two + * disjoint rectangles, in which case we set the rectangle to be a single rectangle spanning + * both of them. + */ + public Builder intersection(S2LatLngRect other) { + lat.intersectionInternal(other.lat); + lng.intersectionInternal(other.lng); + // The lat/lng ranges must either be both empty or both non-empty. + if (lat.isEmpty() && !lng.isEmpty()) { + lng.setEmpty(); + } else if (lng.isEmpty() && !lat.isEmpty()) { + lat.setEmpty(); + } + return this; + } + + /** + * Mutates the current rectangle to contain the convolution of this rectangle with a cap of the + * given angle. This expands the rectangle by a fixed distance (as opposed to growing the + * rectangle in latitude-longitude space). The new rectangle includes all points whose minimum + * distance to the original rectangle is at most the given angle. + */ + public Builder convolveWithCap(S1Angle angle) { + S1ChordAngle r = S1ChordAngle.fromS1Angle(angle); + // Make a local copy of the original coordinates. + double latLo = lat.lo(); + double latHi = lat.hi(); + double lngLo = lng.lo(); + double lngHi = lng.hi(); + union(S2Cap.fromAxisChord(S2LatLng.fromRadians(latLo, lngLo).toPoint(), r).getRectBound()); + union(S2Cap.fromAxisChord(S2LatLng.fromRadians(latLo, lngHi).toPoint(), r).getRectBound()); + union(S2Cap.fromAxisChord(S2LatLng.fromRadians(latHi, lngLo).toPoint(), r).getRectBound()); + union(S2Cap.fromAxisChord(S2LatLng.fromRadians(latHi, lngHi).toPoint(), r).getRectBound()); + return this; + } + + // NOTE: This should be marked as @Override, but clone() isn't present in GWT's version of + // Object, so we can't mark it as such. + @SuppressWarnings("MissingOverride") + public S2Region clone() { + return new S2LatLngRect(this.lo(), this.hi()); + } + + @Override + public S2LatLngRect getRectBound() { + return build(); + } + } +} diff --git a/core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/S2LatLngRectBase.java b/core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/S2LatLngRectBase.java new file mode 100644 index 0000000000..3ee5b1d10a --- /dev/null +++ b/core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/S2LatLngRectBase.java @@ -0,0 +1,818 @@ +/* + * Copyright 2005 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.mogo.eagle.core.utilcode.geometry; + +import com.google.common.annotations.GwtCompatible; +import com.google.common.base.Preconditions; +import java.io.Serializable; + +/** + * Base class for methods shared between the immutable {@link S2LatLngRect} and the mutable {@link + * S2LatLngRect.Builder}. + */ +@GwtCompatible(serializable = false) +public abstract strictfp class S2LatLngRectBase implements S2Region, Serializable { + protected final R1Interval lat; + protected final S1Interval lng; + + /** + * Constructs a rectangle from minimum and maximum latitudes and longitudes. If lo.lng() > + * hi.lng(), the rectangle spans the 180 degree longitude line. Both points must be normalized, + * with lo.lat() <= hi.lat(). The rectangle contains all the points p such that 'lo' <= p <= 'hi', + * where '<=' is defined in the obvious way. + */ + S2LatLngRectBase(final S2LatLng lo, final S2LatLng hi) { + lat = new R1Interval(lo.lat().radians(), hi.lat().radians()); + lng = new S1Interval(lo.lng().radians(), hi.lng().radians()); + // assert (isValid()); + } + + /** + * Constructs a rectangle from latitude and longitude intervals. The two intervals must either be + * both empty or both non-empty, and the latitude interval must not extend outside [-90, +90] + * degrees. Note that both intervals (and hence the rectangle) are closed. + */ + S2LatLngRectBase(R1Interval lat, S1Interval lng) { + this.lat = lat; + this.lng = lng; + // assert (isValid()); + } + + /** + * Constructs a rectangle with lat and lng fields set to empty intervals, as defined in {@link + * R1Interval} and {@link S1Interval}. + */ + S2LatLngRectBase() { + lat = R1Interval.empty(); + lng = S1Interval.empty(); + } + + /** + * Returns true if the rectangle is valid, which essentially just means that the latitude bounds + * do not exceed Pi/2 in absolute value and the longitude bounds do not exceed Pi in absolute + * value. Also, if either the latitude or longitude bound is empty then both must be. + */ + public final boolean isValid() { + // The lat/lng ranges must either be both empty or both non-empty. + return (Math.abs(lat.lo()) <= S2.M_PI_2 + && Math.abs(lat.hi()) <= S2.M_PI_2 + && lng.isValid() + && lat.isEmpty() == lng.isEmpty()); + } + + // Accessor methods. + public final S1Angle latLo() { + return S1Angle.radians(lat.lo()); + } + + public final S1Angle latHi() { + return S1Angle.radians(lat.hi()); + } + + public final S1Angle lngLo() { + return S1Angle.radians(lng.lo()); + } + + public final S1Angle lngHi() { + return S1Angle.radians(lng.hi()); + } + + /** Returns the latitude range of this rectangle. */ + public abstract R1Interval lat(); + + /** Returns the longitude range of this rectangle. */ + public abstract S1Interval lng(); + + public final S2LatLng lo() { + return new S2LatLng(latLo(), lngLo()); + } + + public final S2LatLng hi() { + return new S2LatLng(latHi(), lngHi()); + } + + /** Returns true if the rectangle is empty, i.e. it contains no points at all. */ + public final boolean isEmpty() { + return lat.isEmpty(); + } + + /** Returns true if the rectangle is full, i.e. it contains all points. */ + public final boolean isFull() { + return lat.equals(S2LatLngRect.fullLat()) && lng.isFull(); + } + + /** Returns true if the rectangle is a point, i.e. lo() == hi() */ + public final boolean isPoint() { + return lat().lo() == lat().hi() && lng.lo() == lng().hi(); + } + + /** + * Returns true if lng_.lo() > lng_.hi(), i.e. the rectangle crosses the 180 degree latitude line. + */ + public final boolean isInverted() { + return lng.isInverted(); + } + + /** + * Returns the kth vertex of the rectangle (k = 0,1,2,3) in CCW order (lower-left, + * lower right, upper right, upper left). + */ + public final S2LatLng getVertex(int k) { + switch (k) { + case 0: + return S2LatLng.fromRadians(lat.lo(), lng.lo()); + case 1: + return S2LatLng.fromRadians(lat.lo(), lng.hi()); + case 2: + return S2LatLng.fromRadians(lat.hi(), lng.hi()); + case 3: + return S2LatLng.fromRadians(lat.hi(), lng.lo()); + default: + throw new IllegalArgumentException("Invalid vertex index."); + } + } + + /** + * Returns the center of the rectangle in latitude-longitude space (in general this is not the + * center of the region on the sphere). + */ + public final S2LatLng getCenter() { + return S2LatLng.fromRadians(lat.getCenter(), lng.getCenter()); + } + + /** + * Returns the minimum distance (measured along the surface of the sphere) from a given point to + * the rectangle (both its boundary and its interior). The latLng must be valid. + */ + public final S1Angle getDistance(S2LatLng p) { + // The algorithm here is the same as in getDistance(S2LatLngRect), only with simplified + // calculations. + S2LatLngRectBase a = this; + + Preconditions.checkState(!a.isEmpty()); + Preconditions.checkArgument(p.isValid()); + + if (a.lng().contains(p.lng().radians())) { + return S1Angle.radians( + Math.max( + 0.0, Math.max(p.lat().radians() - a.lat().hi(), a.lat().lo() - p.lat().radians()))); + } + + S1Interval interval = new S1Interval(a.lng().hi(), a.lng().complement().getCenter()); + double aLng = a.lng().lo(); + if (interval.contains(p.lng().radians())) { + aLng = a.lng().hi(); + } + + S2Point lo = S2LatLng.fromRadians(a.lat().lo(), aLng).toPoint(); + S2Point hi = S2LatLng.fromRadians(a.lat().hi(), aLng).toPoint(); + S2Point loCrossHi = S2LatLng.fromRadians(0, aLng - S2.M_PI_2).normalized().toPoint(); + return S2EdgeUtil.getDistance(p.toPoint(), lo, hi, loCrossHi); + } + + /** + * Returns the minimum distance (measured along the surface of the sphere) to the given + * S2LatLngRectBase. Both S2LatLngRectBases must be non-empty. + */ + public final S1Angle getDistance(S2LatLngRectBase other) { + S2LatLngRectBase a = this; + S2LatLngRectBase b = other; + + Preconditions.checkState(!a.isEmpty()); + Preconditions.checkArgument(!b.isEmpty()); + + // First, handle the trivial cases where the longitude intervals overlap. + if (a.lng().intersects(b.lng())) { + if (a.lat().intersects(b.lat())) { + // Intersection between a and b. + return S1Angle.radians(0); + } + + // We found an overlap in the longitude interval, but not in the latitude interval. This means + // the shortest path travels along some line of longitude connecting the high-latitude of the + // lower rect with the low-latitude of the higher rect. + S1Angle lo; + S1Angle hi; + if (a.lat().lo() > b.lat().hi()) { + lo = b.latHi(); + hi = a.latLo(); + } else { + lo = a.latHi(); + hi = b.latLo(); + } + return S1Angle.radians(hi.radians() - lo.radians()); + } + + // The longitude intervals don't overlap. In this case, the closest points occur somewhere on + // the pair of longitudinal edges which are nearest in longitude-space. + S1Angle aLng; + S1Angle bLng; + S1Interval loHi = S1Interval.fromPointPair(a.lng().lo(), b.lng().hi()); + S1Interval hiLo = S1Interval.fromPointPair(a.lng().hi(), b.lng().lo()); + if (loHi.getLength() < hiLo.getLength()) { + aLng = a.lngLo(); + bLng = b.lngHi(); + } else { + aLng = a.lngHi(); + bLng = b.lngLo(); + } + + // The shortest distance between the two longitudinal segments will include at least one segment + // endpoint. We could probably narrow this down further to a single point-edge distance by + // comparing the relative latitudes of the endpoints, but for the sake of clarity, we'll do all + // four point-edge distance tests. + S2Point aLo = new S2LatLng(a.latLo(), aLng).toPoint(); + S2Point aHi = new S2LatLng(a.latHi(), aLng).toPoint(); + S2Point aLoCrossHi = S2LatLng.fromRadians(0, aLng.radians() - S2.M_PI_2).normalized().toPoint(); + S2Point bLo = new S2LatLng(b.latLo(), bLng).toPoint(); + S2Point bHi = new S2LatLng(b.latHi(), bLng).toPoint(); + S2Point bLoCrossHi = S2LatLng.fromRadians(0, bLng.radians() - S2.M_PI_2).normalized().toPoint(); + + return S1Angle.min( + S2EdgeUtil.getDistance(aLo, bLo, bHi, bLoCrossHi), + S1Angle.min( + S2EdgeUtil.getDistance(aHi, bLo, bHi, bLoCrossHi), + S1Angle.min( + S2EdgeUtil.getDistance(bLo, aLo, aHi, aLoCrossHi), + S2EdgeUtil.getDistance(bHi, aLo, aHi, aLoCrossHi)))); + } + + /** + * Returns the undirected Hausdorff distance (measured along the surface of the sphere) to the + * given S2LatLngRect. The directed Hausdorff distance from rectangle A to rectangle B is given by + * {@code h(A, B) = max_{p in A} min_{q in B} d(p, q)}. The Hausdorff distance between rectangle A + * and rectangle B is given by {@code H(A, B) = max{h(A, B), h(B, A)}}. + */ + public final S1Angle getHausdorffDistance(S2LatLngRectBase other) { + return S1Angle.max( + getDirectedHausdorffDistance(other), other.getDirectedHausdorffDistance(this)); + } + + /** + * Returns the directed Hausdorff distance (measured along the surface of the sphere) to the given + * S2LatLngRect. The directed Hausdorff distance from rectangle A to rectangle B is given by + * {@code h(A, B) = max_{p in A} min_{q in B} d(p, q)}. The Hausdorff distance between rectangle A + * and rectangle B is given by {@code H(A, B) = max{h(A, B), h(B, A)}}. + */ + public final S1Angle getDirectedHausdorffDistance(S2LatLngRectBase other) { + if (isEmpty()) { + return S1Angle.radians(0); + } + if (other.isEmpty()) { + return S1Angle.radians(S2.M_PI); // maximum possible distance on S2 + } + + double lngDistance = lng().getDirectedHausdorffDistance(other.lng()); + // assert lngDistance >= 0; + return getDirectedHausdorffDistance(lngDistance, lat(), other.lat()); + } + + /** + * Return the directed Hausdorff distance from one longitudinal edge spanning latitude range + * {@code a_lat} to the other longitudinal edge spanning latitude range {@code b_lat}, with their + * longitudinal difference given by {@code lngDiff}. + */ + private static S1Angle getDirectedHausdorffDistance(double lngDiff, R1Interval a, R1Interval b) { + // By symmetry, we can assume a's longitude is 0 and b's longitude is lngDiff. Call b's two + // endpoints bLo and bHi. Let H be the hemisphere containing a and delimited by the longitude + // line of b. The Voronoi diagram of b on H has three edges (portions of great circles) all + // orthogonal to b and meeting at bLo cross bHi. + // + // E1: (bLo, bLo cross bHi) + // E2: (bHi, bLo cross bHi) + // E3: (-b_mid, bLo cross bHi), where b_mid is the midpoint of b + // + // They subdivide H into three Voronoi regions. Depending on how longitude 0 (which contains + // edge a) intersects these regions, we distinguish two cases: + // Case 1: it intersects three regions. This occurs when lngDiff <= M_PI_2. + // Case 2: it intersects only two regions. This occurs when lngDiff > M_PI_2. + // + // In the first case, the directed Hausdorff distance to edge b can only be realized by the + // following points on a: + // A1: two endpoints of a. + // A2: intersection of a with the equator, if b also intersects the equator. + // + // In the second case, the directed Hausdorff distance to edge b can only be realized by the + // following points on a: + // B1: two endpoints of a. + // B2: intersection of a with E3 + // B3: farthest point from bLo to the interior of D, and farthest point from bHi to the interior + // of U, if any, where D (resp. U) is the portion of edge a below (resp. above) the intersection + // point from B2. + + // assert lngDiff >= 0; + // assert lngDiff <= S2.M_PI; + + if (lngDiff == 0) { + return S1Angle.radians(a.getDirectedHausdorffDistance(b)); + } + + // Assumed longitude of b. + double bLng = lngDiff; + // Two endpoints of b. + S2Point bLo = S2LatLng.fromRadians(b.lo(), bLng).toPoint(); + S2Point bHi = S2LatLng.fromRadians(b.hi(), bLng).toPoint(); + + // Handling of each case outlined at the top of the function starts here. + // This is initialized a few lines below. + S1Angle maxDistance; + + // Cases A1 and B1. + S2Point aLo = S2LatLng.fromRadians(a.lo(), 0).toPoint(); + S2Point aHi = S2LatLng.fromRadians(a.hi(), 0).toPoint(); + maxDistance = S2EdgeUtil.getDistance(aLo, bLo, bHi); + maxDistance = S1Angle.max(maxDistance, S2EdgeUtil.getDistance(aHi, bLo, bHi)); + + if (lngDiff <= S2.M_PI_2) { + // Case A2. + if (a.contains(0) && b.contains(0)) { + maxDistance = S1Angle.max(maxDistance, S1Angle.radians(lngDiff)); + } + } else { + // Case B2. + S2Point p = getBisectorIntersection(b, bLng); + double pLat = S2LatLng.latitude(p).radians(); + if (a.contains(pLat)) { + maxDistance = S1Angle.max(maxDistance, new S1Angle(p, bLo)); + } + + // Case B3. + if (pLat > a.lo()) { + maxDistance = + S1Angle.max( + maxDistance, + getInteriorMaxDistance(new R1Interval(a.lo(), Math.min(pLat, a.hi())), bLo)); + } + if (pLat < a.hi()) { + maxDistance = + S1Angle.max( + maxDistance, + getInteriorMaxDistance(new R1Interval(Math.max(pLat, a.lo()), a.hi()), bHi)); + } + } + + return maxDistance; + } + + // A vector orthogonal to longitude 0. + private static final S2Point ORTHO_LNG = S2Point.Y_NEG; + + /** + * Return the intersection of longitude 0 with the bisector of an edge on longitude 'lng' and + * spanning latitude range 'lat'. + */ + private static S2Point getBisectorIntersection(R1Interval lat, double lng) { + lng = Math.abs(lng); + double latCenter = lat.getCenter(); + + // A vector orthogonal to the bisector of the given longitudinal edge. + S2LatLng orthoBisector; + if (latCenter >= 0) { + orthoBisector = S2LatLng.fromRadians(latCenter - S2.M_PI_2, lng); + } else { + orthoBisector = S2LatLng.fromRadians(-latCenter - S2.M_PI_2, lng - S2.M_PI); + } + return S2.robustCrossProd(ORTHO_LNG, orthoBisector.toPoint()); + } + + /** + * Return max distance from a point b to the segment spanning latitude range aLat on longitude 0, + * if the max occurs in the interior of aLat. Otherwise return -1. + */ + private static S1Angle getInteriorMaxDistance(R1Interval aLat, S2Point b) { + // Longitude 0 is in the y=0 plane. b.x() >= 0 implies that the maximum + // does not occur in the interior of aLat. + if (aLat.isEmpty() || b.getX() >= 0) { + return S1Angle.radians(-1); + } + + // Project b to the y=0 plane. The antipodal of the normalized projection is + // the point at which the maxium distance from b occurs, if it is contained + // in aLat. + S2Point intersectionPoint = new S2Point(-b.getX(), 0, -b.getZ()).normalize(); + if (aLat.interiorContains(S2LatLng.latitude(intersectionPoint).radians())) { + return new S1Angle(b, intersectionPoint); + } else { + return S1Angle.radians(-1); + } + } + + /** + * Returns the width and height of this rectangle in latitude-longitude space. Empty rectangles + * have a negative width and height. + */ + public final S2LatLng getSize() { + return S2LatLng.fromRadians(lat.getLength(), lng.getLength()); + } + + // Returns the true centroid of the rectangle multiplied by its surface area (see s2centroids.h + // for details on centroids). The result is not unit length, so you may want to normalize it. + // Note that in general the centroid is *not* at the + // center of the rectangle, and in fact it may not even be contained by the rectangle. (It is the + // "center of mass" of the rectangle viewed as + // subset of the unit sphere, i.e. it is the point in space about which this curved shape would + // rotate.) + // + // The reason for multiplying the result by the rectangle area is to make it easier to compute the + // centroid of more complicated shapes. The centroid of a union of disjoint regions can be + // computed simply by adding their GetCentroid() results. + public final S2Point getCentroid() { + // When a sphere is divided into slices of constant thickness by a set of parallel planes, all + // slices have the same surface area. This implies that the z-component of the centroid is + // simply the midpoint of the z-interval spanned by the S2LatLngRect. + // + // Similarly, it is easy to see that the (x,y) of the centroid lies in the plane through the + // midpoint of the rectangle's longitude interval. We only need to determine the distance "d" + // of this point from the z-axis. + // + // Let's restrict our attention to a particular z-value. In this z-plane, the S2LatLngRect is a + // circular arc. The centroid of this arc lies on a radial line through the midpoint of the + // arc, and at a distance from the z-axis of + // + // r * (sin(alpha) / alpha) + // + // where r = sqrt(1-z^2) is the radius of the arc, and "alpha" is half of the arc length (i.e., + // the arc covers longitudes [-alpha, alpha]). + // + // To find the centroid distance from the z-axis for the entire rectangle, we just need to + // integrate over the z-interval. This gives + // + // d = Integrate[sqrt(1-z^2)*sin(alpha)/alpha, z1..z2] / (z2 - z1) + // + // where [z1, z2] is the range of z-values covered by the rectangle. This simplifies to + // + // d = sin(alpha)/(2*alpha*(z2-z1))*(z2*r2 - z1*r1 + theta2 - theta1) + // + // where [theta1, theta2] is the latitude interval, z1=sin(theta1), z2=sin(theta2), + // r1=cos(theta1), and r2=cos(theta2). + // + // Finally, we want to return not the centroid itself, but the centroid scaled by the area of + // the rectangle. The area of the rectangle is + // + // A = 2 * alpha * (z2 - z1) + // + // which fortunately appears in the denominator of "d". + + if (isEmpty()) { + return new S2Point(); + } + double z1 = Math.sin(latLo().radians()); + double z2 = Math.sin(latHi().radians()); + double r1 = Math.cos(latLo().radians()); + double r2 = Math.cos(latHi().radians()); + double alpha = 0.5 * lng.getLength(); + double r = Math.sin(alpha) * (r2 * z2 - r1 * z1 + lat.getLength()); + double lngCenter = lng.getCenter(); + double z = alpha * (z2 + z1) * (z2 - z1); // scaled by the area + return new S2Point(r * Math.cos(lngCenter), r * Math.sin(lngCenter), z); + } + + /** More efficient version of contains() that accepts a S2LatLng rather than an S2Point. */ + public final boolean contains(S2LatLng ll) { + // assert (ll.isValid()); + return (lat.contains(ll.latRadians()) && lng.contains(ll.lngRadians())); + } + + /** + * Returns true if and only if the given point is contained in the interior of the region (i.e. + * the region excluding its boundary). The point 'p' does not need to be normalized. + */ + public final boolean interiorContains(S2Point p) { + return interiorContains(new S2LatLng(p)); + } + + /** + * More efficient version of interiorContains() that accepts a S2LatLng rather than an S2Point. + */ + public final boolean interiorContains(S2LatLng ll) { + // assert (ll.isValid()); + return (lat.interiorContains(ll.lat().radians()) && lng.interiorContains(ll.lng().radians())); + } + + /** Returns true if and only if the rectangle contains the given other rectangle. */ + public final boolean contains(S2LatLngRectBase other) { + return lat.contains(other.lat) && lng.contains(other.lng); + } + + /** + * Returns true if and only if the interior of this rectangle contains all points of the given + * other rectangle (including its boundary). + */ + public final boolean interiorContains(S2LatLngRectBase other) { + return (lat.interiorContains(other.lat) && lng.interiorContains(other.lng)); + } + + /** Returns true if this rectangle and the given other rectangle have any points in common. */ + public final boolean intersects(S2LatLngRectBase other) { + return lat.intersects(other.lat) && lng.intersects(other.lng); + } + + /** + * Returns true if this rectangle intersects the given cell. (This is an exact test and may be + * fairly expensive, see also MayIntersect below.) + */ + public final boolean intersects(S2Cell cell) { + // First we eliminate the cases where one region completely contains the other. Once these are + // disposed of, then the regions will intersect if and only if their boundaries intersect. + + if (isEmpty()) { + return false; + } + if (contains(cell.getCenterRaw())) { + return true; + } + if (cell.contains(getCenter().toPoint())) { + return true; + } + + // Quick rejection test (not required for correctness). + if (!intersects(cell.getRectBound())) { + return false; + } + + // Now check whether the boundaries intersect. Unfortunately, a latitude-longitude rectangle + // does not have straight edges -- two edges are curved, and at least one of them is concave. + + // Precompute the cell vertices as points and latitude-longitudes. + S2Point[] cellV = new S2Point[4]; + S2LatLng[] cellLl = new S2LatLng[4]; + for (int i = 0; i < 4; ++i) { + // Must be normalized. + cellV[i] = cell.getVertex(i); + cellLl[i] = new S2LatLng(cellV[i]); + if (contains(cellLl[i])) { + // Quick acceptance test. + return true; + } + } + + for (int i = 0; i < 4; ++i) { + S1Interval edgeLng = + S1Interval.fromPointPair(cellLl[i].lng().radians(), cellLl[(i + 1) & 3].lng().radians()); + if (!lng.intersects(edgeLng)) { + continue; + } + + final S2Point a = cellV[i]; + final S2Point b = cellV[(i + 1) & 3]; + if (edgeLng.contains(lng.lo())) { + if (intersectsLngEdge(a, b, lat, lng.lo())) { + return true; + } + } + if (edgeLng.contains(lng.hi())) { + if (intersectsLngEdge(a, b, lat, lng.hi())) { + return true; + } + } + if (intersectsLatEdge(a, b, lat.lo(), lng)) { + return true; + } + if (intersectsLatEdge(a, b, lat.hi(), lng)) { + return true; + } + } + return false; + } + + /** + * Returns true if and only if the interior of this rectangle intersects any point (including the + * boundary) of the given other rectangle. + */ + public final boolean interiorIntersects(S2LatLngRectBase other) { + return (lat.interiorIntersects(other.lat) && lng.interiorIntersects(other.lng)); + } + + /** Returns true if the boundary of this rectangle intersects the given geodesic edge (v0, v1). */ + public final boolean boundaryIntersects(S2Point v0, S2Point v1) { + if (isEmpty()) { + return false; + } + if (!lng.isFull()) { + if (intersectsLngEdge(v0, v1, lat, lng.lo())) { + return true; + } + if (intersectsLngEdge(v0, v1, lat, lng.hi())) { + return true; + } + } + if (lat.lo() != -S2.M_PI_2 && intersectsLatEdge(v0, v1, lat.lo(), lng)) { + return true; + } + if (lat.hi() != S2.M_PI_2 && intersectsLatEdge(v0, v1, lat.hi(), lng)) { + return true; + } + return false; + } + + /** Returns the surface area of this rectangle on the unit sphere. */ + public final double area() { + if (isEmpty()) { + return 0; + } + + // This is the size difference of the two spherical caps, multiplied by the longitude ratio. + return lng().getLength() * Math.abs(Math.sin(latHi().radians()) - Math.sin(latLo().radians())); + } + + /** Returns true if these are the same type of rectangle and contain the same set of points. */ + @Override + public final boolean equals(Object that) { + if ((that == null) || this.getClass() != that.getClass()) { + return false; + } + S2LatLngRectBase otherRect = (S2LatLngRectBase) that; + return lat().equals(otherRect.lat()) && lng().equals(otherRect.lng()); + } + + /** + * Returns true if the latitude and longitude intervals of the two rectangles are the same up to + * the given tolerance. See {@link R1Interval} and {@link S1Interval} for details. + */ + public final boolean approxEquals(S2LatLngRectBase other, double maxError) { + return lat.approxEquals(other.lat, maxError) && lng.approxEquals(other.lng, maxError); + } + + /** Returns true if this rectangle is very nearly identical to the given other rectangle. */ + public final boolean approxEquals(S2LatLngRectBase other) { + return approxEquals(other, 1e-15); + } + + /** + * As {@link #approxEquals(S2LatLngRectBase, double)}, but with separate tolerances for latitude + * and longitude. + */ + public final boolean approxEquals(S2LatLngRectBase other, S2LatLng maxError) { + return lat.approxEquals(other.lat, maxError.lat().radians()) + && lng.approxEquals(other.lng, maxError.lng().radians()); + } + + @Override + public final int hashCode() { + int value = 17; + value = 37 * value + lat.hashCode(); + return (37 * value + lng.hashCode()); + } + + // ////////////////////////////////////////////////////////////////////// + // S2Region interface (see {@code S2Region} for details): + + @Override + public S2Cap getCapBound() { + // We consider two possible bounding caps, one whose axis passes through the center of the + // lat-lng rectangle and one whose axis is the north or south pole. We return the smaller of the + // two caps. + if (isEmpty()) { + return S2Cap.empty(); + } + + double poleZ; + double poleAngle; + if (lat.lo() + lat.hi() < 0) { + // South pole axis yields smaller cap. + poleZ = -1; + poleAngle = S2.M_PI_2 + lat.hi(); + } else { + poleZ = 1; + poleAngle = S2.M_PI_2 - lat.lo(); + } + S2Cap poleCap = S2Cap.fromAxisAngle(new S2Point(0, 0, poleZ), S1Angle.radians(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. + double lngSpan = lng.hi() - lng.lo(); + if (Platform.IEEEremainder(lngSpan, 2 * S2.M_PI) >= 0) { + if (lngSpan < 2 * S2.M_PI) { + S2Cap midCap = S2Cap.fromAxisAngle(getCenter().toPoint(), S1Angle.radians(0)); + for (int k = 0; k < 4; ++k) { + midCap = midCap.addPoint(getVertex(k).toPoint()); + } + if (midCap.height() < poleCap.height()) { + return midCap; + } + } + } + return poleCap; + } + + /** + * Returns true if this latitude/longitude region contains the given cell. A latitude-longitude + * rectangle contains a cell if and only if it contains the cell's bounding rectangle, making this + * an exact test. Note, however, that the cell must be valid; an error may result if e.g. {@link + * S2CellId#sentinel()} is passed here. + */ + @Override + public final boolean contains(S2Cell cell) { + return contains(cell.getRectBound()); + } + + /** + * This test is cheap but is NOT exact. Use Intersects() if you want a more accurate and more + * expensive test. Note that when this method is used by an S2RegionCoverer, the accuracy isn't + * all that important since if a cell may intersect the region then it is subdivided, and the + * accuracy of this method goes up as the cells get smaller. + */ + @Override + public final boolean mayIntersect(S2Cell cell) { + // This test is cheap but is NOT exact (see s2latlngrect.h). + return intersects(cell.getRectBound()); + } + + /** The point 'p' does not need to be normalized. */ + @Override + public final boolean contains(S2Point p) { + return contains(new S2LatLng(p)); + } + + /** Returns true if the edge AB intersects the given edge of constant longitude. */ + public static final boolean intersectsLngEdge(S2Point a, S2Point b, R1Interval lat, double lng) { + // Return true if the segment AB intersects the given edge of constant longitude. The nice thing + // about edges of constant longitude is that they are straight lines on the sphere (geodesics). + + return S2.simpleCrossing( + a, + b, + S2LatLng.fromRadians(lat.lo(), lng).toPoint(), + S2LatLng.fromRadians(lat.hi(), lng).toPoint()); + } + + /** Returns true if the edge AB intersects the given edge of constant latitude. */ + public static final boolean intersectsLatEdge(S2Point a, S2Point b, double lat, S1Interval lng) { + // Return true if the segment AB intersects the given edge of constant latitude. Unfortunately, + // lines of constant latitude are curves on the sphere. They can intersect a straight edge in + // 0, 1, or 2 points. + // assert (S2.isUnitLength(a) && S2.isUnitLength(b)); + + // First, compute the normal to the plane AB that points vaguely north. + S2Point z = S2Point.normalize(S2.robustCrossProd(a, b)); + if (z.z < 0) { + z = S2Point.neg(z); + } + + // Extend this to an orthonormal frame (x,y,z) where x is the direction where the great circle + // through AB achieves its maximum latitude. + S2Point y = S2Point.normalize(S2.robustCrossProd(z, S2Point.Z_POS)); + S2Point x = S2Point.crossProd(y, z); + // assert (S2.isUnitLength(x) && x.z >= 0); + + // 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. + double sinLat = Math.sin(lat); + if (Math.abs(sinLat) >= x.z) { + return false; // The great circle does not reach the given latitude. + } + // assert (x.z > 0); + double cosTheta = sinLat / x.z; + double sinTheta = Math.sqrt(1 - cosTheta * cosTheta); + double theta = Math.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. + S1Interval abTheta = + S1Interval.fromPointPair( + Math.atan2(a.dotProd(y), a.dotProd(x)), Math.atan2(b.dotProd(y), b.dotProd(x))); + + if (abTheta.contains(theta)) { + // Check if the intersection point is also in the given "lng" interval. + S2Point isect = S2Point.add(S2Point.mul(x, cosTheta), S2Point.mul(y, sinTheta)); + if (lng.contains(Math.atan2(isect.y, isect.x))) { + return true; + } + } + if (abTheta.contains(-theta)) { + // Check if the intersection point is also in the given "lng" interval. + S2Point intersection = S2Point.sub(S2Point.mul(x, cosTheta), S2Point.mul(y, sinTheta)); + if (lng.contains(Math.atan2(intersection.y, intersection.x))) { + return true; + } + } + + return false; + } + + @Override + public final String toString() { + return "[Lo=" + lo() + ", Hi=" + hi() + "]"; + } + + public final String toStringDegrees() { + return "[Lo=" + lo().toStringDegrees() + ", Hi=" + hi().toStringDegrees() + "]"; + } +} diff --git a/core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/S2LaxPolygonShape.java b/core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/S2LaxPolygonShape.java new file mode 100644 index 0000000000..781898fa1c --- /dev/null +++ b/core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/S2LaxPolygonShape.java @@ -0,0 +1,377 @@ +/* + * Copyright 2019 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.mogo.eagle.core.utilcode.geometry; + +import com.google.common.annotations.GwtIncompatible; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Iterables; +import com.google.common.collect.Lists; +import com.mogo.eagle.core.utilcode.geometry.PrimitiveArrays.Bytes; +import com.mogo.eagle.core.utilcode.geometry.PrimitiveArrays.Cursor; +import com.mogo.eagle.core.utilcode.geometry.PrimitiveArrays.Longs; +import com.mogo.eagle.core.utilcode.geometry.S2ShapeAspect.ChainAspect; +import com.google.common.primitives.ImmutableLongArray; +import java.io.IOException; +import java.io.OutputStream; +import java.util.List; + +/** + * A region defined by a collection of zero or more closed loops. The interior is the region to the + * left of all loops. Loops are not closed, that is, the last edge is an implicit path from the last + * vertex back to the first vertex. + * + *

    This is similar to {@link S2Polygon#shape}, except that this class supports polygons with two + * types of degeneracy: + * + *

      + *
    1. Degenerate edges (from a vertex to itself) + *
    2. Sibling edge pairs (consisting of two oppositely oriented edges) + *
    + * + *

    Degeneracies can represent either "shells" or "holes" depending on the loop they are contained + * by. For example, a degenerate edge or sibling pair contained by a "shell" would be interpreted as + * a degenerate hole. Such edges form part of the boundary of the polygon. + * + *

    Loops with fewer than three vertices are interpreted as follows: + * + *

      + *
    • A loop with two vertices defines two edges (in opposite directions). + *
    • A loop with one vertex defines a single degenerate edge. + *
    • A loop with no vertices is interpreted as the "full loop" containing all points on the + * sphere. If this loop is present, then all other loops must form degeneracies (i.e., + * degenerate edges or sibling pairs). For example, two loops {} and {X} would be interpreted + * as the full polygon with a degenerate single-point hole at X. + *
    + * + *

    No error checking is performed during construction. It is perfectly fine to create objects + * that do not meet the requirements below (e.g., in order to analyze or fix those problems). + * However, some additional conditions must be satisfied in order to perform certain operations: + * + *

      + *
    • In order to be valid for point containment tests, the polygon must satisfy the "interior is + * on the left" rule. This means that there must not be any crossing edges, and if there are + * duplicate edges then all but at most one of them must belong to a sibling pair (i.e., the + * number of edges in opposite directions must differ by at most one). + *
    • To be valid for boolean operations, degenerate edges and sibling pairs cannot coincide with + * any other edges. For example, the following situations are not allowed: + *
        + *
      • {AA, AA} // degenerate edge coincides with another edge + *
      • {AA, AB} // degenerate edge coincides with another edge + *
      • {AB, BA, AB} // sibling pair coincides with another edge + *
      + *
    + * + *

    Note that this class is faster to initialize and is more compact than {@link S2Polygon}, but + * it does not have any built-in operations, as those are by design provided by other classes. All + * the design considerations here are focused on the meaning and storage of the model itself. All + * implementations store a single list of vertices, and if there are multiple loops, an int[] that + * provides the offset into the list where each loop's vertices begin. This scales at a rate of one + * int per loop. This compares favorably to {@link S2Polygon}, which requires 4 objects and an int + * per loop. Heap size of the vertex data scales at different rates, depending on the storage: + * + *

      + *
    1. {@link #create Standard} polygons copy points into one {@link ImmutableList immutable + * list}, which requires 48 bytes per vertex on a standard 64-bit JVM. + *
    2. {@link #createPacked Packed} polygons copy coordinates into a double[], and convert these + * coordinates back to {@link S2Point point} instances on demand. This consumes 24 + * bytes/vertex, but construction and other operations are slower and drive the garbage + * collector harder. + *
    3. {@link #createSnapped Snapped} polygons copy {@link S2CellId cells} into a long[], and + * convert the cell centers to {@link S2Point point} instances on demand. This consumes just 8 + * bytes/vertex, but construction and especially operations are even slower and drive the + * garbage collector even harder. + *
        + */ +@GwtIncompatible("S2ShapeAspect incompatible") +public interface S2LaxPolygonShape extends S2ShapeAspect.EdgeAspect.Closed { + // When adding a new encoding, be aware that old binaries will not be able to decode it. + static final byte CURRENT_ENCODING_VERSION = 1; + + /** A singleton for the empty polygon. */ + public static S2LaxPolygonShape EMPTY = new MultiArray(ImmutableList.of()); + + /** A singleton for the full polygon. */ + public static S2LaxPolygonShape FULL = new SimpleArray(ImmutableList.of()); + + /** Creates a polygon from the given {@link S2Polygon} by copying its data. */ + public static S2LaxPolygonShape create(S2Polygon polygon) { + if (polygon.isEmpty()) { + return EMPTY; + } else if (polygon.isFull()) { + return FULL; + } else { + // S2Polygon filters out empty loops already. + // Convert full loops to empty lists. + // Other loops must simply be oriented. + return create(Lists.transform(polygon.getLoops(), x -> x.isFull() + ? ImmutableList.of() + : x.orientedVertices())); + } + } + + /** + * Creates a polygon from the given loops, defensively copying any loop's Iterable except an + * {@link ImmutableList}, to ensure the polygon is deeply immutable. + * + *

        If given no loops, the empty polygon is the result. If given only empty loops, the full + * polygon is the result. Otherwise the resulting polygon's interior is on the left of the loops + * when walking the vertices in the given order. + * + *

        Each loop should not be closed, that is, the last vertex in each inner iterable should + * differ from the first vertex, since an implicit edge from the last vertex back to the first is + * assumed. + */ + public static S2LaxPolygonShape create(Iterable> loops) { + if (Iterables.isEmpty(loops)) { + return EMPTY; + } else if (Iterables.all(loops, Iterables::isEmpty)) { + return FULL; + } else if (Iterables.size(loops) == 1) { + return new SimpleArray(Iterables.getOnlyElement(loops)); + } else { + return new MultiArray(loops); + } + } + + /** + * As {@link #create}, but packs coordinates into a double[] array. Operations are slower since + * S2Points are constructed on each access, but this representation has vastly fewer objects, and + * so can be a better choice if polygons may be held in RAM for a long time. + */ + public static S2LaxPolygonShape createPacked(Iterable> loops) { + if (Iterables.isEmpty(loops)) { + return EMPTY; + } else if (Iterables.all(loops, Iterables::isEmpty)) { + return FULL; + } else if (Iterables.size(loops) == 1) { + return new SimplePacked(Iterables.getOnlyElement(loops)); + } else { + return new MultiPacked(loops); + } + } + + /** + * As {@link #create}, but packs vertices into a long[] array. Operations may be much slower since + * S2Points are constructed on each access, but this representation is the smallest, and so may be + * far better if polygons may be held in RAM for a long time. + */ + public static S2LaxPolygonShape createSnapped(Iterable> loops) { + if (Iterables.isEmpty(loops)) { + return EMPTY; + } else if (Iterables.all(loops, Iterables::isEmpty)) { + return FULL; + } else if (Iterables.size(loops) == 1) { + return new SimpleSnapped(Iterables.getOnlyElement(loops)); + } else { + return new MultiSnapped(loops); + } + } + + /** Canonicalizes the empty/full instances on deserialization. */ + default Object readResolve() { + int n = numChains(); + if (n == 0) { + return EMPTY; + } + for (int i = 0; i < n; i++) { + if (getChainLength(i) != 0) { + return this; + } + } + return FULL; + } + + @Override default int dimension() { + return 2; + } + + /** Returns true if this polygon contains no area, i.e. has no loops. */ + default boolean isEmpty() { + return numChains() == 0; + } + + /** Returns true if this polygon contains all points, i.e. there are loops, but all are empty. */ + default boolean isFull() { + int n = numChains(); + for (int i = 0; i < n; i++) { + if (getChainLength(i) != 0) { + return false; + } + } + return n > 0; + } + + @Override default boolean hasInterior() { + return true; + } + + @Override default boolean containsOrigin() { + if (isFull()) { + return true; + } else if (isEmpty()) { + return false; + } else { + return S2ShapeUtil.containsBruteForce(this, S2.origin()); + } + } + + @Override default ReferencePoint getReferencePoint() { + return S2ShapeUtil.getReferencePoint(this); + } + + /** A simple polygon with points referenced from an array. */ + static class SimpleArray extends ChainAspect.Simple.Array implements S2LaxPolygonShape { + SimpleArray(Iterable vertices) { + super(vertices); + } + } + + /** A simple polygon with vertices referenced from a {@link List}. */ + static class SimpleList extends ChainAspect.Simple implements S2LaxPolygonShape { + private final List vertices; + + private SimpleList(List vertices) { + this.vertices = vertices; + } + + @Override + public int numVertices() { + return vertices.size(); + } + + @Override + public S2Point vertex(int vertexId) { + return vertices.get(vertexId); + } + } + + /** A simple polygon with vertex coordinates stored in a double[]. */ + static class SimplePacked extends ChainAspect.Simple.Packed implements S2LaxPolygonShape { + SimplePacked(Iterable vertices) { + super(vertices); + } + } + + /** A simple polygon with vertices at cell ID centers stored in a long[]. */ + static class SimpleSnapped extends ChainAspect.Simple.Snapped implements S2LaxPolygonShape { + SimpleSnapped(Iterable vertices) { + super(vertices); + } + } + + /** A multi polygon with points referenced from an array. */ + static class MultiArray extends ChainAspect.Multi.Array implements S2LaxPolygonShape { + MultiArray(Iterable> loops) { + super(loops); + } + } + + /** + * A multi polygon with vertices referenced from a {@link List}, and cumulative edges + * referenced from an {@link Longs}. + */ + static class MultiList extends ChainAspect.Multi implements S2LaxPolygonShape { + private final List vertices; + + private MultiList(List vertices, Longs cumulativeEdges) { + super(cumulativeEdges.toIntArray()); + this.vertices = vertices; + } + + @Override + public int numVertices() { + return vertices.size(); + } + + @Override + public S2Point vertex(int vertexId) { + return vertices.get(vertexId); + } + } + + /** A multi polygon with vertex coordinates stored in a double[]. */ + static class MultiPacked extends ChainAspect.Multi.Packed implements S2LaxPolygonShape { + MultiPacked(Iterable> loops) { + super(loops); + } + } + + /** A multi polygon with vertices at cell ID centers stored in a long[]. */ + static class MultiSnapped extends ChainAspect.Multi.Snapped implements S2LaxPolygonShape { + MultiSnapped(Iterable> loops) { + super(loops); + } + } + + /** An encoder/decoder of {@link S2LaxPolygonShape}s. */ + @GwtIncompatible("Uses ByteBuffer") + class Coder implements S2Coder { + + /** + * An instance of {@link Coder} which encodes/decodes {@link S2LaxPolygonShape}s in the {@code + * FAST} format. + */ + static final Coder FAST = new Coder(S2PointVectorCoder.FAST); + /** + * An instance of {@link Coder} which encodes/decodes {@link S2LaxPolygonShape}s in the {@code + * COMPACT} format. + */ + static final Coder COMPACT = new Coder(S2PointVectorCoder.COMPACT); + + private final S2Coder> coder; + + private Coder(S2Coder> coder) { + this.coder = coder; + } + + @Override + public void encode(S2LaxPolygonShape shape, OutputStream output) throws IOException { + output.write(CURRENT_ENCODING_VERSION); + // Write the number of loops. + EncodedInts.writeVarint64(output, shape.numChains()); + coder.encode(shape.vertices(), output); + if (shape.numChains() > 1) { + ImmutableLongArray.Builder builder = ImmutableLongArray.builder(); + for (int i = 0; i < shape.numChains(); i++) { + builder.add(shape.getChainStart(i)); + } + builder.add(shape.numVertices()); + UintVectorCoder.UINT32.encode(Longs.fromImmutableLongArray(builder.build()), output); + } + } + + @Override + public S2LaxPolygonShape decode(Bytes data, Cursor cursor) { + byte version = data.get(cursor.position++); + if (version != CURRENT_ENCODING_VERSION) { + throw new IllegalArgumentException( + String.format( + "Expected encoding version %s, got %s.", CURRENT_ENCODING_VERSION, version)); + } + + long numChains = data.readVarint64(cursor); + // Both FAST and COMPACT coders are capable of decoding any encoding format. + List vertices = S2PointVectorCoder.FAST.decode(data, cursor); + + if (numChains == 0) { + return S2LaxPolygonShape.EMPTY; + } else if (numChains == 1) { + return new SimpleList(vertices); + } + return new MultiList(vertices, UintVectorCoder.UINT32.decode(data, cursor)); + } + } +} diff --git a/core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/S2LaxPolylineShape.java b/core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/S2LaxPolylineShape.java new file mode 100644 index 0000000000..1a17df03ed --- /dev/null +++ b/core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/S2LaxPolylineShape.java @@ -0,0 +1,238 @@ +/* + * Copyright 2019 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.mogo.eagle.core.utilcode.geometry; + +import com.google.common.annotations.GwtIncompatible; +import com.google.common.base.Predicates; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Iterables; +import com.mogo.eagle.core.utilcode.geometry.PrimitiveArrays.Bytes; +import com.mogo.eagle.core.utilcode.geometry.PrimitiveArrays.Cursor; +import com.mogo.eagle.core.utilcode.geometry.S2ShapeAspect.ChainAspect; +import java.io.IOException; +import java.io.OutputStream; +import java.util.List; + +/** + * S2LaxPolylineShape represents a polyline. It is similar to {@link S2Polyline} except that + * duplicate vertices are allowed, and the representation is slightly more compact since this class + * does not implement {@link S2Region}. + * + *

        Polylines may have any number of vertices, but note that polylines with fewer than 2 vertices + * do not define any edges. To create a polyline consisting of a single degenerate edge, repeat the + * same vertex twice. + */ +@GwtIncompatible("S2ShapeAspect incompatible") +public interface S2LaxPolylineShape extends S2ShapeAspect.EdgeAspect.Open { + /** A polyline with no edges. */ + static final S2LaxPolylineShape EMPTY = new SimpleArray(ImmutableList.of()); + + /** Creates a lax polyline from the {@code line} by copying its data. */ + public static S2LaxPolylineShape create(S2Polyline line) { + return create(line.vertices()); + } + + /** Creates a new lax polyline from the given vertices. */ + public static S2LaxPolylineShape create(Iterable vertices) { + vertices = filterLine(vertices); + return Iterables.isEmpty(vertices) ? EMPTY : new SimpleArray(vertices); + } + + /** As {@link #create}, but with coordinates packed into a double[]. */ + public static S2LaxPolylineShape createPacked(Iterable vertices) { + vertices = filterLine(vertices); + return Iterables.isEmpty(vertices) ? EMPTY : new SimplePacked(vertices); + } + + /** As {@link #create}, but with vertices at the center of cell IDs, packed into a long[]. */ + public static S2LaxPolylineShape createSnapped(Iterable vertices) { + vertices = filterLine(vertices); + return Iterables.isEmpty(vertices) ? EMPTY : new SimpleSnapped(vertices); + } + + /** Creates a new lax multipolyline with the given lines. */ + public static S2LaxPolylineShape createMulti(Iterable> lines) { + lines = filterLines(lines); + if (Iterables.isEmpty(lines)) { + return EMPTY; + } else if (Iterables.size(lines) == 1) { + return new SimpleArray(Iterables.getOnlyElement(lines)); + } else { + return new MultiArray(lines); + } + } + + /** As {@link #create}, but with coordinates packed into a double[]. */ + public static S2LaxPolylineShape createMultiPacked(Iterable> lines) { + lines = filterLines(lines); + if (Iterables.isEmpty(lines)) { + return EMPTY; + } else if (Iterables.size(lines) == 1) { + return new SimplePacked(Iterables.getOnlyElement(lines)); + } else { + return new MultiPacked(lines); + } + } + + /** As {@link #create}, but with vertices at the center of cell IDs, packed into a long[]. */ + public static S2LaxPolylineShape createMultiSnapped( + Iterable> lines) { + lines = filterLines(lines); + if (Iterables.isEmpty(lines)) { + return EMPTY; + } else if (Iterables.size(lines) == 1) { + return new SimpleSnapped(Iterables.getOnlyElement(lines)); + } else { + return new MultiSnapped(lines); + } + } + + /** Returns 'input' or an empty iterable if 'input' has only one vertex. */ + static Iterable filterLine(Iterable input) { + return Iterables.size(input) < 2 ? ImmutableList.of() : input; + } + + static Iterable> filterLines(Iterable> input) { + return Iterables.filter( + Iterables.transform(input, S2LaxPolylineShape::filterLine), + Predicates.not(Iterables::isEmpty)); + } + + /** Canonicalize exactly empty polylines to EMPTY. */ + default Object readResolve() { + return numVertices() == 0 ? EMPTY : this; + } + + @Override default int dimension() { + return 1; + } + + @Override default boolean hasInterior() { + return false; + } + + @Override default boolean containsOrigin() { + return false; + } + + @Override + default int numEdges() { + return numVertices() == 0 ? 0 : numVertices() - numChains(); + } + + /** Returns true unless there is at least one edge in this line. */ + default boolean isEmpty() { + return numEdges() == 0; + } + + /** Returns false in all cases since a polyline may never cover the entire sphere. */ + default boolean isFull() { + return false; + } + + /** A polyline storing references to previously allocated S2Point instances. */ + static class SimpleArray extends ChainAspect.Simple.Array implements S2LaxPolylineShape { + private SimpleArray(Iterable vertices) { + super(vertices); + } + } + + /** A polyline storing {@link S2Point}s in a {@link List}. */ + static class SimpleList extends ChainAspect.Simple implements S2LaxPolylineShape { + private final List vertices; + + private SimpleList(List vertices) { + this.vertices = vertices; + } + + @Override + public int numVertices() { + return vertices.size(); + } + + @Override + public S2Point vertex(int vertexId) { + return vertices.get(vertexId); + } + } + + /** A polyline storing xyz coordinates in a single packed 'double' array. */ + static class SimplePacked extends ChainAspect.Simple.Packed implements S2LaxPolylineShape { + private SimplePacked(Iterable vertices) { + super(vertices); + } + } + + /** A polyline storing cell IDs in a single 'long' array. */ + static class SimpleSnapped extends ChainAspect.Simple.Snapped implements S2LaxPolylineShape { + private SimpleSnapped(Iterable vertices) { + super(vertices); + } + } + + /** A multi polyline storing references to previously allocated S2Point instances. */ + static class MultiArray extends ChainAspect.Multi.Array implements S2LaxPolylineShape { + private MultiArray(Iterable> chains) { + super(chains); + } + } + + /** A multi polyline storing xyz coordinates in a single packed 'double' array. */ + static class MultiPacked extends ChainAspect.Multi.Packed implements S2LaxPolylineShape { + MultiPacked(Iterable> chains) { + super(chains); + } + } + + /** A multi polyline storing cell IDs in a single 'long' array. */ + static class MultiSnapped extends ChainAspect.Multi.Snapped implements S2LaxPolylineShape { + MultiSnapped(Iterable> chains) { + super(chains); + } + } + + /** An encoder/decoder of {@link S2LaxPolylineShape}s. */ + @GwtIncompatible("Uses EncodedS2PointVector") + class Coder implements S2Coder { + + /** + * An instance of {@link Coder} which encodes/decodes {@link S2LaxPolylineShape}s in the {@code + * FAST} format. + */ + static final Coder FAST = new Coder(S2PointVectorCoder.FAST); + /** + * An instance of {@link Coder} which encodes/decodes {@link S2LaxPolylineShape}s in the {@code + * COMPACT} format. + */ + static final Coder COMPACT = new Coder(S2PointVectorCoder.COMPACT); + + private final S2Coder> coder; + + private Coder(S2Coder> coder) { + this.coder = coder; + } + + @Override + public void encode(S2LaxPolylineShape shape, OutputStream output) throws IOException { + coder.encode(shape.vertices(), output); + } + + @Override + public S2LaxPolylineShape decode(Bytes data, Cursor cursor) { + return new SimpleList(coder.decode(data, cursor)); + } + } +} diff --git a/core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/S2Loop.java b/core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/S2Loop.java new file mode 100644 index 0000000000..7421de2fba --- /dev/null +++ b/core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/S2Loop.java @@ -0,0 +1,2066 @@ +/* + * Copyright 2006 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.mogo.eagle.core.utilcode.geometry; + +import com.google.common.annotations.GwtCompatible; +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Objects; +import com.google.common.base.Preconditions; +import com.google.common.base.Predicate; +import com.google.common.collect.HashMultiset; +import com.google.common.collect.Iterables; +import com.google.common.collect.Lists; +import com.google.common.collect.Multiset; +import com.mogo.eagle.core.utilcode.geometry.S2ShapeIndex.S2ClippedShape; +import com.mogo.eagle.core.utilcode.geometry.S2ShapeUtil.AreaCentroidMeasure; +import com.mogo.eagle.core.utilcode.geometry.S2ShapeUtil.AreaMeasure; +import com.mogo.eagle.core.utilcode.geometry.S2ShapeUtil.CentroidMeasure; +import com.google.common.primitives.UnsignedLongs; +import java.io.IOException; +import java.io.Serializable; +import java.util.AbstractList; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * An S2Loop represents a simple spherical polygon. It consists of a single chain 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). Although these restrictions are not enforced in + * optimized code, you may get unexpected results if they are violated. + * + *

        There are two special loops: the "empty" loop contains no points, while 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 ( + * {@link #empty()} and {@link #full()}.) + * + *

        Point containment of loops is defined such that if the sphere is subdivided into faces + * (loops), every point is contained by exactly one face. This implies that loops do not necessarily + * contain their vertices. + * + */ +@GwtCompatible(serializable = true) +public final strictfp class S2Loop implements S2Region, Comparable, Serializable, S2Shape { + + @VisibleForTesting static final byte LOSSLESS_ENCODING_VERSION = 1; + + /** Max angle that intersections can be off by and yet still be considered collinear. */ + public static final double MAX_INTERSECTION_ERROR = 1e-15; + + /** The single vertex that defines a loop that contains no area. */ + static final S2Point EMPTY_VERTEX = S2Point.Z_POS; + + /** The single vertex that defines a loop that contains the whole sphere. */ + static final S2Point FULL_VERTEX = S2Point.Z_NEG; + + /** Spatial index for this loop. */ + @VisibleForTesting transient S2ShapeIndex index; + + /** + * In general we build the index the first time it is needed, but we make an exception for + * contains(S2Point) because this method has a simple brute force implementation that is + * relatively cheap. For this one method we keep track of the number of calls made and only build + * the index once enough calls have been made that we think an index would be worthwhile. + */ + private final AtomicInteger unindexedContainsCalls = new AtomicInteger(); + + private final S2Point[] vertices; + private final int numVertices; + + /** + * A conservative bound on all points contained by this loop: if A.contains(P), then + * A.bound.contains(new S2LatLng(P)). + */ + private S2LatLngRect bound; + + /** + * 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). + */ + private S2LatLngRect subregionBound; + + private boolean originInside; + private int depth; + + /** + * Initializes a loop with the given vertices. The last vertex is implicitly connected to the + * first. All points should be unit length. Loops must have at least 3 vertices (except for the + * "empty" and "full" loops; see {@link #empty()} and {@link #full()}. + * + * @param vertices the vertices for this new loop + */ + public S2Loop(final List vertices) { + initIndex(); + this.numVertices = vertices.size(); + this.vertices = new S2Point[numVertices]; + vertices.toArray(this.vertices); + this.depth = 0; + initOriginAndBound(); + } + + /** + * Fast/unsafe loop initialization. + * + *

        This constructor provides known good values for bounds and the originInside value. This is + * intended to be a "fast loop creation" when we already know a lot about the loop. It is + * primarily used in combination with the fast S2Polygon initializer ( {@link + * S2Polygon#initWithNestedLoops(java.util.Map)}). The last vertex is implicitly connected to the + * first. All points should be unit length. Loops must have at least 3 vertices, except for the + * empty and full loops (see {@link #empty()} and {@link #full()}.) + * + * @param vertices loop vertices + * @param originInside true if the S2::origin() is inside the loop + * @param bound the lat/long bounds of the loop + * @return new loop. + */ + public static S2Loop newLoopWithTrustedDetails( + List vertices, boolean originInside, S2LatLngRect bound) { + // This is a static method to try to discourage its use. + return new S2Loop(vertices, originInside, bound); + } + + /** Create a circle of points with a given center, radius, and number of vertices. */ + public static S2Loop makeRegularLoop(S2Point center, S1Angle radius, int numVertices) { + return new S2Loop(makeRegularVertices(center, radius, numVertices)); + } + + /** As {@link #makeRegularLoop(S2Point, S1Angle, int)}, but returns vertices as a list. */ + public static List makeRegularVertices(S2Point center, S1Angle radius, int numVertices) { + Matrix3x3 m = S2.getFrame(center); + List vertices = Lists.newArrayList(); + // We construct the loop in the given frame coordinates, with the center at + // (0, 0, 1). For a loop of radius "r", the loop vertices have the form + // (x, y, z) where x^2 + y^2 = sin(r) and z = cos(r). The distance on the + // sphere (arc length) from each vertex to the center is acos(cos(r)) = r. + double z = Math.cos(radius.radians()); + double r = Math.sin(radius.radians()); + double radianStep = 2 * Math.PI / numVertices; + for (int i = 0; i < numVertices; ++i) { + double angle = i * radianStep; + S2Point p = new S2Point(r * Math.cos(angle), r * Math.sin(angle), z); + vertices.add(S2Point.normalize(S2.fromFrame(m, p))); + } + return vertices; + } + + private S2Loop(List vertices, boolean originInside, S2LatLngRect bound) { + initIndex(); + this.numVertices = vertices.size(); + this.vertices = new S2Point[numVertices]; + this.bound = bound; + this.subregionBound = S2EdgeUtil.RectBounder.expandForSubregions(bound); + this.depth = 0; + this.originInside = originInside; + + vertices.toArray(this.vertices); + } + + /** Initialize a loop corresponding to the given cell. */ + public S2Loop(S2Cell cell) { + initIndex(); + numVertices = 4; + vertices = new S2Point[numVertices]; + depth = 0; + for (int i = 0; i < 4; ++i) { + vertices[i] = cell.getVertex(i); + } + // We compute the bounding rectangle ourselves, since S2Cell uses a different method and we need + // all the bounds to be consistent. + initOriginAndBound(); + } + + /** Copy constructor. */ + public S2Loop(S2Loop src) { + initIndex(); + this.numVertices = src.numVertices; + this.vertices = new S2Point[numVertices]; + for (int i = 0; i < numVertices; i++) { + this.vertices[i] = src.vertices[i]; + } + this.bound = src.getRectBound(); + this.subregionBound = src.subregionBound; + this.originInside = src.originInside; + this.depth = src.depth(); + } + + private void initIndex() { + // 'maxUnindexedContainsCalls' was tuned using the benchmarks. We wait until the + // cumulative time we would have saved with an index approximately equals the cost of building + // the index, and then build it. (This gives the optimal competitive ratio of 2; look up + // 'competitive algorithms' for details.) We err on the side of building the index too early, + // because building the index may be forced anyways by other API calls. We select + // 'maxUnindexedContainsCalls' based on the number of vertices since there is great variation in + // contains() efficiency. + int maxUnindexedContainsCalls; + if (numVertices <= 8) { + maxUnindexedContainsCalls = 10; + } else if (numVertices <= 8192) { + maxUnindexedContainsCalls = 50; + } else if (numVertices <= 50000) { + maxUnindexedContainsCalls = 10; + } else { + maxUnindexedContainsCalls = 2; + } + this.unindexedContainsCalls.set(maxUnindexedContainsCalls); + + index = new S2ShapeIndex(); + index.add(this); + } + + /** Returns the same instance after initializing transient fields. */ + private Object readResolve() { + initIndex(); + return this; + } + + /** + * Returns a new loop with one vertex that defines an empty loop (i.e., a loop with no edges that + * contains no points.) + */ + public static final S2Loop empty() { + return new S2Loop(Collections.singletonList(EMPTY_VERTEX)); + } + + /** + * Returns a new loop with one vertex that creates a full loop (i.e., a loop with no edges that + * contains all points). See {@link #empty()} for further details. + */ + public static final S2Loop full() { + return new S2Loop(Collections.singletonList(FULL_VERTEX)); + } + + // Note that this doesn't do anything smart: it just compares a few fields for equality, but + // doesn't check for equivalent loops that were initialized in different ways, etc. + @Override + public boolean equals(Object obj) { + if (obj instanceof S2Loop) { + S2Loop that = (S2Loop) obj; + return Arrays.equals(this.vertices, that.vertices) + && Objects.equal(this.originInside, that.originInside) + && Objects.equal(this.bound, that.bound); + } + return false; + } + + @Override + public int hashCode() { + return bound.hashCode(); + } + + public int depth() { + return depth; + } + + /** + * The depth of a loop is defined as its nesting level within its containing polygon. "Outer + * shell" loops have depth 0, holes within those loops have depth 1, shells within those holes + * have depth 2, etc. This field is only used by the S2Polygon implementation. + * + * @param depth + */ + public void setDepth(int depth) { + this.depth = depth; + } + + /** Return true if this loop represents a hole in its containing polygon. */ + public boolean isHole() { + return (depth & 1) != 0; + } + + /** + * The sign of a loop is -1 if the loop represents a hole in its containing polygon, and +1 + * otherwise. + */ + public int sign() { + return isHole() ? -1 : 1; + } + + public int numVertices() { + return numVertices; + } + + /** + * For convenience, we make two entire copies of the vertex list available: vertex(n..2*n-1) is + * mapped to vertex(0..n-1), where n == numVertices(). + */ + public S2Point vertex(int i) { + try { + return vertices[i >= vertices.length ? i - vertices.length : i]; + } catch (ArrayIndexOutOfBoundsException e) { + throw new IllegalStateException("Invalid vertex index"); + } + } + + /** + * Like vertex(), but this method returns vertices in reverse order if the loop represents a + * polygon hole. For example, arguments 0, 1, 2 are mapped to vertices n-1, n-2, n-3, where n == + * numVertices(). This ensures that the interior of the polygon is always to the left of the + * vertex chain. + */ + public S2Point orientedVertex(int i) { + if (isHole()) { + i = 2 * numVertices() - 1 - i; + } + return vertex(i); + } + + /** Returns an unmodifiable view of the vertices of this polyline. */ + public List vertices() { + return Collections.unmodifiableList(Arrays.asList(vertices)); + } + + /** Returns the vertices oriented such that left is on the inside. */ + public List orientedVertices() { + return new AbstractList() { + @Override public int size() { + return numVertices; + } + @Override public S2Point get(int index) { + return orientedVertex(index); + } + }; + } + + /** Returns true if this is the special "empty" loop that contains no points. */ + public boolean isEmpty() { + return isEmptyOrFull() && !originInside; + } + + /** Returns true if this is the special "full" loop that contains all points. */ + public boolean isFull() { + return isEmptyOrFull() && originInside; + } + + /** Returns true if this loop is either "empty" or "full". */ + public boolean isEmptyOrFull() { + return numVertices == 1; + } + + @Override + public int numEdges() { + return isEmptyOrFull() ? 0 : numVertices; + } + + @Override + public void getEdge(int index, MutableEdge result) { + result.set(vertex(index), vertex(index + 1)); + } + + @Override + public boolean hasInterior() { + return true; + } + + @Override + public boolean containsOrigin() { + return originInside; + } + + @Override + public int numChains() { + return isEmpty() ? 0 : 1; + } + + @Override + public int getChainStart(int chainId) { + Preconditions.checkElementIndex(chainId, numChains()); + return 0; + } + + @Override + public int getChainLength(int chainId) { + Preconditions.checkElementIndex(chainId, numChains()); + return numEdges(); + } + + @Override + public void getChainEdge(int chainId, int offset, MutableEdge result) { + Preconditions.checkElementIndex(chainId, numChains()); + getEdge(offset, result); + } + + @Override + public S2Point getChainVertex(int chainId, int edgeOffset) { + Preconditions.checkElementIndex(chainId, numChains()); + return vertex(edgeOffset); + } + + @Override + public int dimension() { + return 2; + } + + /** Comparator (needed by Comparable interface) */ + @Override + public int compareTo(S2Loop other) { + if (numVertices != other.numVertices) { + return numVertices - other.numVertices; + } else if (numVertices == 0) { + return 0; + } + // Compare the two loops' vertices, starting with each loop's firstLogicalVertex. This allows us + // to always catch cases where logically identical loops have different vertex orderings (e.g. + // ABCD and BCDA). + int maxVertices = numVertices; + int iThis = getCanonicalFirstVertex() % numVertices; + int iOther = other.getCanonicalFirstVertex() % other.numVertices; + for (int i = 0; i < maxVertices; ++i, ++iThis, ++iOther) { + int compare = vertex(iThis).compareTo(other.vertex(iOther)); + if (compare != 0) { + return compare; + } + } + return 0; + } + + /** + * Returns a canonical minimum vertex such that the vertex sequence starting at this vertex does + * not change when the loop vertex order is rotated or inverted. This allows the loop vertices to + * be traversed in a canonical order. If the initial value is less than {@link #numVertices()}, + * then stable iteration should be toward larger indices, otherwise smaller indices. + */ + private int getCanonicalFirstVertex() { + int first = 0; + for (int i = 1; i < numVertices; ++i) { + if (vertex(i).compareTo(vertex(first)) < 0) { + first = i; + } + } + if (numVertices > 0 && vertex(first + 1).compareTo(vertex(first + numVertices - 1)) >= 0) { + first += numVertices; + } + return first; + } + + /** Return true if the loop is generally a left-turning, aka counter-clockwise loop. */ + public boolean isNormalized() { + // Optimization: if the longitude span is less than 180 degrees, then the loop covers less than + // half the sphere and is therefore normalized. + if (bound.lng().getLength() < S2.M_PI) { + return true; + } + + // We allow some error so that hemispheres are always considered normalized. + // TODO(user): This is no longer required by the S2Polygon implementation, so alternatively we + // could create the invariant that a loop is normalized if and only if its complement is not + // normalized. + return S2ShapeMeasures.turningAngle(vertices()) >= -S2.getTurningAngleMaxError(numVertices); + } + + /** Invert the loop if necessary so that the area enclosed by the loop is at most 2*Pi. */ + public void normalize() { + if (!isNormalized()) { + invert(); + } + } + + /** + * Reverse the order of the loop vertices, effectively complementing the region represented by the + * loop. + */ + public void invert() { + initIndex(); + int last = numVertices - 1; + if (isEmptyOrFull()) { + vertices[0] = isFull() ? EMPTY_VERTEX : FULL_VERTEX; + } else { + for (int i = (last - 1) / 2; i >= 0; --i) { + S2Point t = vertices[i]; + vertices[i] = vertices[last - i]; + vertices[last - i] = t; + } + } + originInside ^= true; + if (bound.lat().lo() > -S2.M_PI_2 && bound.lat().hi() < S2.M_PI_2) { + // The complement of this loop contains both poles. + bound = S2LatLngRect.full(); + subregionBound = bound; + } else { + // initBound() requires 'bound' to be null in order for its contains(S2Point) calls to work. + bound = null; + initBound(); + } + } + + /** + * Returns the area of the loop interior, i.e. the region on the left side of the loop regardless + * of whether it is a shell or a hole. This value is between 0 and 4*Pi, or explicitly 0 if the + * loop is invalid. + */ + public double getArea() { + if (isEmptyOrFull()) { + return originInside ? (4 * S2.M_PI) : 0; + } else if (numVertices < 3) { + return 0; + } + AreaMeasure area = new AreaMeasure(); + S2ShapeUtil.visitSurfaceIntegral(vertices(), area); + return area.value(numVertices, this::isNormalized); + } + + /** + * Returns the true centroid of the loop multiplied by the area of the loop, or null if this loop + * is empty, full, or invalid. + * + *

        The result is not unit length, so you may want to normalize it. Also note that in general, + * the centroid may not be contained by the loop. See {@link S2} for additional centroid details. + * + *

        We prescale by the loop area for two reasons: + * + *

          + *
        1. It is cheaper to compute this way, and + *
        2. It makes it easier to compute the centroid of more complicated shapes (by splitting them + * into disjoint regions and summing their centroids). + *
        + * + *

        Note that the return value is not affected by whether this loop is a "hole" or a "shell". + */ + public S2Point getCentroid() { + if (numVertices < 3) { + return null; + } + // visitSurfaceIntegral() returns either the integral of position over loop interior, or the + // negative of the integral of position over the loop exterior. But these two values are the + // same (!), because the integral of position over the entire sphere is (0, 0, 0). + CentroidMeasure centroid = new CentroidMeasure(); + S2ShapeUtil.visitSurfaceIntegral(vertices(), centroid); + return centroid.value(); + } + + /** + * Returns a pair of {@link #getArea()} and {@link #getCentroid()}, computed more efficiently than + * computing them separately. + */ + public S2AreaCentroid getAreaAndCentroid() { + if (numVertices < 3) { + return new S2AreaCentroid(getArea(), getCentroid()); + } + AreaCentroidMeasure areaCentroid = new AreaCentroidMeasure(); + S2ShapeUtil.visitSurfaceIntegral(vertices(), areaCentroid); + return areaCentroid.value(numVertices, this::isNormalized); + } + + /** + * Returns the sum of the turning angles at each vertex. The return value is positive if the loop + * is counter-clockwise, negative if the loop is clockwise, and zero if the loop is a great + * circle. + * + *

        Degenerate and nearly-degenerate loops are handled consistently with {@link + * S2Predicates#sign(S2Point, S2Point, S2Point)}. + * + *

        For example, if a loop has zero area (i.e., it is a very small CCW loop) then the turning + * angle will always be negative. + * + *

        This quantity is also called the "geodesic curvature" of the loop. + */ + public double getTurningAngle() { + // For empty and full loops, we return the limit value as the loop area approaches 0 or 4*Pi + // respectively. + if (isEmptyOrFull()) { + return originInside ? (-2 * S2.M_PI) : (2 * S2.M_PI); + } + return S2ShapeMeasures.turningAngle(vertices()); + } + + // The following are the possible relationships between two loops A and B: + // + // (1) A and B do not intersect. + // (2) A contains B. + // (3) B contains A. + // (4) The boundaries of A and B cross (i.e. the boundary of A intersects the interior and + // exterior of B and vice versa). + // (5) (A union B) is the entire sphere (i.e. A contains the complement of B and vice versa). + // + // More than one of these may be true at the same time, for example if A == B or + // A == Complement(B). + + /** + * Return true if the region contained by this loop is a superset of the region contained by the + * given other loop. + */ + public boolean contains(S2Loop b) { + // For this loop A to contains the given loop B, all of the following must be true: + // + // (1) There are no edge crossings between A and B except at vertices. + // + // (2) At every vertex that is shared between A and B, the local edge ordering implies that A + // contains B. + // + // (3) If there are no shared vertices, then A must contain a vertex of B and B must not contain + // a vertex of A. (An arbitrary vertex may be chosen in each case.) + // + // The second part of (3) is necessary to detect the case of two loops whose union is the entire + // sphere, i.e. two loops that contains each other's boundaries but not each other's interiors. + + if (!subregionBound.contains(b.bound)) { + return false; + } + + // Special cases to handle either loop being empty or full. + if (isEmptyOrFull() || b.isEmptyOrFull()) { + return isFull() || b.isEmpty(); + } + + // Check whether there are any edge crossings, and also check the loop relationship at any + // shared vertices. + ContainsRelation relation = new ContainsRelation(); + if (hasCrossingRelation(this, b, relation)) { + return false; + } + + // There are no crossings, and if there are any shared vertices, then A contains B locally at + // each shared vertex. + if (relation.foundSharedVertex()) { + return true; + } + + // Since there are no edge intersections or shared vertices, we just need to test condition (3) + // above. We can skip this test if we discovered that A contains at least one point of B while + // checking for edge crossings. + if (!contains(b.vertex(0))) { + return false; + } + + // We still need to check whether (A union B) is the entire sphere. Normally this check is very + // cheap due to the bounding box precondition. + if ((b.subregionBound.contains(bound) || b.bound.union(bound).isFull()) + && b.contains(vertex(0))) { + return false; + } + + return true; + } + + /** + * Return true if the region contained by this loop intersects the region contained by the given + * other loop. + */ + public boolean intersects(S2Loop b) { + // This code is similar to contains(), but is optimized for the case where both loops enclose + // less than half of the sphere. + if (!bound.intersects(b.bound)) { + return false; + } + + // Now check whether there are any edge crossings, and also check the loop relationship at any + // shared vertices. + IntersectsRelation relation = new IntersectsRelation(); + if (hasCrossingRelation(this, b, relation)) { + return true; + } + if (relation.foundSharedVertex()) { + return false; + } + + // Since there are no edge intersections or shared vertices, the loops intersect only if A + // contains B, B contains A, or the two loops contain each other's boundaries. These checks are + // usually cheap because of the bounding box preconditions. Note that neither loop is empty + // (because of the bounding box check above), so it is safe to access vertex(0). + + // Check whether A contains B, or A and B contain each other's boundaries. (Note that A contains + // all the vertices of B in either case.) + if (subregionBound.contains(b.bound) || bound.union(b.bound).isFull()) { + if (contains(b.vertex(0))) { + return true; + } + } + // Check whether B contains A. + if (b.subregionBound.contains(bound)) { + if (b.contains(vertex(0))) { + return true; + } + } + return false; + } + + /** + * Returns true if the wedge (a0, ab1, a2) contains the edge (ab1, b2), where [a0, ab1, a2] are a + * subset of the vertices of loop A, and [ab1, ab2, b2] are a subset of the vertices of loop B. + * + *

        Shared edges are handled as follows: If XY is a shared edge, define reversed(XY) to be true + * if this edge appears in opposite directions in A and B. Then A contains XY if and only if + * {@code reversed(XY) == bReversed}. + */ + static boolean wedgeContainsSemiwedge( + S2Point a0, S2Point ab1, S2Point a2, S2Point b2, boolean bReversed) { + boolean b2EqualsA0 = b2.equalsPoint(a0); + if (b2EqualsA0 || b2.equalsPoint(a2)) { + // We have a shared or reversed edge. + return b2EqualsA0 == bReversed; + } else { + return S2Predicates.orderedCCW(a0, a2, b2, ab1); + } + } + + /** + * Given two loops of a polygon, return true if A contains B. This version of Contains() is cheap + * because it does not test for edge intersections. The loops must meet all the S2Polygon + * requirements; for example this implies that their boundaries may not cross or have any shared + * edges (although they may have shared vertices). + */ + public boolean containsNested(S2Loop b) { + if (!subregionBound.contains(b.bound)) { + return false; + } + + // Special cases to handle either loop being empty or full. We check if b.numVertices() < 2 + // in order to also handle the case where b.numVertices() == 0. + if (isEmptyOrFull() || b.numVertices() < 2) { + return isFull() || b.isEmpty(); + } + + // We are given that A and B do not share any edges, and that either one loop contains the other + // or they do not intersect. + int m = findVertex(b.vertex(1)); + if (m < 0) { + // Since b->vertex(1) is not shared, we can check whether A contains it. + return contains(b.vertex(1)); + } + // Check whether the edge order around b.vertex(1) is compatible with A containing B. + return (new S2EdgeUtil.WedgeContains()) + .test(vertex(m - 1), vertex(m), vertex(m + 1), b.vertex(0), b.vertex(2)) + > 0; + } + + /** + * Returns +1 if A contains the boundary of B, -1 if A excludes the boundary of B, and 0 if the + * boundaries of A and B cross. + * + *

        Shared edges are handled as follows: If XY is a shared edge, define reversed(XY) to be true + * if XY appears in opposite directions in A and B. Then A contains XY if and only if reversed(XY) + * == B->isHole(). Intuitively, this checks whether A contains a vanishingly small region + * extending from the boundary of B toward the interior of the polygon to which loop B belongs. + * + *

        This method is used for testing containment and intersection of multi-loop polygons. Note + * that this method is not symmetric, since the result depends on the direction of loop A but not + * on the direction of loop B (in the absence of shared edges). + * + * @param b the loop to compare against this loop; neither loop may be empty, and if {@code b} is + * full, then it must not be a hole. + */ + public int compareBoundary(S2Loop b) { + Preconditions.checkArgument(!isEmpty() && !b.isEmpty()); + Preconditions.checkArgument(!b.isFull() || !b.isHole()); + + // The bounds must intersect for containment or crossing. + if (!bound.intersects(b.bound)) { + return -1; + } + + // Full loops are handled as though the loop surrounded the entire sphere. + if (isFull()) { + return 1; + } + if (b.isFull()) { + return -1; + } + + // Check whether there are any edge crossings, and also check the loop relationship at any + // shared vertices. + CompareBoundaryRelation relation = new CompareBoundaryRelation(b.isHole()); + if (hasCrossingRelation(this, b, relation)) { + return 0; + } + if (relation.foundSharedVertex()) { + return relation.containsEdge() ? 1 : -1; + } + + // There are no edge intersections or shared vertices, so we can check whether A contains an + // arbitrary vertex of B. + return contains(b.vertex(0)) ? 1 : -1; + } + + /** + * Returns true if two loops have the same boundary. This is true if and only if the loops have + * the same vertices in the same cyclic order. The empty and full loops are considered to have + * different boundaries. (For testing purposes.) + */ + boolean boundaryEquals(S2Loop b) { + if (numVertices != b.numVertices) { + return false; + } + + // Special case to handle empty or full loops. Since they have the same number of vertices, if + // one loop is empty/full then so is the other. + if (isEmptyOrFull()) { + return isEmpty() == b.isEmpty(); + } + + for (int offset = 0; offset < numVertices; ++offset) { + if (vertex(offset).equalsPoint(b.vertex(0))) { + // There is at most one starting offset since loop vertices are unique. + for (int i = 0; i < numVertices; ++i) { + if (!vertex(i + offset).equalsPoint(b.vertex(i))) { + return false; + } + } + return true; + } + } + return false; + } + + /** + * Returns true if two loops have the same boundary except for vertex perturbations. More + * precisely, the vertices in the two loops must be in the same cyclic order, and corresponding + * vertex pairs must be separated by no more than maxError. Note: This method mostly useful only + * for testing purposes. + */ + boolean boundaryApproxEquals(S2Loop b, double maxError) { + final S2Loop a = this; + if (a.numVertices != b.numVertices) { + return false; + } + + // Special case to handle empty or full loops. Since they have the same number of vertices, if + // one loop is empty/full then so is the other. + if (isEmptyOrFull()) { + return isEmpty() == b.isEmpty(); + } + + for (int offset = 0; offset < a.numVertices; ++offset) { + if (S2.approxEquals(a.vertex(offset), b.vertex(0), maxError)) { + boolean success = true; + for (int i = 0; i < a.numVertices; ++i) { + if (!S2.approxEquals(a.vertex(i + offset), b.vertex(i), maxError)) { + success = false; + break; + } + } + if (success) { + return true; + } + // Otherwise continue looping. There may be more than one candidate starting offset since + // vertices are only matched approximately. + } + } + return false; + } + + boolean boundaryApproxEquals(S2Loop loop) { + return boundaryApproxEquals(loop, 1e-15); + } + + /** + * Offsets into two loops at which a boundary distance comparison will start. + * + *

        Used only by {@code matchBoundaries}. + */ + private static final class LoopOffsets { + /** The offset of the first loop. */ + public final int first; + /** The offset of the second loop. */ + public final int second; + + public LoopOffsets(int first, int second) { + this.first = first; + this.second = second; + } + + @Override + public int hashCode() { + return first * 517 + second; + } + + @Override + public boolean equals(Object o) { + if (o instanceof LoopOffsets) { + LoopOffsets that = (LoopOffsets) o; + return this.first == that.first && this.second == that.second; + } else { + return false; + } + } + } + + /** + * Helper method called by {@code boundaryNear()} to determine if this loop and loop {@code b} + * remain within {@code maxError} of each other, starting the comparison with this loop at vertex + * {@code a_offset} and loop {@code b} at vertex 0. + */ + boolean matchBoundaries(S2Loop b, int aOffset, double maxError) { + final S2Loop a = this; + + // The state consists of a pair (i,j). A state transition consists of incrementing either "i" + // or "j". "i" can be incremented only if a(i+1+a_offset) is near the edge from b(j) to b(j+1), + // and a similar rule applies to "j". The function returns true iff we can proceed all the way + // around both loops in this way. + // + // Note that when "i" and "j" can both be incremented, sometimes only one choice leads to a + // solution. We handle this using a stack and backtracking. We also keep track of which states + // have already been explored to avoid duplicating work. + + List pending = Lists.newArrayList(); + Multiset done = HashMultiset.create(); + pending.add(new LoopOffsets(0, 0)); + while (!pending.isEmpty()) { + LoopOffsets last = pending.remove(pending.size() - 1); + int i = last.first; + int j = last.second; + if (i == a.numVertices && j == b.numVertices) { + return true; + } + done.add(new LoopOffsets(i, j)); + + // If (i == na && offset == na-1) where na == a.numVertices, then (i+1+offset) overflows the + // [0, 2*na-1] range allowed by vertex(). So we reduce the range if necessary. + int io = i + aOffset; + if (io >= a.numVertices) { + io -= a.numVertices; + } + + if (i < a.numVertices + && done.count(new LoopOffsets(i + 1, j)) == 0 + && S2EdgeUtil.getDistance(a.vertex(io + 1), b.vertex(j), b.vertex(j + 1)).radians() + <= maxError) { + pending.add(new LoopOffsets(i + 1, j)); + } + if (j < b.numVertices + && done.count(new LoopOffsets(i, j + 1)) == 0 + && S2EdgeUtil.getDistance(b.vertex(j + 1), a.vertex(io), a.vertex(io + 1)).radians() + <= maxError) { + pending.add(new LoopOffsets(i, j + 1)); + } + } + return false; + } + + /** + * Returns true if the two loop boundaries are within {@code maxError} of each other along their + * entire lengths. The two loops may have different numbers of vertices. More precisely, this + * method returns true if the two loops have parameterizations a:[0,1] -> S^2, b:[0,1] -> S^2 such + * that {@code distance(a(t), b(t)) <= maxError} for all t. + * + *

        You can think of this as testing whether it is possible to drive two cars all the way around + * the two loops such that no car ever goes backward and the cars are always within {@code + * maxError} of each other. + * + *

        (Package private, only used for testing purposes.) + */ + boolean boundaryNear(S2Loop b, double maxError) { + // Special case to handle empty or full loops. + if (isEmptyOrFull() || b.isEmptyOrFull()) { + return (isEmpty() && b.isEmpty()) || (isFull() && b.isFull()); + } + + for (int aOffset = 0; aOffset < numVertices; ++aOffset) { + if (matchBoundaries(b, aOffset, maxError)) { + return true; + } + } + return false; + } + + boolean boundaryNear(S2Loop loop) { + return boundaryNear(loop, 1e-15); + } + + // S2Region interface (see {@code S2Region} for details): + + /** + * Returns a spherical cap that bounds this loop. It may be expanded slightly such that if the + * loop contains a point P, then the bound contains P also. + */ + @Override + public S2Cap getCapBound() { + return bound.getCapBound(); + } + + /** + * Returns a fairly tight bounding latitude-longitude rectangle. It is not guaranteed to be as + * tight as possible, to ensure that if the loop contains a point P, then the bound contains P + * also. + */ + @Override + public S2LatLngRect getRectBound() { + return bound; + } + + /** + * Returns a slightly looser bounding latitude-longitude rectangle than that returned by {@link + * #getRectBound()}. It is not guaranteed that if this loop contains a loop X, then the subregion + * bound will contain X.getRectBound(). + */ + public S2LatLngRect getSubregionBound() { + return subregionBound; + } + + /** + * If this method returns true, the region completely contains the given cell. Otherwise, either + * the region does not contain the cell or the containment relationship could not be determined. + */ + @Override + public boolean contains(S2Cell target) { + S2Iterator it = index.iterator(); + S2ShapeIndex.CellRelation relation = it.locate(target.id()); + + // If 'target' is disjoint from all index cells, it is not contained. Similarly, if 'target' is + // subdivided into one or more index cells, then it is not contained, since index cells are + // subdivided only if they (nearly) intersect a sufficient number of edges. (But note that if + // 'target' itself is an index cell then it may be contained, since it could be a cell with no + // edges in the loop interior. + if (relation != S2ShapeIndex.CellRelation.INDEXED) { + return false; + } + + // Otherwise check if any edges intersect 'target'. + if (boundaryApproxIntersects(it, target)) { + return false; + } + + // Otherwise check if the loop contains the center of 'target'. + return contains(it, target.getCenter()); + } + + /** + * If this method returns false, the region does not intersect the given cell. Otherwise, either + * region intersects the cell, or the intersection relationship could not be determined. + */ + @Override + public boolean mayIntersect(S2Cell target) { + S2Iterator it = index.iterator(); + S2ShapeIndex.CellRelation relation = it.locate(target.id()); + + // If 'target' does not overlap any index cell, there is no intersection. + if (relation == S2ShapeIndex.CellRelation.DISJOINT) { + return false; + } + + // If 'target' is subdivided into one or more index cells, there is an intersection to within + // the S2ShapeIndex error bound (see contains). + if (relation == S2ShapeIndex.CellRelation.SUBDIVIDED) { + return true; + } + + // If 'target' is an index cell, there is an intersection because index cells are created only + // if they have at least one edge or they are entirely contained by that loop. + if (it.compareTo(target.id()) == 0) { + return true; + } + + // Otherwise check if any edges intersect 'target'. + if (boundaryApproxIntersects(it, target)) { + return true; + } + + // Otherwise check if the loop contains the center of 'target'. + return contains(it, target.getCenter()); + } + + /** + * Returns true if the loop boundary intersects 'target'. It may also return true when the loop + * boundary does not intersect 'target' but some edge comes within the worst-case error tolerance. + * + *

        Requires: it.id().contains(target.id()). (This condition is true whenever it.locate(target) + * returns INDEXED.) + */ + private boolean boundaryApproxIntersects(S2Iterator it, S2Cell target) { + // assert (it.id().contains(target.id())); + S2ClippedShape aClipped = it.entry().clipped(0); + int aNumClipped = aClipped.numEdges(); + + // If there are no edges, there is no intersection. + if (aNumClipped == 0) { + return false; + } + + // We can save some work if 'target' is the index cell itself. + if (it.compareTo(target.id()) == 0) { + return true; + } + + // Otherwise check whether any of the edges intersect 'target'. + R2Rect bound = target.getBoundUV().expanded(S2EdgeUtil.MAX_CELL_EDGE_ERROR); + R2Vector v0 = new R2Vector(); + R2Vector v1 = new R2Vector(); + for (int i = 0; i < aNumClipped; ++i) { + int ai = aClipped.edge(i); + if (S2EdgeUtil.clipToPaddedFace( + vertex(ai), vertex(ai + 1), target.face(), S2EdgeUtil.MAX_CELL_EDGE_ERROR, v0, v1) + && S2EdgeUtil.intersectsRect(v0, v1, bound)) { + return true; + } + } + + return false; + } + + /** + * Returns a simplified loop, which may be self-intersecting, or null if the entire loop was + * within the tolerance. + * + *

        If self-intersections could occur and a valid result is needed, instead use {@link + * S2Polygon#initToSimplified(S2Polygon, S1Angle, boolean)}. + * + *

        Always keeps the first vertex from the loop, and if {@code vertexFilter} is not null, also + * keeps vertices for which {@code vertexFilter.shouldKeepVertex()} is true. + */ + // Covered by tests of S2Polygon.initToSimplified. + public S2Loop simplify(S1Angle tolerance, Predicate vertexFilter) { + if (vertices.length < 2) { + // Unable to simplify further, just return whatever this loop is. + return this; + } + + // Add the last point so we can simplify along the final edge as well, and then remove the last + // point if it's still there. + List points = Lists.newArrayListWithCapacity(vertices.length + 1); + Collections.addAll(points, vertices); + points.add(vertices[0]); + S2Polyline line = new S2Polyline(points); + List simplified = line.subsampleVertices(tolerance).vertices(); + if (simplified.get(0).equalsPoint(Iterables.getLast(simplified))) { + simplified = simplified.subList(0, simplified.size() - 1); + } + + // Merge in vertices that we should keep, if a filter was provided. + if (vertexFilter != null) { + List toKeep = Lists.newArrayList(); + for (int i = 0, j = 0; i < numVertices(); i++) { + S2Point p = vertex(i); + if (simplified.get(j).equalsPoint(p)) { + j++; + toKeep.add(p); + } else if (vertexFilter.apply(p)) { + toKeep.add(p); + } + } + simplified = toKeep; + } + + return simplified.size() <= 2 ? null : new S2Loop(simplified); + } + + /** + * Returns true if the point is contained by the loop. The containment test is exact, placing + * {@code p} arbitrarily within or without the loop depending on orientation of the edges, such + * that given two loops sharing an edge, and a point on that edge, only one of the loops will + * contain it. The point does not need to be normalized. + */ + @Override + public boolean contains(S2Point p) { + // NOTE(user): A bounds check slows down the C++ version of this function by about 50%. It is + // worthwhile only when it might allow us to delay building the index. + if (!index.isFresh() && bound != null && !bound.contains(p)) { + return false; + } + + // We must use the bruteForceContains() during loop initialization because initOriginAndBound() + // calls contains() before creating the index. Also, for small loops it is faster to just check + // all the crossings. Otherwise, we keep track of the number of calls to contains() and only + // build the index when enough calls have been made so that we think it is worth the effort. + // Note that the code below is structured so that if many calls are made in parallel, only one + // thread builds the index, while the rest continue using brute force until the index is + // actually available. + int maxBruteForceVertices = 32; + if (numVertices <= maxBruteForceVertices + || (!index.isFresh() && unindexedContainsCalls.decrementAndGet() > 0)) { + return bruteForceContains(p); + } + + // Otherwise we look up the S2ShapeIndex cell containing this point. Note that the index is + // built automatically the first time an iterator is created. + S2Iterator it = index.iterator(); + if (!it.locate(p)) { + return false; + } + return contains(it, p); + } + + // TODO(b/120887495): This @VisibleForTesting annotation was being ignored by prod code. + // Please check that removing it is correct, and remove this comment along with it. + // @VisibleForTesting + boolean bruteForceContains(S2Point p) { + // 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 (numVertices < 3) { + return originInside; + } + + S2Point origin = S2.origin(); + S2EdgeUtil.EdgeCrosser crosser = new S2EdgeUtil.EdgeCrosser(origin, p, vertex(0)); + boolean inside = originInside; + for (int i = 1; i <= numVertices; ++i) { + inside ^= crosser.edgeOrVertexCrossing(vertex(i)); + } + return inside; + } + + /** + * Given an iterator that is already positioned at the S2ShapeIndexCell containing {@code p}, + * returns contains(p). + */ + private boolean contains(S2Iterator it, S2Point p) { + // Test containment by drawing a line segment from the cell center to the given point and + // counting edge crossings. + S2ClippedShape aClipped = it.entry().clipped(0); + boolean inside = aClipped.containsCenter(); + int aNumClipped = aClipped.numEdges(); + if (aNumClipped > 0) { + S2Point center = it.center(); + S2EdgeUtil.EdgeCrosser crosser = new S2EdgeUtil.EdgeCrosser(center, p); + int aiPrev = -2; + for (int i = 0; i < aNumClipped; ++i) { + int ai = aClipped.edge(i); + if (ai != aiPrev + 1) { + crosser.restartAt(vertex(ai)); + } + aiPrev = ai; + inside ^= crosser.edgeOrVertexCrossing(vertex(ai + 1)); + } + } + return inside; + } + + /** + * Returns the shortest distance from a point P to this loop, given as the angle formed between P, + * the origin and the nearest point on the loop to P. This angle in radians is equivalent to the + * arclength along the unit sphere. + */ + public S1Angle getDistance(S2Point p) { + S2Point normalized = S2Point.normalize(p); + + // The farthest point from p on the sphere is its antipode, which is an angle of PI radians. + // This is an upper bound on the angle. + S1Angle minDistance = S1Angle.radians(Math.PI); + for (int i = 0; i < numVertices; i++) { + minDistance = + S1Angle.min(minDistance, S2EdgeUtil.getDistance(normalized, vertex(i), vertex(i + 1))); + } + return minDistance; + } + + /** + * Return true if the S2:origin() is inside this loop. + * + *

        Primarily used to serialize internal details about a loop for later fast initialization. + */ + public boolean isOriginInside() { + return originInside; + } + + /** Returns true if this loop is valid. */ + public boolean isValid() { + return !findValidationError(new S2Error()); + } + + /** + * Static version of isValid(), to be used only when an S2Loop instance is not available, but + * validity of the points must be checked. + * + * @return true if the given loop is valid. Creates an instance of S2Loop and defers this call to + * {@link #isValid()}. + */ + public static boolean isValid(List vertices) { + return new S2Loop(vertices).isValid(); + } + + /** + * Returns true if this is *not* a valid loop and sets {@code error} appropriately. Otherwise + * returns false and leaves {@code error} unchanged. Requires that error != null. + */ + public boolean findValidationError(S2Error error) { + return findValidationErrorNoIndex(error) + || S2ShapeUtil.findSelfIntersection(index, this, error); + } + + /** + * Like findValidationError(), but skips any checks that would require building the S2ShapeIndex + * (i.e., self-intersection tests). This will be used by the S2Polygon implementation, which uses + * its own index to check for loop self-intersection. + */ + public boolean findValidationErrorNoIndex(S2Error error) { + // subregionBound must be at least as large as bound. (This is an internal consistency check + // rather than a test of client data. + // assert(subregionBound.contains(bound)); + + // All vertices must be unit length. + for (int i = 0; i < numVertices; ++i) { + if (!S2.isUnitLength(vertex(i))) { + error.init(S2Error.Code.NOT_UNIT_LENGTH, "Vertex " + i + " is not unit length."); + return true; + } + } + + // Loops must have at least 3 vertices (except for 'empty' and 'full'). + if (numVertices < 3) { + if (isEmptyOrFull()) { + // Skip the remaining tests. + return false; + } + error.init( + S2Error.Code.LOOP_NOT_ENOUGH_VERTICES, + "Non-empty, non-full loops must have at least 3 vertices"); + return true; + } + + // Loops are not allowed to have any degenerate edges (edge with identical vertices). + for (int i = 0; i < numVertices; ++i) { + if (vertex(i).equalsPoint(vertex(i + 1))) { + error.init( + S2Error.Code.DUPLICATE_VERTICES, "Edge " + i + " is degenerate (duplicate vertex)."); + return true; + } + } + + return false; + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder("S2Loop, "); + + builder.append(vertices.length).append(" points. ["); + + for (S2Point v : vertices) { + builder.append(v.toDegreesString()).append(" "); + } + builder.append("]"); + + return builder.toString(); + } + + private void initOriginAndBound() { + if (numVertices < 3) { + // Check for the special 'empty' and 'full' loops (which have one vertex). + if (isEmptyOrFull()) { + // If the vertex is in the southern hemisphere then the loop is full, otherwise it is empty. + originInside = (vertex(0).z < 0); + } else { + // Bail out without trying to access non-existent vertices. + originInside = false; + } + } else { + // Point containment testing is done by counting edge crossings starting at a fixed point on + // the sphere. (S2.origin()). Historically this was important, but it is now no longer + // necessary, and it may be worthwhile experimenting with using a loop vertex as the + // reference point. In any case, we need to know whether the reference point (S2.origin) 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 = S2.ortho(B) 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 S2EdgeUtil.vertexCrossing. (Note that we can't use S2.origin() as the + // fixed vector because of the possibility that B == S2.origin().) + // + // TODO(user): Investigate using vertex(0) as the reference point. + + // Initialize before calling contains(). + originInside = false; + boolean v1Inside = + S2Predicates.orderedCCW(S2.ortho(vertex(1)), vertex(0), vertex(2), vertex(1)); + if (v1Inside != contains(vertex(1))) { + originInside = true; + } + } + initBound(); + } + + /** Initializes the bound. Requires {@code bound == null}. */ + private void initBound() { + if (numVertices < 3) { + if (isFull()) { + subregionBound = bound = S2LatLngRect.full(); + } else { + subregionBound = bound = S2LatLngRect.empty(); + } + 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. + S2EdgeUtil.RectBounder bounder = new S2EdgeUtil.RectBounder(); + for (int i = 0; i <= numVertices; ++i) { + bounder.addPoint(vertex(i)); + } + S2LatLngRect b = bounder.getBound(); + + // Note that this and the following contains calls only work correctly if 'bound' is already + // null. When constructing the loop, it will be, but invert() has to clear the bound first. + if (contains(S2Point.Z_POS)) { + b = new S2LatLngRect(new R1Interval(b.lat().lo(), S2.M_PI_2), 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. + if (b.lng().isFull() && contains(S2Point.Z_NEG)) { + b = new S2LatLngRect(new R1Interval(-S2.M_PI_2, b.lat().hi()), b.lng()); + } + + bound = b; + subregionBound = S2EdgeUtil.RectBounder.expandForSubregions(bound); + } + + /** + * Return the index of a vertex at point "p", or -1 if not found. The return value is in the range + * 1..num_vertices_ if found. + */ + @VisibleForTesting + int findVertex(S2Point p) { + // 10 is the max number of edges in a cell. As an optimization, it is directly entered here + // instead of creating an S2ShapeIndex.Options and then calling getMaxEdgesPerCell. + // TODO(eengle): Create and use Options.DEFAULT_MAX_EDGES_PER_CELL instead. + if (numVertices < 10) { + // Exhaustive search. Return value must be in the range [1..N]. + for (int i = 1; i <= numVertices; ++i) { + if (vertex(i).equalsPoint(p)) { + return i; + } + } + return -1; + } + S2Iterator it = index.iterator(); + if (!it.locate(p)) { + return -1; + } + + S2ClippedShape aClipped = it.entry().clipped(0); + for (int i = aClipped.numEdges() - 1; i >= 0; --i) { + int ai = aClipped.edge(i); + // Return value must be in the range [1..N]. + if (vertex(ai).equalsPoint(p)) { + return (ai == 0) ? numVertices : ai; + } + if (vertex(ai + 1).equalsPoint(p)) { + return ai + 1; + } + } + return -1; + } + + + private static class CompressedEncodingProperties { + + // Recomputing the bound multiplies the decode time taken per vertex by a factor of about 3.5. + // Without recomputing the bound, decode takes approximately 125 ns / vertex. A loop with 63 + // vertices encoded without the bound will take ~30us to decode, which is acceptable. + // At ~3.5 bytes / vertex without the bound, adding the bound will increase the size by <15%, + // which is also acceptable. + private static final int MIN_LOOP_VERTICES_FOR_BOUND = 64; + + public enum Property { + ORIGIN_INSIDE(1L), + BOUND_ENCODED(1L << 1); + + public long bitValue; + + private Property(long bitValue) { + this.bitValue = bitValue; + } + } + + private long bits = 0L; + + public CompressedEncodingProperties(S2Loop loop) { + if (loop.containsOrigin()) { + setProperty(Property.ORIGIN_INSIDE); + } + + // Write whether there is a bound so we can change the threshold later. + if (loop.numVertices() >= MIN_LOOP_VERTICES_FOR_BOUND) { + setProperty(Property.BOUND_ENCODED); + } + } + + public CompressedEncodingProperties(long bits) { + this.bits = bits; + } + + public void setProperty(Property property) { + bits ^= property.bitValue; + } + + public boolean hasProperty(Property property) { + return (bits & property.bitValue) != 0; + } + + public long asLong() { + return bits; + } + } + + void encodeCompressed(int level, LittleEndianOutput encoder) throws IOException { + // Encode the number of vertices. + encoder.writeVarint32(numVertices()); + + // Encode the individual vertices. + S2PointCompression.encodePointsCompressed(Arrays.asList(vertices), level, encoder); + + // Encode the compression properties. + CompressedEncodingProperties properties = new CompressedEncodingProperties(this); + encoder.writeVarint64(properties.asLong()); + + // Encode the depth. + encoder.writeVarint32(depth()); + + // Optionally encode the bounds, if the properties indicate it should be encoded. + if (properties.hasProperty(CompressedEncodingProperties.Property.BOUND_ENCODED)) { + getRectBound().encode(encoder); + } + } + + static S2Loop decodeCompressed(int level, LittleEndianInput decoder) throws IOException { + // Decode the number of vertices. + int numVertices = decoder.readVarint32(); + + // Decode the individual vertices. + List vertices = S2PointCompression.decodePointsCompressed(numVertices, level, decoder); + + // Decode the compression properties. + CompressedEncodingProperties properties = + new CompressedEncodingProperties(decoder.readVarint64()); + + // Decode the depth. + int depth = decoder.readVarint32(); + + // If the bounds are encoded, decode them and instantiate the loop with all the available + // information. Otherwise, just create the new loop by passing in the vertices, and let it do + // the full instantiation. + S2Loop loop = null; + if (properties.hasProperty(CompressedEncodingProperties.Property.BOUND_ENCODED)) { + // TODO(eengle): Many loops will be small enough that the bound isn't encoded, but we still + // want to init with the known originInside value to avoid wasted work. + boolean originInside = + properties.hasProperty(CompressedEncodingProperties.Property.ORIGIN_INSIDE); + + S2LatLngRect bound = S2LatLngRect.decode(decoder); + // Since we have all the ingredients, we can speed up construction here. + loop = S2Loop.newLoopWithTrustedDetails(vertices, originInside, bound); + } else { + loop = new S2Loop(vertices); + } + + // Set the depth explicitly, since it's not set during instantiation. + loop.setDepth(depth); + + return loop; + } + + private static S2Loop decodeInternal(LittleEndianInput decoder) throws IOException { + int numVertices = decoder.readInt(); + Preconditions.checkState( + numVertices >= 0, "Loops with more than 2^31 - 1 vertices not supported."); + + ArrayList vertices = new ArrayList<>(numVertices); + for (int i = 0; i < numVertices; i++) { + vertices.add(S2Point.decode(decoder)); + } + + boolean originInside = decoder.readByte() != 0; + int depth = decoder.readInt(); + S2LatLngRect bound = S2LatLngRect.decode(decoder); + S2Loop loop = S2Loop.newLoopWithTrustedDetails(vertices, originInside, bound); + loop.setDepth(depth); + + // An initialized loop will have some non-zero count of vertices. An uninitialized loop has zero + // vertices. This code supports encoding and decoding of uninitialized loops, but we only want + // to call InitIndex for initialized loops. Otherwise we defer InitIndex until the call to + // Init(). + if (numVertices > 0) { + loop.initIndex(); + } + + return loop; + } + + private void encodeInternal(LittleEndianOutput encoder) throws IOException { + encoder.writeInt(numVertices); + for (int i = 0; i < numVertices; i++) { + vertex(i).encode(encoder); + } + encoder.writeByte(isOriginInside() ? (byte) 1 : (byte) 0); + encoder.writeInt(depth); + bound.encode(encoder); + } + + /** Encodes this S2Loop using the lossless encoding. */ + void encode(LittleEndianOutput encoder) throws IOException { + // Only LOSSLESS encoding is supported. + encoder.writeByte(LOSSLESS_ENCODING_VERSION); + encodeInternal(encoder); + } + + /** + * Returns a loop decoded from the given stream. Note S2Loops are intended to be serialized as + * part of an S2Polygon; see {@link S2Polygon#decode(java.io.InputStream)}. + */ + static S2Loop decode(LittleEndianInput decoder) throws IOException { + byte version = decoder.readByte(); + switch (version) { + case LOSSLESS_ENCODING_VERSION: + return decodeInternal(decoder); + + default: + throw new IOException( + "Unknown S2Loop encoding version encountered during decoding: " + version); + } + } + + /** + * This method checks all edges of loop A for intersection against all edges of loop B. If there + * is any shared vertex, the wedges centered at this vertex are set to {@code relation}. + */ + private static boolean hasCrossingRelation(S2Loop a, S2Loop b, LoopRelation relation) { + // We look for S2CellId ranges where the indexes of A and B overlap, and then test those edges + // for crossings. + S2ShapeIndex.RangeIterator ai = new S2ShapeIndex.RangeIterator(a.index); + S2ShapeIndex.RangeIterator bi = new S2ShapeIndex.RangeIterator(b.index); + // Tests edges of A against B. + LoopCrosser ab = new LoopCrosser(a, b, relation, false); + // Tests edges of B against A. + LoopCrosser ba = new LoopCrosser(b, a, relation, true); + while (!ai.done() || !bi.done()) { + if (ai.rangeMax().lessThan(bi.rangeMin())) { + // The A and B cells don't overlap, and A precedes B. + ai.seekTo(bi); + } else if (bi.rangeMax().lessThan(ai.rangeMin())) { + // The A and B cells don't overlap, and B precedes A. + bi.seekTo(ai); + } else { + // One cell contains the other. Determine which cell is larger. + long abRelation = UnsignedLongs.compare(ai.id().lowestOnBit(), bi.id().lowestOnBit()); + if (abRelation > 0) { + // A's index cell is larger. + if (ab.hasCrossingRelation(ai, bi)) { + return true; + } + } else if (abRelation < 0) { + // B's index cell is larger. + if (ba.hasCrossingRelation(bi, ai)) { + return true; + } + } else { + // The A and B cells are the same. Since the two cells have the same center point P, check + // whether P satisfies the crossing targets. + if (ab.aCrossingTarget() == (ai.containsCenter() ? 1 : 0) + && ab.bCrossingTarget() == (bi.containsCenter() ? 1 : 0)) { + return true; + } + // Otherwise test all the edge crossings directly. + if (ai.numEdges() > 0 + && bi.numEdges() > 0 + && ab.cellCrossesCell(ai.clipped(), bi.clipped())) { + return true; + } + ai.next(); + bi.next(); + } + } + } + return false; + } + + /** + * LoopCrosser is a helper class for determining whether two loops cross. It is instantiated twice + * for each pair of loops to be tested, once for the pair (A, B) and once for the pair (B, A), in + * order to be able to process edges in either loop nesting order. + */ + private static final class LoopCrosser { + private final S2Loop a; + private final S2Loop b; + private final LoopRelation relation; + private final boolean swapped; + private final int aCrossingTarget; + private final int bCrossingTarget; + + // State maintained by startEdge() and edgeCrossesCell(). + private S2EdgeUtil.EdgeCrosser crosser; + private int aj; + private int bjPrev; + + // Temporary data declared here to avoid repeated memory allocations. + private final S2EdgeQuery bQuery; + private final List bCells; + + /** + * If {@code swapped} is true, the loops A and B have been swapped. This affects how arguments + * are passed to the given loop relation, since for example A.contains(B) is not the same as + * B.contains(A). + */ + public LoopCrosser(S2Loop a, S2Loop b, LoopRelation relation, boolean swapped) { + this.a = a; + this.b = b; + this.relation = relation; + this.swapped = swapped; + aCrossingTarget = swapped ? relation.bCrossingTarget() : relation.aCrossingTarget(); + bCrossingTarget = swapped ? relation.aCrossingTarget() : relation.bCrossingTarget(); + bQuery = new S2EdgeQuery(b.index); + bCells = Lists.newArrayList(); + } + + /** + * Returns the crossing targets for the loop relation, taking into account whether the loops + * have been swapped. + */ + public int aCrossingTarget() { + return aCrossingTarget; + } + + public int bCrossingTarget() { + return bCrossingTarget; + } + + /** + * Given two iterators positioned such that {@code ai.id().contains(bi.id())}, returns true if + * there is a crossing relationship anywhere within {@code ai.id()}. Specifically, this method + * returns true if there is an edge crossing, a wedge crossing, or a point P that matches both + * "crossing targets". Advances both iterators past {@code ai.id()}. + */ + public boolean hasCrossingRelation( + S2ShapeIndex.RangeIterator ai, S2ShapeIndex.RangeIterator bi) { + // assert(ai.id().contains(bi.id())); + if (ai.numEdges() == 0) { + if (aCrossingTarget == (ai.containsCenter() ? 1 : 0)) { + // All points within ai.id() satisfy the crossing target for A, so it's worth iterating + // through the cells of B to see whether any cell centers also satisfy the crossing target + // for B. + S2CellId maxRange = ai.rangeMax(); + do { + if (bCrossingTarget == (bi.containsCenter() ? 1 : 0)) { + return true; + } + bi.next(); + } while (bi.id().lessOrEquals(maxRange)); + } else { + // The crossing target for A is not satisfied, so we skip over the cells of B using + // binary search. + bi.seekBeyond(ai); + } + } else { + // The current cell of A has at least one edge, so check for crossings. + if (hasCrossing(ai, bi)) { + return true; + } + } + ai.next(); + return false; + } + + /** + * Given two index cells, returns true if there are any edge crossings or wedge crossings within + * those cells. + */ + public boolean cellCrossesCell(S2ClippedShape aClipped, S2ClippedShape bClipped) { + // Test all edges of 'aClipped' against all edges of 'bClipped'. + int aNumClipped = aClipped.numEdges(); + for (int i = 0; i < aNumClipped; ++i) { + startEdge(aClipped.edge(i)); + if (edgeCrossesCell(bClipped)) { + return true; + } + } + return false; + } + + /** + * Given two iterators positioned such that {@code ai.id().contains(bi.id())}, returns true if + * there is an edge crossing or a wedge crossing anywhere within {@code ai.id()}. Advances + * {@code bi} (only) past {@code ai.id()}. + */ + private boolean hasCrossing(S2ShapeIndex.RangeIterator ai, S2ShapeIndex.RangeIterator bi) { + // assert(ai.id().contains(bi.id())); + // If ai.id() intersects many edges of B, then it is faster to use S2EdgeQuery to narrow down + // the candidates. But if it intersects only a few edges, it is faster to check all the + // crossings directly. We handle this by advancing 'bi' and keeping track of how many edges we + // would need to test. + + // Tuned using Caliper benchmarking. + final int edgeQueryMinEdges = 40; + int totalEdges = 0; + bCells.clear(); + S2CellId maxRange = ai.rangeMax(); + do { + if (bi.numEdges() > 0) { + totalEdges += bi.numEdges(); + if (totalEdges >= edgeQueryMinEdges) { + // There are too many edges to test them directly, so use S2EdgeQuery. + if (cellCrossesAnySubcell(ai.clipped(), ai.id())) { + return true; + } + bi.seekBeyond(ai); + return false; + } + bCells.add(bi.cell()); + } + bi.next(); + } while (bi.id().lessOrEquals(maxRange)); + + // Test all the edge crossings directly. + for (int c = 0; c < bCells.size(); ++c) { + if (cellCrossesCell(ai.clipped(), bCells.get(c).clipped(0))) { + return true; + } + } + + return false; + } + + /** + * Given an index cell of A, returns true if there are any edge or wedge crossings with any + * index cell of B contained within {@code bId}. + */ + private boolean cellCrossesAnySubcell(S2ClippedShape aClipped, S2CellId bId) { + // Test all edges of 'aClipped' against all edges of B. The relevant B edges are guaranteed to + // be children of 'bId', which lets us find the correct index cells more efficiently. + S2PaddedCell bRoot = new S2PaddedCell(bId, 0); + int aNumClipped = aClipped.numEdges(); + for (int i = 0; i < aNumClipped; ++i) { + int aj = aClipped.edge(i); + // Use an S2EdgeQuery starting at 'bRoot' to find the index cells of B that might contain + // crossing edges. + if (!bQuery.getCells(a.vertex(aj), a.vertex(aj + 1), bRoot, bCells)) { + continue; + } + startEdge(aj); + for (int c = 0; c < bCells.size(); ++c) { + if (edgeCrossesCell(bCells.get(c).clipped(0))) { + return true; + } + } + } + return false; + } + + /** Prepares to check the given edge of loop A for crossings. */ + private void startEdge(int aj) { + // Start testing the given edge of A for crossings. + crosser = new S2EdgeUtil.EdgeCrosser(a.vertex(aj), a.vertex(aj + 1)); + this.aj = aj; + bjPrev = -2; + } + + /** + * Checks the current edge of loop A for crossings with all edges of the given index cell of + * loop B. + */ + private boolean edgeCrossesCell(S2ClippedShape bClipped) { + // Test the current edge of A against all edges of 'bClipped'. + int bNumClipped = bClipped.numEdges(); + for (int j = 0; j < bNumClipped; ++j) { + int bj = bClipped.edge(j); + if (bj != bjPrev + 1) { + crosser.restartAt(b.vertex(bj)); + } + bjPrev = bj; + int crossing = crosser.robustCrossing(b.vertex(bj + 1)); + if (crossing < 0) { + continue; + } + if (crossing > 0) { + return true; + } + // We only need to check each shared vertex once, so we only consider the case where + // a.vertex(aj + 1).equalsPoint(b.vertex(bj + 1)). + if (a.vertex(aj + 1).equalsPoint(b.vertex(bj + 1))) { + if (swapped) { + if (relation.wedgesCross( + b.vertex(bj), b.vertex(bj + 1), b.vertex(bj + 2), a.vertex(aj), a.vertex(aj + 2))) { + return true; + } + } else { + if (relation.wedgesCross( + a.vertex(aj), a.vertex(aj + 1), a.vertex(aj + 2), b.vertex(bj), b.vertex(bj + 2))) { + return true; + } + } + } + } + return false; + } + } + + /** A relation between two loops (e.g. Contains, Intersects, or CompareBoundary.) */ + private interface LoopRelation { + /** + * Optionally, {@code aCrossingTarget} and {@code bCrossingTarget} can specify an early-exit + * condition for the loop relation. If any point P is found such that + * + *

        {@code aCrossingTarget == (a.contains(P) ? 1 : 0) && bCrossingTarget == (b.contains(P) ? 1 + * : 0) } + * + *

        then the loop relation is assumed to be the same as if a pair of crossing edges were + * found. For example, the contains() relation has + * + *

        {@code aCrossingTarget() == 0 bCrossingTarget() == 1 } + * + *

        because if {@code !a.contains(P)} and {@code b.contains(P)} for any point P, then it is + * equivalent to finding an edge crossing (i.e., since contains() returns false in both cases). + * + *

        Loop relations that do not have an early-exit condition of this form should return -1 for + * both crossing targets. + */ + int aCrossingTarget(); + + int bCrossingTarget(); + + /** + * Given a vertex {@code ab1} that is shared between the two loops, returns true if the two + * associated wedges (a0, ab1, b2) and (b0, ab1, b2) are equivalent to an edge crossing. The + * loop relation is also allowed to maintain its own internal state, and can return true if it + * observes any sequence of wedges that are equivalent to an edge crossing. + */ + boolean wedgesCross(S2Point a0, S2Point ab1, S2Point a2, S2Point b0, S2Point b2); + } + + /** Loop relation for contains(). */ + private static final class ContainsRelation implements LoopRelation { + private boolean foundSharedVertex = false; + + public boolean foundSharedVertex() { + return foundSharedVertex; + } + + /** + * If A.contains(P) == false && B.contains(P) == true, it is equivalent to having an edge + * crossing (i.e., contains() returns false). + */ + @Override + public int aCrossingTarget() { + // signifies false + return 0; + } + + @Override + public int bCrossingTarget() { + // signifies true + return 1; + } + + @Override + public boolean wedgesCross(S2Point a0, S2Point ab1, S2Point a2, S2Point b0, S2Point b2) { + foundSharedVertex = true; + return new S2EdgeUtil.WedgeContains().test(a0, ab1, a2, b0, b2) != 1; + } + } + + /** Loop relation for intersects(). */ + private static final class IntersectsRelation implements LoopRelation { + private boolean foundSharedVertex = false; + + public boolean foundSharedVertex() { + return foundSharedVertex; + } + + /** + * If A.contains(P) == false && B.contains(P) == true, it is equivalent to having an edge + * crossing (i.e., intersects() returns true). + */ + @Override + public int aCrossingTarget() { + // signifies true + return 1; + } + + @Override + public int bCrossingTarget() { + // signifies true + return 1; + } + + @Override + public boolean wedgesCross(S2Point a0, S2Point ab1, S2Point a2, S2Point b0, S2Point b2) { + foundSharedVertex = true; + // TODO(eengle): Make the test methods of each WedgeProcessor available as a static method, so + // we can call them directly without having to instantiate an object. + return new S2EdgeUtil.WedgeIntersects().test(a0, ab1, a2, b0, b2) == -1; + } + } + + /** Loop relation for compareBoundary(). */ + private static final class CompareBoundaryRelation implements LoopRelation { + /** True if loop B should be reversed. */ + private final boolean bReversed; + + /** True if any wedge was processed. */ + private boolean foundSharedVertex = false; + + /** True if any edge of B is contained by A. */ + private boolean containsEdge = false; + + /** True if any edge of B is excluded by A. */ + private boolean excludesEdge = false; + + public CompareBoundaryRelation(boolean reverseB) { + this.bReversed = reverseB; + } + + public boolean foundSharedVertex() { + return foundSharedVertex; + } + + public boolean containsEdge() { + return containsEdge; + } + + /** + * The CompareBoundaryRelation does not have a useful early-exit condition, so we return -1 for + * both crossing targets. + * + *

        Aside: A possible early exit condition could be based on the following: + * + *

          + *
        • If A contains a point of both B and ~B, then A intersects Boundary(B). + *
        • If ~A contains a point of both B and ~B, then ~A intersects Boundary(B). + *
        • So if the intersections of {A, ~A} with {B, ~B} are all non-empty, the return value is + * 0, i.e., Boundary(A) intersects Boundary(B). + *
        + * + * Unfortunately, it isn't worth detecting this situation because by the time we have seen a + * point in all four intersection regions, we are also guaranteed to have seen at least one pair + * of crossing edges. + */ + @Override + public int aCrossingTarget() { + // Signifies no early-exit condition. + return -1; + } + + @Override + public int bCrossingTarget() { + // Signifies no early-exit condition. + return -1; + } + + @Override + public boolean wedgesCross(S2Point a0, S2Point ab1, S2Point a2, S2Point b0, S2Point b2) { + // Because we don't care about the interior of B, only its boundary, it is sufficient to check + // whether A contains the semiwedge (ab1, b2). + foundSharedVertex = true; + if (wedgeContainsSemiwedge(a0, ab1, a2, b2, bReversed)) { + containsEdge = true; + } else { + excludesEdge = true; + } + return containsEdge && excludesEdge; + } + } +} diff --git a/core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/S2PaddedCell.java b/core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/S2PaddedCell.java new file mode 100644 index 0000000000..04c057b127 --- /dev/null +++ b/core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/S2PaddedCell.java @@ -0,0 +1,264 @@ +/* + * Copyright 2014 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.mogo.eagle.core.utilcode.geometry; + +import static com.mogo.eagle.core.utilcode.geometry.S2Projections.PROJ; +import static com.mogo.eagle.core.utilcode.geometry.S2Projections.siTiToSt; +import static com.mogo.eagle.core.utilcode.geometry.S2Projections.stToIj; + +import com.google.common.annotations.GwtCompatible; +import com.mogo.eagle.core.utilcode.geometry.R1Interval.Endpoint; + +/** + * S2PaddedCell represents an S2Cell whose (u,v)-range has been expanded on all sides by a given + * amount of "padding". Unlike S2Cell, its methods and representation are optimized for clipping + * edges against S2Cell boundaries to determine which cells are intersected by a given set of edges. + */ +@GwtCompatible +public class S2PaddedCell { + /** The cell being padded. */ + private S2CellId id; + + /** UV padding on all sides. */ + private double padding; + + /** Bound in (u,v)-space. Includes padding. */ + private R2Rect bound; + + /** + * The rectangle in (u,v)-space that belongs to all four padded children. It is computed on demand + * by the middle() accessor method. + */ + private R2Rect middle; + + /** Minimum (i,j)-coordinates of this cell, before padding. */ + private int iLo; + + private int jLo; + + /** Hilbert curve orientation of this cell. */ + private int orientation; + + /** Level of this cell. */ + private int level; + + /** Construct an S2PaddedCell for the given cell id and padding. */ + public S2PaddedCell(S2CellId id, double padding) { + this.id = id; + this.padding = padding; + if (id.isFace()) { + // Fast path for constructing a top-level face (the most common case). + double limit = 1 + padding; + bound = new R2Rect(new R1Interval(-limit, limit), new R1Interval(-limit, limit)); + middle = new R2Rect(new R1Interval(-padding, padding), new R1Interval(-padding, padding)); + iLo = jLo = 0; + orientation = id.face() & 1; + level = 0; + } else { + long ijo = id.toIJOrientation(); + int i = S2CellId.getI(ijo); + int j = S2CellId.getJ(ijo); + orientation = S2CellId.getOrientation(ijo); + level = id.level(); + bound = S2CellId.ijLevelToBoundUv(i, j, level).expanded(padding); + int ijSize = S2CellId.getSizeIJ(level); + iLo = i & -ijSize; + jLo = j & -ijSize; + } + } + + /** + * Construct the child of this cell with the given (i,j) index. The four child cells have indices + * of (0,0), (0,1), (1,0), (1,1), where the i and j indices correspond to increasing u- and + * v-values respectively. + */ + public S2PaddedCell childAtIJ(int i, int j) { + return new S2PaddedCell(this, S2.ijToPos(orientation, i * 2 + j), i, j); + } + + /** Construct the child of this cell with the given Hilbert curve position, from 0 to 3. */ + public S2PaddedCell childAtPos(int pos) { + int ij = S2.posToIJ(orientation, pos); + return new S2PaddedCell(this, pos, ij >> 1, ij & 1); + } + + /** Private constructor to create a new S2PaddedCell for the child at the given (i,j) position. */ + private S2PaddedCell(S2PaddedCell parent, int pos, int i, int j) { + this.padding = parent.padding; + this.bound = new R2Rect(parent.bound); + this.level = parent.level + 1; + + // Compute the position and orientation of the child incrementally from the orientation of the + // parent. + id = parent.id.child(pos); + int ijSize = S2CellId.getSizeIJ(level); + iLo = parent.iLo + i * ijSize; + jLo = parent.jLo + j * ijSize; + orientation = parent.orientation ^ S2.posToOrientation(pos); + + // For each child, one corner of the bound is taken directly from the parent while the + // diagonally opposite corner is taken from middle(). + R2Rect middle = parent.middle(); + Endpoint uEnd = i == 0 ? Endpoint.HI : Endpoint.LO; + bound.x().setValue(uEnd, middle.x().getValue(uEnd)); + Endpoint vEnd = j == 0 ? Endpoint.HI : Endpoint.LO; + bound.y().setValue(vEnd, middle.y().getValue(vEnd)); + } + + /** Returns the ID of this padded cell. */ + public S2CellId id() { + return id; + } + + /** Returns the padding around this cell. */ + public double padding() { + return padding; + } + + /** Returns the level of this cell. */ + public int level() { + return level; + } + + /** Returns the orientation of this cell. */ + public int orientation() { + return orientation; + } + + /** Returns the bound for this cell (including padding.) */ + public R2Rect bound() { + return bound; + } + + /** + * Return the "middle" of the padded cell, defined as the rectangle that belongs to all four + * children. + * + *

        Note that this method is *not* thread-safe, because the return value is computed on demand + * and cached. (It is expected that this class will be mainly useful in the context of single- + * threaded recursive algorithms.) + */ + public R2Rect middle() { + // We compute this field lazily because it is not needed the majority of the time (i.e., for + // cells where the recursion terminates.) + if (middle == null) { + int ijSize = S2CellId.getSizeIJ(level); + double u = PROJ.stToUV(siTiToSt(2L * iLo + ijSize)); + double v = PROJ.stToUV(siTiToSt(2L * jLo + ijSize)); + middle = + new R2Rect( + new R1Interval(u - padding, u + padding), new R1Interval(v - padding, v + padding)); + } + return middle; + } + + /** + * Returns the smallest cell that contains all descendants of this cell whose bounds intersect + * "rect". For algorithms that use recursive subdivision to find the cells that intersect a + * particular object, this method can be used to skip all the initial subdivision steps where only + * one child needs to be expanded. + * + *

        Note that this method is not the same as returning the smallest cell that contains the + * intersection of this cell with "rect". Because of the padding, even if one child completely + * contains "rect" it is still possible that a neighboring child also intersects "rect". + * + *

        Results are undefined if {@link #bound()} does not intersect the given rectangle. + */ + public S2CellId shrinkToFit(R2Rect rect) { + // assert bound().intersects(rect); + + // Quick rejection test: if "rect" contains the center of this cell along either axis, then no + // further shrinking is possible. + int ijSize = S2CellId.getSizeIJ(level); + if (level == 0) { + // Fast path (most calls to this function start with a face cell). + if (rect.x().contains(0) || rect.y().contains(0)) { + return id(); + } + } else { + if (rect.x().contains(PROJ.stToUV(siTiToSt(2L * iLo + ijSize))) + || rect.y().contains(PROJ.stToUV(siTiToSt(2L * jLo + ijSize)))) { + return id(); + } + } + // Otherwise we expand "rect" by the given padding() on all sides and find the range of + // coordinates that it spans along the i- and j-axes. We then compute the highest bit position + // at which the min and max coordinates differ. This corresponds to the first cell level at + // which at least two children intersect "rect". + + // Increase the padding to compensate for the error in uvToST(). + // (The constant below is a provable upper bound on the additional error.) + R2Rect padded = rect.expanded(padding() + 1.5 * S2.DBL_EPSILON); + int iMin = Math.max(iLo, stToIj(PROJ.uvToST(padded.x().lo()))); + int jMin = Math.max(jLo, stToIj(PROJ.uvToST(padded.y().lo()))); + int iMax = Math.min(iLo + ijSize - 1, stToIj(PROJ.uvToST(padded.x().hi()))); + int jMax = Math.min(jLo + ijSize - 1, stToIj(PROJ.uvToST(padded.y().hi()))); + int iXor = iMin ^ iMax; + int jXor = jMin ^ jMax; + + // Compute the highest bit position where the two i- or j-endpoints differ, and then choose the + // cell level that includes both of these endpoints. So if both pairs of endpoints are equal we + // choose MAX_LEVEL; if they differ only at bit 0, we choose (MAX_LEVEL - 1), and so on. + int levelMsb = ((iXor | jXor) << 1) + 1; + int level = S2CellId.MAX_LEVEL - floorLog2(levelMsb); + if (level <= this.level) { + return id(); + } + return S2CellId.fromFaceIJ(id().face(), iMin, jMin).parent(level); + } + + /** Returns the floor of the log2 of x, assuming x is positive. */ + private static final int floorLog2(long x) { + return 63 - Long.numberOfLeadingZeros(x); + } + + /** Returns the center of this cell. */ + public S2Point getCenter() { + int ijSize = S2CellId.getSizeIJ(level); + long si = 2L * iLo + ijSize; + long ti = 2L * jLo + ijSize; + return S2Point.normalize(PROJ.faceSiTiToXyz(id.face(), si, ti)); + } + + /** Returns the vertex where the S2 space-filling curve enters this cell. */ + public S2Point getEntryVertex() { + // The curve enters at the (0,0) vertex unless the axis directions are reversed, in which case + // it enters at the (1,1) vertex. + int i = iLo; + int j = jLo; + if ((orientation & S2.INVERT_MASK) != 0) { + int ijSize = S2CellId.getSizeIJ(level); + i += ijSize; + j += ijSize; + } + return S2Point.normalize(PROJ.faceSiTiToXyz(id.face(), 2L * i, 2L * j)); + } + + /** Returns the vertex where the S2 space-filling curve exits this cell. */ + public S2Point getExitVertex() { + // The curve exits at the (1,0) vertex unless the axes are swapped or inverted but not both, in + // which case it exits at the (0,1) vertex. + int i = iLo; + int j = jLo; + int ijSize = S2CellId.getSizeIJ(level); + if (orientation == 0 || orientation == S2.SWAP_MASK + S2.INVERT_MASK) { + i += ijSize; + } else { + j += ijSize; + } + return S2Point.normalize(PROJ.faceSiTiToXyz(id.face(), 2L * i, 2L * j)); + } +} diff --git a/core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/S2Point.java b/core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/S2Point.java new file mode 100644 index 0000000000..722a727338 --- /dev/null +++ b/core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/S2Point.java @@ -0,0 +1,596 @@ +/* + * Copyright 2006 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.mogo.eagle.core.utilcode.geometry; + +import com.google.common.annotations.GwtCompatible; +import com.google.common.base.Preconditions; +import com.mogo.eagle.core.utilcode.geometry.PrimitiveArrays.Bytes; +import com.mogo.eagle.core.utilcode.geometry.PrimitiveArrays.Cursor; +import com.google.errorprone.annotations.CanIgnoreReturnValue; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.Serializable; +import java.util.AbstractList; +import java.util.List; +import javax.annotation.CheckReturnValue; + +/** + * An S2Point represents a point on the unit sphere as a 3D vector. Usually points are normalized to + * be unit length, but some methods do not require this. + * + */ +@GwtCompatible(serializable = true) +@CheckReturnValue +@SuppressWarnings("AmbiguousMethodReference") +public strictfp class S2Point implements S2Region, Comparable, Serializable { + /** Origin of the coordinate system, [0,0,0]. */ + public static final S2Point ORIGIN = new S2Point(0, 0, 0); + + /** Direction of the x-axis. */ + public static final S2Point X_POS = new S2Point(1, 0, 0); + + /** Opposite direction of the x-axis. */ + public static final S2Point X_NEG = new S2Point(-1, 0, 0); + + /** Direction of the y-axis. */ + public static final S2Point Y_POS = new S2Point(0, 1, 0); + + /** Opposite direction of the y-axis. */ + public static final S2Point Y_NEG = new S2Point(0, -1, 0); + + /** Direction of the z-axis. */ + public static final S2Point Z_POS = new S2Point(0, 0, 1); + + /** Opposite direction of the z-axis. */ + public static final S2Point Z_NEG = new S2Point(0, 0, -1); + + // coordinates of the points + final double x; + final double y; + final double z; + + public S2Point() { + x = y = z = 0; + } + + public S2Point(double x, double y, double z) { + this.x = x; + this.y = y; + this.z = z; + } + + public double getX() { + return x; + } + + public double getY() { + return y; + } + + public double getZ() { + return z; + } + + /** Returns add(this,p). */ + public S2Point add(S2Point p) { + return add(this, p); + } + + /** Returns the component-wise addition of 'p1' and 'p2'. */ + public static final S2Point add(final S2Point p1, final S2Point p2) { + return new S2Point(p1.x + p2.x, p1.y + p2.y, p1.z + p2.z); + } + + /** Returns sub(this,p). */ + public S2Point sub(S2Point p) { + return sub(this, p); + } + + /** Returns the component-wise subtraction of 'p1' and 'p2'. */ + public static final S2Point sub(final S2Point p1, final S2Point p2) { + return new S2Point(p1.x - p2.x, p1.y - p2.y, p1.z - p2.z); + } + + /** Returns sub(this,p). */ + public static final S2Point minus(S2Point p1, S2Point p2) { + return sub(p1, p2); + } + + /** Returns mul(this,scale). */ + public S2Point mul(double scale) { + return S2Point.mul(this, scale); + } + + /** Returns the component-wise multiplication of 'p' with 'm'. */ + public static final S2Point mul(final S2Point p, double m) { + return new S2Point(m * p.x, m * p.y, m * p.z); + } + + /** Returns div(this,scale). */ + public S2Point div(double scale) { + return S2Point.div(this, scale); + } + + /** Returns the component-wise division of 'p' by 'm'. */ + public static final S2Point div(final S2Point p, double m) { + return new S2Point(p.x / m, p.y / m, p.z / m); + } + + /** Returns the vector dot product of 'this' with 'that'. */ + public final double dotProd(S2Point that) { + return this.x * that.x + this.y * that.y + this.z * that.z; + } + + /** Returns crossProd(this,p). */ + public S2Point crossProd(S2Point p) { + return crossProd(this, p); + } + + /** Returns the R3 vector cross product of 'p1' and 'p2'. */ + public static final S2Point crossProd(final S2Point p1, final S2Point p2) { + return new S2Point( + p1.y * p2.z - p1.z * p2.y, p1.z * p2.x - p1.x * p2.z, p1.x * p2.y - p1.y * p2.x); + } + + /** Returns neg(this). */ + public S2Point neg() { + return S2Point.neg(this); + } + + /** Returns the component-wise negation of 'p', i.e. its antipodal point. */ + public static final S2Point neg(S2Point p) { + return new S2Point(-p.x, -p.y, -p.z); + } + + /** Returns fabs(this). */ + public S2Point fabs() { + return S2Point.fabs(this); + } + + /** Returns the component-wise absolute point from 'p'. */ + public static final S2Point fabs(S2Point p) { + return new S2Point(Math.abs(p.x), Math.abs(p.y), Math.abs(p.z)); + } + + /** Returns normalize(this). */ + public S2Point normalize() { + return S2Point.normalize(this); + } + + /** Returns a copy of 'p' rescaled to be unit-length. */ + public static final S2Point normalize(S2Point p) { + double norm = p.norm(); + if (norm != 0) { + norm = 1.0 / norm; + } + return S2Point.mul(p, norm); + } + + /** Returns the vector magnitude {@code sqrt(x*x+y*y+z*z)}. */ + public double norm() { + return Math.sqrt(norm2()); + } + + /** Returns the square of the vector magnitude {@code x*x+y*y+z*z}. */ + public final double norm2() { + return x * x + y * y + z * z; + } + + /** + * Returns the scalar triple product, {@code a.dotProd(b.crossProd(c))}. + * + *

        This is a faster implementation than calling the dotProd and crossProd methods directly. + */ + public static final double scalarTripleProduct(S2Point a, S2Point b, S2Point c) { + double x = b.y * c.z - b.z * c.y; + double y = b.z * c.x - b.x * c.z; + double z = b.x * c.y - b.y * c.x; + double result = a.x * x + a.y * y + a.z * z; + // assert result == a.dotProd(S2Point.crossProd(b, c)); + return result; + } + + /** + * Returns the distance in 3D coordinates from this to that. + * + *

        Equivalent to {@code a.sub(b).norm()}, but significantly faster. + * + *

        If ordering points by angle, this is faster than {@link #norm}, and much faster than {@link + * #angle}, but consider using {@link S1ChordAngle}. + */ + public double getDistance(S2Point that) { + return Math.sqrt(getDistance2(that)); + } + + /** + * Returns the square of the distance in 3D coordinates from this to that. + * + *

        Equivalent to {@code getDistance(that)2}, but significantly faster. + * + *

        If ordering points by angle, this is much faster than {@link #angle}, but consider using + * {@link S1ChordAngle}. + */ + public double getDistance2(S2Point that) { + double dx = this.x - that.x; + double dy = this.y - that.y; + double dz = this.z - that.z; + return dx * dx + dy * dy + dz * dz; + } + + /** return a vector orthogonal to this one */ + public final S2Point ortho() { + switch (largestAbsComponent()) { + case 1: + return crossProd(X_POS).normalize(); + case 2: + return crossProd(Y_POS).normalize(); + default: + return crossProd(Z_POS).normalize(); + } + } + + /** Return the index of the largest component fabs */ + public final int largestAbsComponent() { + return largestAbsComponent(x, y, z); + } + + /** Return the index of the largest component fabs */ + static final int largestAbsComponent(double x, double y, double z) { + final double absX = Math.abs(x); + final double absY = Math.abs(y); + final double absZ = Math.abs(z); + if (absX > absY) { + if (absX > absZ) { + return 0; + } else { + return 2; + } + } else { + if (absY > absZ) { + return 1; + } else { + return 2; + } + } + } + + public final double get(int axis) { + return (axis == 0) ? x : (axis == 1) ? y : z; + } + + /** + * Returns the norm of the cross product, {@code S2Point.crossProd(this, va).norm()}. This is more + * efficient than calling crossProd() followed by norm(). + */ + public final double crossProdNorm(S2Point va) { + double x = this.y * va.z - this.z * va.y; + double y = this.z * va.x - this.x * va.z; + double z = this.x * va.y - this.y * va.x; + double result = Math.sqrt(x * x + y * y + z * z); + // assert result == S2Point.crossProd(this, va).norm(); + return result; + } + + /** + * Rotates this point around an arbitrary axis. The result is normalized. + * + * @param axis point around which rotation should be performed. + * @param radians radians to rotate the point counterclockwise around the given axis. + */ + public S2Point rotate(S2Point axis, double radians) { + S2Point point = normalize(); + S2Point normAxis = axis.normalize(); + S2Point pointOnAxis = normAxis.mul(point.dotProd(normAxis)); + S2Point axisToPoint = point.sub(pointOnAxis); + S2Point axisToPointNormal = normAxis.crossProd(axisToPoint); + axisToPoint = axisToPoint.mul(Math.cos(radians)); + axisToPointNormal = axisToPointNormal.mul(Math.sin(radians)); + // Explicitly normalize the result because there are cases where the accumulated error is + // a bit larger than the tolerance of isUnitLength(). + return axisToPoint.add(axisToPointNormal).add(pointOnAxis).normalize(); + } + + /** Return the angle between two vectors in radians */ + public final double angle(S2Point va) { + return Math.atan2(crossProdNorm(va), dotProd(va)); + } + + /** Compare two vectors, return true if all their components are within a difference of margin. */ + boolean aequal(S2Point that, double margin) { + return (Math.abs(x - that.x) < margin) + && (Math.abs(y - that.y) < margin) + && (Math.abs(z - that.z) < margin); + } + + @Override + public boolean equals(Object that) { + if (!(that instanceof S2Point)) { + return false; + } + S2Point thatPoint = (S2Point) that; + return this.x == thatPoint.x && this.y == thatPoint.y && this.z == thatPoint.z; + } + + /** + * Returns true if this point is equal to {@code that}. Slightly faster than {@link + * #equals(Object)}. + */ + public boolean equalsPoint(S2Point that) { + return this.x == that.x && this.y == that.y && this.z == that.z; + } + + public boolean lessThan(S2Point vb) { + if (x < vb.x) { + return true; + } + if (vb.x < x) { + return false; + } + if (y < vb.y) { + return true; + } + if (vb.y < y) { + return false; + } + if (z < vb.z) { + return true; + } + return false; + } + + // Required for Comparable + @Override + public int compareTo(S2Point other) { + return (lessThan(other) ? -1 : (equalsPoint(other) ? 0 : 1)); + } + + @Override + public String toString() { + return "(" + x + ", " + y + ", " + z + ")"; + } + + public String toDegreesString() { + S2LatLng s2LatLng = new S2LatLng(this); + return "(" + + Double.toString(s2LatLng.latDegrees()) + + ", " + + Double.toString(s2LatLng.lngDegrees()) + + ")"; + } + + /** Returns a new Builder initialized to a copy of this point. */ + public Builder toBuilder() { + return new Builder().add(this); + } + + /** + * Calcualates hashcode based on stored coordinates. Since we want +0.0 and -0.0 to be treated the + * same, we ignore the sign of the coordinates. + */ + @Override + public int hashCode() { + long value = 17; + value += 37 * value + Double.doubleToLongBits(Math.abs(x)); + value += 37 * value + Double.doubleToLongBits(Math.abs(y)); + value += 37 * value + Double.doubleToLongBits(Math.abs(z)); + return (int) (value ^ (value >>> 32)); + } + + // S2Region implementation. + @Override + public boolean contains(S2Cell cell) { + return false; + } + + @Override + public boolean contains(S2Point other) { + return equalsPoint(other); + } + + @Override + public S2Cap getCapBound() { + return S2Cap.fromAxisHeight(this, 0); + } + + @Override + public S2LatLngRect getRectBound() { + S2LatLng latLng = new S2LatLng(this); + return S2LatLngRect.fromPoint(latLng); + } + + @Override + public boolean mayIntersect(S2Cell cell) { + return cell.contains(this); + } + + /** Writes this point to the given output stream. */ + public void encode(OutputStream os) throws IOException { + encode(new LittleEndianOutput(os)); + } + + /** Writes this point to the given little endian output stream. */ + void encode(LittleEndianOutput os) throws IOException { + os.writeDouble(x); + os.writeDouble(y); + os.writeDouble(z); + } + + /** Returns a new S2Point decoded from the given input stream. */ + public static S2Point decode(InputStream is) throws IOException { + return decode(new LittleEndianInput(is)); + } + + /** Returns a new S2Point decoded from the given little endian input stream. */ + static S2Point decode(LittleEndianInput is) throws IOException { + return new S2Point(is.readDouble(), is.readDouble(), is.readDouble()); + } + + /** + * An S2Shape representing a list of S2Points. Each point is represented as a degenerate edge with + * the same starting and ending vertices. + * + *

        This class is useful for adding a collection of points to an S2ShapeIndex. + */ + public abstract static class Shape extends AbstractList + implements S2Shape, Serializable { + private static final long serialVersionUID = 1L; + + public static Shape singleton(final S2Point point) { + return new Shape() { + private static final long serialVersionUID = 1L; + + @Override + public int size() { + return 1; + } + + @Override + public S2Point get(int index) { + if (index != 0) { + throw new IndexOutOfBoundsException(); + } + return point; + } + }; + } + + public static Shape fromList(final List points) { + return new Shape() { + private static final long serialVersionUID = 1L; + + @Override + public int size() { + return points.size(); + } + + @Override + public S2Point get(int index) { + return points.get(index); + } + }; + } + + @Override + public boolean hasInterior() { + return false; + } + + @Override + public boolean containsOrigin() { + return false; + } + + @Override + public int numEdges() { + return size(); + } + + @Override + public void getEdge(int index, MutableEdge result) { + result.a = result.b = get(index); + } + + @Override + public int numChains() { + return size(); + } + + @Override + public int getChainStart(int chainId) { + Preconditions.checkElementIndex(chainId, numChains()); + return chainId; + } + + @Override + public int getChainLength(int chainId) { + Preconditions.checkElementIndex(chainId, numChains()); + return 1; + } + + @Override + public void getChainEdge(int chainId, int offset, MutableEdge result) { + result.a = result.b = getChainVertex(chainId, offset); + } + + @Override + public S2Point getChainVertex(int chainId, int edgeOffset) { + Preconditions.checkElementIndex(edgeOffset, getChainLength(chainId)); + return get(chainId); + } + + @Override + public int dimension() { + return 0; + } + + /** An encoder/decoder of {@link Shape}s. */ + @GwtCompatible + public static class Coder implements S2Coder { + /** + * An instance of {@link Coder} which encodes/decodes {@link Shape}s in the {@code + * FAST} format. + */ + static final Coder FAST = new Coder(S2PointVectorCoder.FAST); + + /** + * An instance of {@link Coder} which encodes/decodes {@link Shape}s in the {@code + * COMPACT} format. + */ + static final Coder COMPACT = new Coder(S2PointVectorCoder.COMPACT); + + private final S2PointVectorCoder coder; + + private Coder(S2PointVectorCoder coder) { + this.coder = coder; + } + + @Override + public void encode(Shape shape, OutputStream output) throws IOException { + coder.encode(shape, output); + } + + @Override + public Shape decode(Bytes data, Cursor cursor) { + return Shape.fromList(coder.decode(data, cursor)); + } + } + } + + /** A builder of {@link S2Point} instances. */ + public static final class Builder { + private double x; + private double y; + private double z; + + /** Constructs a new builder initialized to {@link #ORIGIN}. */ + public Builder() {} + + /** Adds point. */ + @CanIgnoreReturnValue + public Builder add(S2Point point) { + x += point.x; + y += point.y; + z += point.z; + return this; + } + + /** Returns a new {@link S2Point} copied from the current state of this builder. */ + public S2Point build() { + return new S2Point(x, y, z); + } + } +} diff --git a/core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/S2PointCompression.java b/core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/S2PointCompression.java new file mode 100644 index 0000000000..961c8349ff --- /dev/null +++ b/core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/S2PointCompression.java @@ -0,0 +1,345 @@ +/* + * Copyright 2016 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.mogo.eagle.core.utilcode.geometry; + +import com.google.common.annotations.GwtCompatible; +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Preconditions; +import com.google.common.collect.Iterables; +import com.mogo.eagle.core.utilcode.geometry.S2Projections.FaceSiTi; +import com.google.common.primitives.Longs; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; + +@GwtCompatible +public final strictfp class S2PointCompression { + + private S2PointCompression() {} + + private static final int DERIVATIVE_ENCODING_ORDER = 2; + + /** + * Encode a list of points into an efficient, lossless binary representation, which can be decoded + * by calling {@link S2PointCompression#decodePointsCompressed}. The encoding is byte-compatible + * with the C++ version of the S2 library. + * + *

        Points that are snapped to the specified level will require approximately 4 bytes per point, + * while other points will require 24 bytes per point. + * + * @param points The list of points to encode. + * @param level The {@link S2Cell} level at which points should be encoded. + * @param output The output stream into which the encoding should be written. + * @throws IOException if there was a problem writing into the output stream. + */ + public static void encodePointsCompressed(List points, int level, OutputStream output) + throws IOException { + encodePointsCompressed(points, level, new LittleEndianOutput(output)); + } + + static void encodePointsCompressed(List points, int level, LittleEndianOutput encoder) + throws IOException { + // Convert the points to (face, pi, qi) coordinates. + FaceRunCoder faces = new FaceRunCoder(); + int[] verticesPi = new int[points.size()]; + int[] verticesQi = new int[points.size()]; + List offCenter = new ArrayList<>(); + for (int i = 0; i < points.size(); i++) { + FaceSiTi faceSiTi = S2Projections.PROJ.xyzToFaceSiTi(points.get(i)); + faces.addFace(faceSiTi.face); + verticesPi[i] = siTiToPiQi(faceSiTi.si, level); + verticesQi[i] = siTiToPiQi(faceSiTi.ti, level); + if (S2Projections.PROJ.levelIfCenter(faceSiTi, points.get(i)) != level) { + offCenter.add(i); + } + } + + // Encode the runs of the faces. + faces.encode(encoder); + + // Encode the (pi, qi) coordinates of all the points, in order. + NthDerivativeCoder piCoder = new NthDerivativeCoder(DERIVATIVE_ENCODING_ORDER); + NthDerivativeCoder qiCoder = new NthDerivativeCoder(DERIVATIVE_ENCODING_ORDER); + for (int i = 0; i < verticesPi.length; i++) { + int pi = piCoder.encode(verticesPi[i]); + int qi = qiCoder.encode(verticesQi[i]); + if (i == 0) { + // The first point will be just the (pi, qi) coordinates of the S2Point. + // NthDerivativeCoder will not save anything in that case, so we encode in fixed format + // rather than varint to avoid the varint overhead. + + // Interleave to reduce overhead from two partial bytes to one. + long interleavedPiQi = EncodedInts.interleaveBits(pi, qi); + + // Java uses big-endian representation, but the wire format requires little-endian. + // Simultaneously, we only write as many bytes as are actually required for the given level, + // i.e. we wish to truncate the byte representation of the long. + // We do this by reversing the bytes of the value, then truncating the byte array + // representing the result to the required length. + int bytesRequired = (level + 7) / 8 * 2; + byte[] littleEndianInterleavedPiQiBytes = + Arrays.copyOf(Longs.toByteArray(Long.reverseBytes(interleavedPiQi)), bytesRequired); + encoder.writeBytes(littleEndianInterleavedPiQiBytes); + } else { + // ZigZagEncode, as varint requires the maximum number of bytes for negative numbers. + int zigZagEncodedPi = EncodedInts.encodeZigZag32(pi); + int zigZagEncodedQi = EncodedInts.encodeZigZag32(qi); + + // Interleave to reduce overhead from two partial bytes to one. + long interleavedPiQi = EncodedInts.interleaveBits(zigZagEncodedPi, zigZagEncodedQi); + encoder.writeVarint64(interleavedPiQi); + } + } + + // Encode the number of off-center points. + encoder.writeVarint32(offCenter.size()); + + // Encode the actual off-center points. + for (int index : offCenter) { + encoder.writeVarint32(index); + points.get(index).encode(encoder); + } + } + + /** + * Decode a list of points that were encoded using {@link + * S2PointCompression#encodePointsCompressed}. + * + *

        Points that are snapped to the specified level will require approximately 4 bytes per point, + * while other points will require 24 bytes per point. + * + * @param numVertices The number of points to decode. + * @param level The {@link S2Cell} level at which points are encoded. + * @param input The input stream containing the encoded point data. + * @return the list of decoded points. + * @throws IOException if there was a problem reading from the input stream. + */ + public static List decodePointsCompressed(int numVertices, int level, InputStream input) + throws IOException { + return decodePointsCompressed(numVertices, level, new LittleEndianInput(input)); + } + + static List decodePointsCompressed(int numVertices, int level, LittleEndianInput decoder) + throws IOException { + List vertices = new ArrayList<>(numVertices); + + FaceRunCoder faces = new FaceRunCoder(); + faces.decode(numVertices, decoder); + Iterator faceIterator = faces.getFaceIterator(); + + NthDerivativeCoder piCoder = new NthDerivativeCoder(DERIVATIVE_ENCODING_ORDER); + NthDerivativeCoder qiCoder = new NthDerivativeCoder(DERIVATIVE_ENCODING_ORDER); + for (int i = 0; i < numVertices; i++) { + int pi; + int qi; + if (i == 0) { + // The interleaved coordinates are stored in a truncated (depending on level) little-endian + // representation, but we need big-endian for Java, so reconstruct the necessary bytes here. + // Do this by reading in the bytes as-is, padding the end with zeros to get the full 8-byte + // array, converting to a long (still little-endian), and finally reversing the byte + // representation to get big-endian. + int bytesRequired = (level + 7) / 8 * 2; + byte[] littleEndianBytes = decoder.readBytes(bytesRequired); + long interleavedPiQi = + Long.reverseBytes(Longs.fromByteArray(Arrays.copyOf(littleEndianBytes, Longs.BYTES))); + pi = piCoder.decode(EncodedInts.deinterleaveBits1(interleavedPiQi)); + qi = qiCoder.decode(EncodedInts.deinterleaveBits2(interleavedPiQi)); + } else { + long piqi = decoder.readVarint64(); + pi = piCoder.decode(EncodedInts.decodeZigZag32(EncodedInts.deinterleaveBits1(piqi))); + qi = qiCoder.decode(EncodedInts.decodeZigZag32(EncodedInts.deinterleaveBits2(piqi))); + } + int face = faceIterator.next(); + vertices.add(facePiQiToXyz(face, pi, qi, level)); + } + + // Now decode the off-center points. + int numOffCenter = decoder.readVarint32(); + if (numOffCenter > numVertices) { + throw new IOException("Number of off-center points is greater than total amount of points."); + } + for (int i = 0; i < numOffCenter; i++) { + int index = decoder.readVarint32(); + double x = decoder.readDouble(); + double y = decoder.readDouble(); + double z = decoder.readDouble(); + vertices.set(index, new S2Point(x, y, z)); + } + + return vertices; + } + + private static int siTiToPiQi(long si, int level) { + si = Math.min(si, S2Projections.MAX_SITI - 1); + return (int) (si >>> (S2CellId.MAX_LEVEL + 1 - level)); + } + + private static double piQiToST(int pi, int level) { + // We want to recover the position at the center of the cell. If the point + // was snapped to the center of the cell, then modf(s * 2^level) == 0.5. + // Inverting STtoPiQi gives: + // s = (pi + 0.5) / 2^level. + return (pi + 0.5) / (1 << level); + } + + private static S2Point facePiQiToXyz(int face, int pi, int qi, int level) { + return S2Point.normalize( + S2Projections.faceUvToXyz( + face, + S2Projections.PROJ.stToUV(piQiToST(pi, level)), + S2Projections.PROJ.stToUV(piQiToST(qi, level)))); + } + + private static class FaceRunCoder { + private static class FaceRun { + public FaceRun(int face, int count) { + this.face = face; + this.count = count; + } + + public int face; + public int count; + } + + private final List faces = new ArrayList<>(); + + public void addFace(int face) { + FaceRun lastRun = !faces.isEmpty() ? Iterables.getLast(faces) : null; + if (lastRun != null && lastRun.face == face) { + lastRun.count += 1; + } else { + faces.add(new FaceRun(face, 1)); + } + } + + public void encode(LittleEndianOutput encoder) throws IOException { + for (FaceRun run : faces) { + // It isn't necessary to encode the number of faces left for the last run, but since this + // would only help if there were more than 21 faces, it will be a small overall savings, + // much smaller than the bound encoding. + encoder.writeVarint64(S2CellId.NUM_FACES * run.count + run.face); + } + } + + public void decode(int vertices, LittleEndianInput decoder) throws IOException { + int facesParsed = 0; + while (facesParsed < vertices) { + long faceAndCount = decoder.readVarint64(); + FaceRun run = + new FaceRun( + (int) (faceAndCount % S2CellId.NUM_FACES), + (int) (faceAndCount / S2CellId.NUM_FACES)); + faces.add(run); + facesParsed += run.count; + } + } + + public Iterator getFaceIterator() { + final Iterator faceRunIterator = faces.iterator(); + // Special case if there are not faces at all. + if (!faceRunIterator.hasNext()) { + return Collections.emptyIterator(); + } + return new Iterator() { + private FaceRun currentFaceRun = faceRunIterator.next(); + private int usedCountForCurrentFaceRun = 0; + + @Override + public boolean hasNext() { + return usedCountForCurrentFaceRun < currentFaceRun.count || faceRunIterator.hasNext(); + } + + @Override + public void remove() { + throw new UnsupportedOperationException(); + } + + @Override + public Integer next() { + if (usedCountForCurrentFaceRun < currentFaceRun.count) { + usedCountForCurrentFaceRun++; + } else { + usedCountForCurrentFaceRun = 1; + currentFaceRun = faceRunIterator.next(); + } + return currentFaceRun.face; + } + }; + } + } + + @VisibleForTesting + static final class NthDerivativeCoder { + + // The range of supported Ns is [N_MIN, N_MAX]. + public static final int N_MIN = 0; + public static final int N_MAX = 10; + + // The derivative order of the coder (the N in NthDerivative). + private final int n; + + // The derivative order in which to code the next value (ramps up to n). + private int m; + + // Value memory, from oldest to newest. + private final int[] memory; + + public NthDerivativeCoder(int n) { + Preconditions.checkArgument(N_MIN <= n && n <= N_MAX, "Unsupported N: %s", n); + this.n = n; + memory = new int[N_MAX]; + reset(); + } + + public int getN() { + return n; + } + + public int encode(int k) { + for (int i = 0; i < m; i++) { + int delta = k - memory[i]; + memory[i] = k; + k = delta; + } + if (m < n) { + memory[m] = k; + m++; + } + return k; + } + + public int decode(int k) { + if (m < n) { + m++; + } + for (int i = m - 1; i >= 0; i--) { + memory[i] += k; + k = memory[i]; + } + return k; + } + + public void reset() { + Arrays.fill(memory, 0, n, 0); + m = 0; + } + } +} diff --git a/core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/S2PointIndex.java b/core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/S2PointIndex.java new file mode 100644 index 0000000000..03db3a6ca4 --- /dev/null +++ b/core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/S2PointIndex.java @@ -0,0 +1,190 @@ +/* + * Copyright 2015 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.mogo.eagle.core.utilcode.geometry; + +import com.google.common.annotations.GwtCompatible; +import com.google.common.base.Objects; +import com.google.common.collect.Lists; +import com.google.common.primitives.UnsignedLongs; +import java.util.Collections; +import java.util.List; + +/** + * S2PointIndex maintains an index of points sorted by leaf S2CellId. Each point has some associated + * client-supplied data, such as an index or object the point was taken from, useful to map query + * results back to another data structure. + * + *

        The class supports adding or removing points dynamically, and provides a seekable iterator + * interface for navigating the index. + * + *

        You can use this class in conjunction with {@link S2ClosestPointQuery} to find the closest + * index points to a given query point. For example: + * + *

        + * void test(List points, S2Point target) {
        + *   // The generic type allows auxiliary data to be attached to each point
        + *   // In this case, attach the original index of the point.
        + *   S2PointIndex index = new S2PointIndex();
        + *   for (int i = 0; i < points.size(); i++) {
        + *     index.add(points.get(i), i);
        + *   }
        + *   S2ClosestPointQuery query = new S2ClosestPointQuery<>(index);
        + *   query.findClosestPoint(target);
        + *   if (query.num_points() > 0) {
        + *     // query.point(0) is the closest point (result 0).
        + *     // query.distance(0) is the distance to the target.
        + *     // query.data(0) is the auxiliary data (the array index set above).
        + *     doSomething(query.point(0), query.data(0), query.distance(0));
        + *   }
        + * }
        + * 
        + * + *

        Alternatively, you can access the index directly using the iterator interface. For example, + * here is how to iterate through all the points in a given S2CellId "targetId": + * + *

        + * S2Iterator> it = index.iterator();
        + * it.seek(targetId.rangeMin());
        + * for (; !it.done() && it.compareTo(targetId.rangeMax()) <= 0; it.next()) {
        + *   doSomething(it.entry());
        + * }
        + * 
        + * + *

        Points can be added or removed from the index at any time by calling add() or remove(), but + * doing so invalidates existing iterators. New iterators must be created. + * + *

        This class is not thread-safe. + */ +// TODO(user): Make this a subtype of S2Region, so that it can also be used to efficiently compute +// coverings of a collection of S2Points. +@GwtCompatible +public final class S2PointIndex { + private final List> entries = Lists.newArrayList(); + private boolean sorted = true; + + /** Returns the number of points in the index. */ + public int numPoints() { + return entries.size(); + } + + /** + * Returns a new iterator over the cells of this index, after sorting entries by cell ID if any + * modifications have been made since the last iterator was created. + */ + public S2Iterator> iterator() { + if (!sorted) { + Collections.sort(entries); + sorted = true; + } + return S2Iterator.create(entries); + } + + /** As {@link #add(Entry)}, but more convenient. */ + public void add(S2Point point, Data data) { + add(createEntry(point, data)); + } + + /** Adds a new entry to the index. Invalidates all iterators; clients must create new ones. */ + public void add(Entry entry) { + sorted = false; + entries.add(entry); + } + + /** As {@link #remove(Entry)}, but more convenient. */ + public boolean remove(S2Point point, Data data) { + return remove(createEntry(point, data)); + } + + /** + * Removes the given entry from the index, and returns whether the given entry was present and + * removed. Both the "point" and "data" fields must match the point to be removed. Invalidates all + * iterators; clients must create new ones. + */ + public boolean remove(Entry entry) { + return entries.remove(entry); + } + + /** + * Resets the index to its original empty state. Invalidates all iterators; clients must create + * new ones. + */ + public void reset() { + sorted = true; + entries.clear(); + } + + /** Convenience method to create an index entry from the given point and data value. */ + public static Entry createEntry(S2Point point, Data data) { + return new Entry<>(S2CellId.fromPoint(point), point, data); + } + + /** + * An S2Iterator-compatible pair of S2Point with associated client data of a given type. + * + *

        Equality and hashing are based on the point and data value. The natural order of this type + * is by the leaf cell that contains the point, which is not consistent with + * equals. + */ + public static class Entry implements S2Iterator.Entry, Comparable> { + private final long id; + private final S2Point point; + private final Data data; + + private Entry(S2CellId cellId, S2Point point, Data data) { + this.id = cellId.id(); + this.point = point; + this.data = data; + } + + @Override + public long id() { + return id; + } + + public S2Point point() { + return point; + } + + public Data data() { + return data; + } + + @Override + public boolean equals(Object other) { + if (other instanceof Entry) { + Entry e = (Entry) other; + return point.equalsPoint(e.point) && Objects.equal(data, e.data); + } else { + return false; + } + } + + @Override + public int hashCode() { + return point.hashCode() * 31 + (data == null ? 0 : data.hashCode()); + } + + @Override + public int compareTo(Entry other) { + return UnsignedLongs.compare(id, other.id); + } + + @Override + public String toString() { + return new S2LatLng(point) + ": " + data; + } + } +} diff --git a/core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/S2PointRegion.java b/core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/S2PointRegion.java new file mode 100644 index 0000000000..ed531fbcf3 --- /dev/null +++ b/core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/S2PointRegion.java @@ -0,0 +1,152 @@ +/* + * Copyright 2017 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.mogo.eagle.core.utilcode.geometry; + +import com.google.common.annotations.GwtCompatible; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.Serializable; + +/** + * An S2PointRegion is a region that contains a single point. It is more expensive than the raw + * S2Point type and is useful mainly for completeness. + */ +@GwtCompatible(serializable = true) +public final strictfp class S2PointRegion + implements S2Region, Comparable, Serializable { + /** The byte in a stream that signifies the lossless encoding of an S2PointRegion follows. */ + private static final byte POINT_REGION_LOSSLESS_ENCODING_VERSION = 1; + + private final S2Point point; + + public S2PointRegion() { + this.point = S2Point.ORIGIN; + } + + public S2PointRegion(double x, double y, double z) { + this.point = new S2Point(x, y, z); + } + + public S2PointRegion(S2Point point) { + this.point = point; + } + + public S2Point getPoint() { + return point; + } + + public double getX() { + return point.getX(); + } + + public double getY() { + return point.getY(); + } + + public double getZ() { + return point.getZ(); + } + + @Override + public boolean equals(Object that) { + if (!(that instanceof S2PointRegion)) { + return false; + } + S2PointRegion thatPointRegion = (S2PointRegion) that; + return getPoint().equalsPoint(thatPointRegion.getPoint()); + } + + public boolean lessThan(S2PointRegion vb) { + return getPoint().lessThan(vb.getPoint()); + } + + // Required for Comparable + @Override + public int compareTo(S2PointRegion other) { + return (lessThan(other) ? -1 : (equals(other) ? 0 : 1)); + } + + @Override + public String toString() { + return point.toString(); + } + + public String toDegreesString() { + return point.toDegreesString(); + } + + /** + * Calcualates hashcode based on stored coordinates. Since we want +0.0 and -0.0 to be treated the + * same, we ignore the sign of the coordinates. + */ + @Override + public int hashCode() { + return point.hashCode(); + } + + // S2Region implementation + @Override + public boolean contains(S2Cell cell) { + return false; + } + + @Override + public boolean contains(S2Point p) { + return getPoint().contains(p); + } + + @Override + public S2Cap getCapBound() { + return S2Cap.fromAxisHeight(getPoint(), 0); + } + + @Override + public S2LatLngRect getRectBound() { + S2LatLng latLng = new S2LatLng(getPoint()); + return S2LatLngRect.fromPoint(latLng); + } + + @Override + public boolean mayIntersect(S2Cell cell) { + return cell.contains(getPoint()); + } + + /** Writes this point region to the given output stream. */ + public void encode(OutputStream os) throws IOException { + encode(new LittleEndianOutput(os)); + } + + /** Writes this point region to the given little endian output stream. */ + void encode(LittleEndianOutput os) throws IOException { + os.writeByte(POINT_REGION_LOSSLESS_ENCODING_VERSION); + point.encode(os); + } + + /** Returns a new S2PointRegion decoded from the given input stream. */ + public static S2PointRegion decode(InputStream is) throws IOException { + return decode(new LittleEndianInput(is)); + } + + /** Returns a new S2PointRegion decoded from the given little endian input stream. */ + static S2PointRegion decode(LittleEndianInput is) throws IOException { + byte version = is.readByte(); + if (version != POINT_REGION_LOSSLESS_ENCODING_VERSION) { + throw new IOException("Unsupported S2PointRegion encoding version " + version); + } + return new S2PointRegion(S2Point.decode(is)); + } +} diff --git a/core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/S2PointVectorCoder.java b/core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/S2PointVectorCoder.java new file mode 100644 index 0000000000..eb0d736ee7 --- /dev/null +++ b/core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/S2PointVectorCoder.java @@ -0,0 +1,856 @@ +/* + * Copyright 2019 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.mogo.eagle.core.utilcode.geometry; + +import com.google.common.annotations.GwtCompatible; +import com.google.common.base.Preconditions; +import com.google.common.collect.Lists; +import com.mogo.eagle.core.utilcode.geometry.PrimitiveArrays.Bytes; +import com.mogo.eagle.core.utilcode.geometry.PrimitiveArrays.Cursor; +import com.mogo.eagle.core.utilcode.geometry.PrimitiveArrays.Longs; +import com.mogo.eagle.core.utilcode.geometry.S2Projections.FaceSiTi; +import com.google.common.primitives.Doubles; +import com.google.common.primitives.ImmutableLongArray; +import com.google.common.primitives.Ints; +import com.google.common.primitives.UnsignedInts; +import com.google.common.primitives.UnsignedLongs; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.util.AbstractList; +import java.util.ArrayList; +import java.util.List; + +/** + * An encoder/decoder of {@link List}s. + * + *

        Values from the {@link List} returned by {@link #decode(Bytes, Cursor)} are decoded + * only when they are accessed. This allows for very fast initialization and no additional memory + * use beyond the encoded data. The encoded data is not owned by the {@code List}; typically it + * points into a large contiguous buffer that contains other encoded data as well. + */ +@GwtCompatible +class S2PointVectorCoder implements S2Coder> { + /** + * An instance of a {@code S2PointVectorCoder} which encodes/decodes {@link S2Point}s in the FAST + * encoding format. The FAST format is optimized for fast encoding/decoding. + */ + static final S2PointVectorCoder FAST = new S2PointVectorCoder(Format.FAST); + + /** + * An instance of a {@code S2PointVectorCoder} which encodes/decodes {@link S2Point}s in the + * COMPACT encoding format. The COMPACT format is optimized for disk usage and memory footprint. + */ + static final S2PointVectorCoder COMPACT = new S2PointVectorCoder(Format.COMPACT); + + /** + * Controls whether to optimize for speed or size when encoding points. (Note that encoding is + * always lossless, and that currently compact encodings are only possible when points have been + * snapped to S2CellId centers). + */ + private enum Format { + FAST, + COMPACT + } + + private final Format type; + + /** The value of the FAST encoding format. */ + private static final int FORMAT_FAST = 0; + /** The value of the COMPACT encoding format. */ + private static final int FORMAT_COMPACT = 1; + + // To save space (especially for vectors of length 0, 1, and 2), the encoding format is encoded in + // the low-order 3 bits of the vector size. Up to 7 encoding formats are supported (only 2 are + // currently defined). Additional formats could be supported by using "7" as an overflow indicator + // and encoding the actual format separately, but it seems unlikely we will ever need to do that. + private static final int ENCODING_FORMAT_BITS = 3; + private static final byte ENCODING_FORMAT_MASK = (1 << ENCODING_FORMAT_BITS) - 1; + + /** The size of an encoded {@link S2Point} in bytes (3 doubles * 8 bytes per double). */ + private static final int SIZEOF_S2POINT = 3 * 8; + + /** The left shift factor for {@link #BLOCK_SIZE}. */ + private static final int BLOCK_SHIFT = 4; + + /** + * {@link S2CellId}s are represented in a special 64-bit format and are encoded in fixed-size + * blocks. {@code BLOCK_SIZE} represents the number of values per block. Block sizes of 4, 8, 16, + * and 32 were tested, and {@code BLOCK_SIZE} == 16 seems to offer the best compression. (Note + * that {@code BLOCK_SIZE == 32} requires some code modifications which have since been removed). + */ + private static final int BLOCK_SIZE = 1 << BLOCK_SHIFT; + + /** The exception value in the COMPACT encoding format. */ + private static final long EXCEPTION = S2CellId.sentinel().id(); + + private S2PointVectorCoder(Format type) { + this.type = type; + } + + @Override + public void encode(List values, OutputStream output) throws IOException { + switch (type) { + case FAST: + encodeFast(values, output); + break; + case COMPACT: + encodeCompact(values, output); + break; + } + } + + @Override + public List decode(Bytes data, Cursor cursor) { + // Peek at the format but don't advance the decoder. + int format = data.get(cursor.position) & ENCODING_FORMAT_MASK; + + switch (format) { + case FORMAT_FAST: + return decodeFast(data, cursor); + case FORMAT_COMPACT: + return decodeCompact(data, cursor); + default: + throw new IllegalArgumentException("Unexpected format: " + format); + } + } + + private static void encodeFast(List values, OutputStream output) throws IOException { + // The encoding format is as follows: + // + // varint64, bits 0-2: encoding format (UNCOMPRESSED) + // bits 3-63: vector size + // array of values.size() S2Points in little-endian order + + long sizeFormat = (((long) values.size()) << ENCODING_FORMAT_BITS) | FORMAT_FAST; + EncodedInts.writeVarint64(output, sizeFormat); + for (S2Point point : values) { + point.encode(output); + } + } + + private static List decodeFast(Bytes data, Cursor cursor) { + long tmpSize = data.readVarint64(cursor); + tmpSize >>= ENCODING_FORMAT_BITS; + int size = Ints.checkedCast(tmpSize); + long offset = cursor.position; + cursor.position += size * SIZEOF_S2POINT; + + return new AbstractList() { + @Override + public S2Point get(int index) { + long position = offset + index * SIZEOF_S2POINT; + return new S2Point( + data.readLittleEndianDouble(position), + data.readLittleEndianDouble(position + Doubles.BYTES), + data.readLittleEndianDouble(position + Doubles.BYTES * 2)); + } + + @Override + public int size() { + return size; + } + }; + } + + /** + * Encodes a vector of {@link S2Point}s, optimizing for space. + * + *

        OVERVIEW + * + *

        We attempt to represent each S2Point as the center of an S2CellId. All S2CellIds must be at + * the same level. Any points that cannot be encoded exactly as S2CellId centers are stored as + * exceptions using 24 bytes each. If there are so many exceptions that the COMPACT encoding does + * not save significant space, we give up and use the uncompressed encoding. + * + *

        The first step is to choose the best S2CellId level. This requires converting each point to + * (face, si, ti) coordinates and checking whether the point can be represented exactly as an + * S2CellId center at some level. We then build a histogram of S2CellId levels (just like the + * similar code in S2Polygon.encode) and choose the best level (or give up, if there are not + * enough S2CellId-encodable points). + * + *

        The simplest approach would then be to take all the S2CellIds and right-shift them to remove + * all the constant bits at the chosen level. This would give the best spatial locality and hence + * the smallest deltas. However instead we give up some spatial locality and use the similar but + * faster transformation described below. + * + *

        Each encodable point is first converted to the (sj, tj) representation defined below: + * + *

        {@code
        +   * sj = (((face & 3) << 30) | (si >> 1)) >> (30 - level);
        +   * tj = (((face & 4) << 29) | ti) >> (31 - level);
        +   * }
        + * + * These two values encode the (face, si, ti) tuple using (2 * level + 3) bits. To see this, + * recall that "si" and "ti" are 31-bit values that all share a common suffix consisting of a "1" + * bit followed by (30 - level) "0" bits. The code above right-shifts these values to remove the + * constant bits and then prepends the bits for "face", yielding a total of (level + 2) bits for + * "sj" and (level + 1) bits for "tj". + * + *

        We then combine (sj, tj) into one 64-bit value by interleaving bit pairs: + * + *

        {@code
        +   * v = interleaveBitPairs(sj, tj);
        +   * }
        + * + * (We could also interleave individual bits, but it is faster this way). The result is similar to + * right-shifting an S2CellId by (61 - 2 * level), except that it is faster to decode and the + * spatial locality is not quite as good. + * + *

        The 64-bit values are divided into blocks of size 8, and then each value is encoded as the + * sum of a base value, a per-block offset, and a per-value delta within that block: + * + *

        {@code
        +   * v[i,j] = base + offset[i] + delta[i, j]
        +   * }
        + * + *

        where "i" represents a block and "j" represents an entry in that block. + * + *

        The deltas for each block are encoded using a fixed number of 4-bit nibbles (1-16 nibbles + * per delta). This allows any delta to be accessed in constant time. + * + *

        The "offset" for each block is a 64-bit value encoded in 0-8 bytes. The offset is + * left-shifted such that it overlaps the deltas by a configurable number of bits (either 0 or 4), + * called the "overlap". The overlap and offset length (0-8 bytes) are specified per block. The + * reason for the overlap is that it allows fewer delta bits to be used in some cases. For example + * if base == 0 and the range within a block is 0xf0 to 0x110, then rather than using 12-bits + * deltas with an offset of 0, the overlap lets us use 8-bits deltas with an offset of 0xf0 + * (saving 7 bytes per block). + * + *

        The global minimum value "base" is encoded using 0-7 bytes starting with the + * most-significant non-zero bit possible for the chosen level. For example, if (level == 7) then + * the encoded values have at most 17 bits, so if "base" is encoded in 1 byte then it is shifted + * to occupy bits 9-16. + * + *

        Example: at level == 15, there are at most 33 non-zero value bits. The following shows the + * bit positions covered by "base", "offset", and "delta" assuming that "base" and "offset" are + * encoded in 2 bytes each, deltas are encoded in 2 nibbles (1 byte) each, and "overlap" is 4 + * bits: + * + *

        {@code
        +   * Base:             1111111100000000-----------------
        +   * Offset:           -------------1111111100000000----
        +   * Delta:            -------------------------00000000
        +   * Overlap:                                   ^^^^
        +   * }
        + * + *

        The numbers (0 or 1) in this diagram denote the byte number of the encoded value. Notice + * that "base" is shifted so that it starts at the leftmost possible bit, "delta" always starts at + * the rightmost possible bit (bit 0), and "offset" is shifted so that it overlaps "delta" by the + * chosen "overlap" (either 0 or 4 bits). Also note that all of these values are summed, and + * therefore each value can affect higher-order bits due to carries. + * + *

        NOTE(user): Encoding deltas in 4-bit rather than 8-bit length increments reduces encoded + * sizes by about 7%. Allowing a 4-bit overlap between the offset and deltas reduces encoded sizes + * by about 1%. Both optimizations make the code more complex but don't affect running times + * significantly. + * + *

        ENCODING DETAILS
        + * + *

        Now we can move on to the actual encodings. First, there is a 2 byte header encoded as + * follows: + * + *

        {@code
        +   * Byte 0, bits 0-2: encodingFormat (COMPACT)
        +   * Byte 0, bit  3:   haveExceptions
        +   * Byte 0, bits 4-7: (lastBlockSize - 1)
        +   * Byte 1, bits 0-2: baseBytes
        +   * Byte 1, bits 3-7: level (0-30)
        +   * }
        + * + *

        This is followed by an EncodedStringVector containing the encoded blocks. Each block + * contains BLOCK_SIZE (8) values. The total size of the EncodedS2PointVector is not stored + * explicitly, but instead is calculated as + * + *

        +   * num_values == BLOCK_SIZE * (numBlocks - 1) + lastBlockSize
        +   * 
        + * + *

        (An empty vector has numBlocks == 0 and lastBlockSize == BLOCK_SIZE.) + * + *

        Each block starts with a 1 byte header containing the following: + * + *

        {@code
        +   * Byte 0, bits 0-2: (offsetBytes - overlapNibbles)
        +   * Byte 0, bit  3:   overlapNibbles
        +   * Byte 0, bits 4-7: (deltaNibbles - 1)
        +   * }
        + * + *

        "overlapNibbles" is either 0 or 1 (indicating an overlap of 0 or 4 bits), while + * "offsetBytes" is in the range 0-8 (indicating the number of bytes used to encode the offset for + * this block). Note that some combinations cannot be encoded: in particular, offsetBytes == 0 can + * only be encoded with an overlap of 0 bits, and offsetBytes == 8 can only be encoded with an + * overlap of 4 bits. This allows us to encode offset lengths of 0-8 rather than just 0-7 without + * using an extra bit. (Note that the combinations that can't be encoded are not useful anyway). + * + *

        The header is followed by "offsetBytes" bytes for the offset, and then (4 * deltaNibbles) + * bytes for the deltas. + * + *

        If there are any points that could not be represented as S2CellIds, then "haveExceptions" in + * the header is true. In that case the delta values within each block are encoded as (delta + 8), + * and values 0-7 are used to represent exceptions. If a block has exceptions, they are encoded + * immediately following the array of deltas, and are referenced by encoding the corresponding + * exception index (0-7) as the delta. + * + *

        TODO(user): A vector containing a single leaf cell is currently encoded as 13 bytes (2 byte + * header, 7 byte base, 1 byte block count, 1 byte block length, 1 byte block header, 1 byte + * delta). However if this case occurs often, a better solution would be implement a separate + * format that encodes the leading k bytes of an S2CellId. It would have a one-byte header + * consisting of the encoding format (3 bits) and the number of bytes encoded (3 bits), followed + * by the S2CellId bytes. The extra 2 header bits could be used to store single points using other + * encodings, e.g. E7. + * + *

        If we wind up using 8-value blocks, we could also use the extra bit in the first byte of the + * header to indicate that there is only one value, and then skip the 2nd byte of header and the + * EncodedStringVector. But this would be messy because it also requires special cases while + * decoding. Essentially this would be a sub-format within the COMPACT format. + */ + private static void encodeCompact(List values, OutputStream output) throws IOException { + // 1. Compute (level, face, si, ti) for each point, build a histogram of levels, and determine + // the optimal level to use for encoding (if any). + List cellPoints = Lists.newArrayListWithCapacity(values.size()); + int level = chooseBestLevel(values, cellPoints); + if (level < 0) { + encodeFast(values, output); + return; + } + + // 2. Convert the points into encodable 64-bit values. We don't use the S2CellId itself + // because it requires a somewhat more complicated bit interleaving operation. + // + // TODO(user): Benchmark using shifted S2CellIds instead. + ImmutableLongArray cellPointValues = convertCellsToValues(cellPoints, level); + boolean haveExceptions = cellPointValues.contains(EXCEPTION); + + // 3. Choose the global encoding parameter "base" (consisting of the bit prefix shared by all + // values to be encoded). + Base base = chooseBase(cellPointValues, level, haveExceptions); + + // Now encode the output, starting with the 2-byte header (see above). + int numBlocks = (cellPointValues.length() + BLOCK_SIZE - 1) >> BLOCK_SHIFT; + int baseBytes = base.baseBits >> 3; + int lastBlockCount = cellPointValues.length() - BLOCK_SIZE * (numBlocks - 1); + Preconditions.checkArgument(lastBlockCount >= 0); + Preconditions.checkArgument(lastBlockCount <= BLOCK_SIZE); + Preconditions.checkArgument(baseBytes <= 7); + Preconditions.checkArgument(level <= 30); + output.write(FORMAT_COMPACT | ((haveExceptions ? 1 : 0) << 3) | ((lastBlockCount - 1) << 4)); + output.write(baseBytes | (level << 3)); + + // Next we encode 0-7 bytes of "base". + int baseShift = baseShift(level, base.baseBits); + EncodedInts.encodeUintWithLength(output, base.base >> baseShift, baseBytes); + + // Now we encode the contents of each block. + List blocks = new ArrayList<>(); + List exceptions = new ArrayList<>(); + MutableBlockCode code = new MutableBlockCode(); + for (int i = 0; i < cellPointValues.length(); i += BLOCK_SIZE) { + int blockSize = Math.min(BLOCK_SIZE, cellPointValues.length() - i); + getBlockCode(code, cellPointValues.subArray(i, i + blockSize), base.base, haveExceptions); + + // Encode the one-byte block header (see above). + + ByteArrayOutput block = new ByteArrayOutput(); + int offsetBytes = code.offsetBits >> 3; + int deltaNibbles = code.deltaBits >> 2; + int overlapNibbles = code.overlapBits >> 2; + Preconditions.checkArgument((offsetBytes - overlapNibbles) <= 7); + Preconditions.checkArgument(overlapNibbles <= 1); + Preconditions.checkArgument(deltaNibbles <= 16); + block.write((offsetBytes - overlapNibbles) | (overlapNibbles << 3) | (deltaNibbles - 1) << 4); + + // Determine the offset for this block, and whether there are exceptions. + long offset = -1L; + int numExceptions = 0; + for (int j = 0; j < blockSize; j++) { + if (cellPointValues.get(i + j) == EXCEPTION) { + numExceptions += 1; + } else { + Preconditions.checkArgument(cellPointValues.get(i + j) >= base.base); + offset = UnsignedLongs.min(offset, cellPointValues.get(i + j) - base.base); + } + } + if (numExceptions == blockSize) { + offset = 0; + } + + // Encode the offset. + int offsetShift = code.deltaBits - code.overlapBits; + offset &= ~bitMask(offsetShift); + Preconditions.checkArgument((offset == 0) == (offsetBytes == 0)); + if (offset > 0) { + EncodedInts.encodeUintWithLength(block, offset >>> offsetShift, offsetBytes); + } + + // Encode the deltas, and also gather any exceptions present. + int deltaBytes = (deltaNibbles + 1) >> 1; + exceptions.clear(); + for (int j = 0; j < blockSize; j++) { + long delta; + if (cellPointValues.get(i + j) == EXCEPTION) { + delta = exceptions.size(); + exceptions.add(values.get(i + j)); + } else { + Preconditions.checkArgument( + UnsignedLongs.compare(cellPointValues.get(i + j), offset + base.base) >= 0); + delta = cellPointValues.get(i + j) - (offset + base.base); + if (haveExceptions) { + Preconditions.checkArgument(UnsignedLongs.compare(delta, -1L - BLOCK_SIZE) <= 0); + delta += BLOCK_SIZE; + } + } + Preconditions.checkArgument(UnsignedLongs.compare(delta, bitMask(code.deltaBits)) <= 0); + if (((deltaNibbles & 1) != 0) && ((j & 1) != 0)) { + // Combine this delta with the high-order 4 bits of the previous delta. + int lastByte = block.removeLast(); + delta = (delta << 4) | (lastByte & 0xf); + } + EncodedInts.encodeUintWithLength(block, delta, deltaBytes); + } + // Append any exceptions to the end of the block. + if (numExceptions > 0) { + for (S2Point p : exceptions) { + p.encode(block); + } + } + blocks.add(block.toByteArray()); + } + VectorCoder.BYTE_ARRAY.encode(blocks, output); + } + + private static List decodeCompact(Bytes data, Cursor cursor) { + // First we decode the two-byte header: + // Byte 0, bits 0-2: encodingFormat (COMPACT) + // Byte 0, bit 3: haveExceptions + // Byte 0, bits 4-7: (lastBlockSize - 1) + // Byte 1, bits 0-2: baseBytes + // Byte 1, bits 3-7: level (0-30) + int header1 = data.get(cursor.position++) & 0xFF; + int header2 = data.get(cursor.position++) & 0xFF; + Preconditions.checkArgument((header1 & 7) == FORMAT_COMPACT); + int lastBlockCount; + int baseBytes; + + boolean haveExceptions = (header1 & 8) != 0; + lastBlockCount = (header1 >> 4) + 1; + baseBytes = header2 & 7; + int level = header2 >> 3; + + // Decode the base value (if any). + long tmpBase = data.readUintWithLength(cursor, baseBytes); + long base = tmpBase << baseShift(level, baseBytes << 3); + + // Initialize the vector of encoded blocks. + Longs blockOffsets = UintVectorCoder.UINT64.decode(data, cursor); + long offset = cursor.position; + + int size = BLOCK_SIZE * (blockOffsets.length() - 1) + lastBlockCount; + cursor.position += blockOffsets.length() > 0 ? blockOffsets.get(blockOffsets.length() - 1) : 0; + + return new AbstractList() { + @Override + public S2Point get(int index) { + // First we decode the block header. + int iShifted = index >> BLOCK_SHIFT; + long position = offset + ((iShifted == 0) ? 0 : blockOffsets.get(iShifted - 1)); + int header = data.get(position++) & 0xFF; + int overlapNibbles = (header >> 3) & 1; + int offsetBytes = (header & 7) + overlapNibbles; + int deltaNibbles = (header >> 4) + 1; + + // Decode the offset for this block. + int offsetShift = (deltaNibbles - overlapNibbles) << 2; + long offset; + long delta; + try { + offset = data.readUintWithLength(data.cursor(position), offsetBytes) << offsetShift; + position += offsetBytes; + long exceptionPosition = position; + + // Decode the delta for the requested value. + int deltaNibbleOffset = (index & (BLOCK_SIZE - 1)) * deltaNibbles; + int deltaBytes = (deltaNibbles + 1) >> 1; + position += deltaNibbleOffset >> 1; + delta = data.readUintWithLength(data.cursor(position), deltaBytes); + delta >>>= (deltaNibbleOffset & 1) << 2; + delta &= bitMask(deltaNibbles << 2); + + // Test whether this point is encoded as an exception. + if (haveExceptions) { + if (delta < BLOCK_SIZE) { + int blockSize = Math.min(BLOCK_SIZE, size - (index & -BLOCK_SIZE)); + exceptionPosition += (blockSize * deltaNibbles + 1) >> 1; + exceptionPosition += delta * SIZEOF_S2POINT; + return S2Point.decode(data.toInputStream(exceptionPosition)); + } + delta -= BLOCK_SIZE; + } + } catch (IOException e) { + // This should never happen because Bytes.get() does not throw an IOException. + throw new RuntimeException(e); + } + + // Otherwise convert the 64-bit value back to an S2Point. + long value = base + offset + delta; + int shift = S2CellId.MAX_LEVEL - level; + + // The S2CellId version of the following code is: + // return S2CellId(((value << 1) | 1) << (2 * shift)).ToPoint(); + int sj = EncodedInts.deinterleaveBitPairs1(value); + int tj = EncodedInts.deinterleaveBitPairs2(value); + int si = (((sj << 1) | 1) << shift) & 0x7fffffff; + int ti = (((tj << 1) | 1) << shift) & 0x7fffffff; + int face = ((sj << shift) >>> 30) | (((tj << (shift + 1)) >>> 29) & 4); + return S2Projections.faceUvToXyz( + face, + S2Projections.PROJ.stToUV(S2Projections.siTiToSt(si)), + S2Projections.PROJ.stToUV(S2Projections.siTiToSt(ti))) + .normalize(); + } + + @Override + public int size() { + return size; + } + }; + } + + /** + * Represents the encoding parameters to be used for a given block (consisting of {@link + * #BLOCK_SIZE} encodable 64-bit values). + */ + private static final class MutableBlockCode { + /** Delta length in bits (multiple of 4). */ + int deltaBits; + /** Offset length in bits (multiple of 8). */ + int offsetBits; + /** {Delta, Offset} overlap in bits (0 or 4). */ + int overlapBits; + + MutableBlockCode() {} + + public void set(int deltaBits, int offsetBits, int overlapBits) { + this.deltaBits = deltaBits; + this.offsetBits = offsetBits; + this.overlapBits = overlapBits; + } + } + + /** Return type of {@link #chooseBase}. */ + private static final class Base { + long base; + int baseBits; + + Base(long base, int baseBits) { + this.base = base; + this.baseBits = baseBits; + } + } + + /** + * Represents a point that can be encoded as an {@link S2CellId} center. + * + *

        (If such an encoding is not possible then level < 0). + */ + private static final class CellPoint { + short level; + short face; + int si; + int ti; + + CellPoint(int level, FaceSiTi faceSiTi) { + this.level = (short) level; + // TODO(user): UnsignedBytes is marked GwtIncompatible. Use that here when we can. + Preconditions.checkArgument(faceSiTi.face >> Byte.SIZE == 0); + this.face = (byte) faceSiTi.face; + this.si = UnsignedInts.checkedCast(faceSiTi.si); + this.ti = UnsignedInts.checkedCast(faceSiTi.ti); + } + } + + /** + * A thin wrapper over {@link ByteArrayOutputStream} which allows the last written byte to be + * removed. + */ + private static class ByteArrayOutput extends ByteArrayOutputStream { + /** Removes and returns the last written byte. */ + int removeLast() { + Preconditions.checkState(count > 0); + return buf[--count]; + } + } + + /** Returns a bit mask with {@code n} low-order 1 bits, for {@code 0 <= n <= 64}. */ + private static long bitMask(int n) { + return (n == 0) ? 0 : (-1L >>> (64 - n)); + } + + /** Returns the maximum number of bits per value at the given {@link S2CellId} level. */ + private static int maxBitsForLevel(int level) { + return 2 * level + 3; + } + + /** + * Returns the number of bits that {@code base} should be right-shifted in order to encode only + * its leading {@code baseBits} bits, assuming that all points are encoded at the given {@link + * S2CellId} level. + */ + private static int baseShift(int level, int baseBits) { + return Math.max(0, maxBitsForLevel(level) - baseBits); + } + + /** + * Returns the {@code S2CellId} level for which the greatest number of the given points can be + * represented as the center of an {@code S2CellId}, or -1 if there is no S2CellId that would + * result in significant space savings. + * + *

        Adds the {@code S2CellId} representation of each point (if any) to {@code cellPoints}. + */ + private static int chooseBestLevel(List points, List cellPoints) { + // Count the number of points at each level. + int[] levelCounts = new int[S2CellId.MAX_LEVEL + 1]; + for (S2Point point : points) { + FaceSiTi faceSiTi = S2Projections.PROJ.xyzToFaceSiTi(point); + int level = S2Projections.PROJ.levelIfCenter(faceSiTi, point); + cellPoints.add(new CellPoint(level, faceSiTi)); + if (level >= 0) { + levelCounts[level]++; + } + } + // Choose the level for which the most points can be encoded. + int bestLevel = 0; + for (int level = 1; level <= S2CellId.MAX_LEVEL; level++) { + if (levelCounts[level] > levelCounts[bestLevel]) { + bestLevel = level; + } + } + // The uncompressed encoding is smaller *and* faster when very few of the points are encodable + // as S2CellIds. The COMPACT encoding uses about 1 extra byte per point in this case, + // consisting of up to a 3 byte EncodedArray offset for each block, a 1 byte block header, and + // 4 bits per delta (encoding an exception number from 0-7), for a total of 8 bytes per block. + // This represents a space overhead of about 4%, so we require that at least 5% of the input + // points should be encodable as S2CellIds in order for the COMPACT format to be worthwhile. + double minEncodableFraction = 0.05; + if (levelCounts[bestLevel] <= minEncodableFraction * points.size()) { + return -1; + } + return bestLevel; + } + + /** + * Given a vector of points in {@link CellPoint} format and an {@link S2CellId} level that has + * been chosen for encoding, returns a vector of 64-bit values that should be encoded in order to + * represent these points. Points that cannot be represented losslessly as the center of an {@code + * S2CellId} at the chosen level are indicated by the value {@link #EXCEPTION}. + */ + private static ImmutableLongArray convertCellsToValues(List cellPoints, int level) { + ImmutableLongArray.Builder builder = ImmutableLongArray.builder(cellPoints.size()); + int shift = S2CellId.MAX_LEVEL - level; + for (CellPoint cp : cellPoints) { + if (cp.level != level) { + builder.add(EXCEPTION); + } else { + // Note that bit 31 of tj is always zero, and that bits are interleaved in + // such a way that bit 63 of the result is always zero. + // + // The S2CellId version of the following code is: + // long v = S2CellId.fromFaceIJ(cp.face, cp.si >> 1, cp.ti >> 1) + // .parent(level).id() >> (2 * shift + 1); + int sj = (((cp.face & 3) << 30) | (cp.si >>> 1)) >>> shift; + int tj = (((cp.face & 4) << 29) | cp.ti) >>> (shift + 1); + long v = EncodedInts.interleaveBitPairs(sj, tj); + Preconditions.checkArgument(UnsignedLongs.compare(v, bitMask(maxBitsForLevel(level))) <= 0); + builder.add(v); + } + } + return builder.build(); + } + + /** + * Returns the global minimum value {@link Base#base} and the number of bits that should be used + * to encode it ({@link Base#baseBits}). + */ + private static Base chooseBase(ImmutableLongArray values, int level, boolean haveExceptions) { + // Find the minimum and maximum non-exception values to be represented. + long vMin = EXCEPTION; + long vMax = 0; + for (int i = 0; i < values.length(); i++) { + long v = values.get(i); + if (v != EXCEPTION) { + vMin = UnsignedLongs.min(vMin, v); + vMax = UnsignedLongs.max(vMax, v); + } + } + if (vMin == EXCEPTION) { + return new Base(0, 0); + } + + // Generally "base" is chosen as the bit prefix shared by vMin and vMax. However, there are a + // few adjustments we need to make. + // + // 1. Encodings are usually smaller if the bits represented by "base" and "delta" do not + // overlap. Usually the shared prefix rule does this automatically, but if vMin == vMax or + // there are special circumstances that increase deltaBits (such as values.size() == 1) then + // we need to make an adjustment. + // + // 2. The format only allows us to represent up to 7 bytes (56 bits) of "base", so we need to + // ensure that "base" conforms to this requirement. + int minDeltaBits = (haveExceptions || values.length() == 1) ? 8 : 4; + int excludedBits = + Ints.max( + 63 - Long.numberOfLeadingZeros(vMin ^ vMax) + 1, minDeltaBits, baseShift(level, 56)); + long base = vMin & ~bitMask(excludedBits); + + int baseBits = 0; + // Determine how many bytes are needed to represent this prefix. + if (base != 0) { + int lowBit = Long.numberOfTrailingZeros(base); + baseBits = (maxBitsForLevel(level) - lowBit + 7) & ~7; + } + + // Since baseBits has been rounded up to a multiple of 8, we may now be able to represent + // additional bits of vMin. In general this reduces the final encoded size. + // + // NOTE(user): A different strategy for choosing "base" is to encode all blocks under the + // assumption that "base" equals vMin exactly, and then set base equal to the minimum-length + // prefix of "vMin" that allows these encodings to be used. This strategy reduces the encoded + // sizes by about 0.2% relative to the strategy here, but is more complicated. + return new Base(vMin & ~bitMask(baseShift(level, baseBits)), baseBits); + } + + /** + * Returns true if the range of values {@code [dMin, dMax]} can be encoded using the specified + * parameters ({@code deltaBits}, {@code overlapBits}, and {@code haveExceptions}). + */ + private static boolean canEncode( + long dMin, long dMax, int deltaBits, int overlapBits, boolean haveExceptions) { + // "offset" can't represent the lowest (deltaBits - overlapBits) of dMin. + dMin &= ~bitMask(deltaBits - overlapBits); + + // The maximum delta is reduced by BLOCK_SIZE if any exceptions exist, since deltas + // 0..BLOCK_SIZE-1 are used to indicate exceptions. + long maxDelta = bitMask(deltaBits); + if (haveExceptions) { + if (UnsignedLongs.compare(maxDelta, BLOCK_SIZE) < 0) { + return false; + } + maxDelta -= BLOCK_SIZE; + } + // The first test below is necessary to avoid 64-bit overflow. + return (UnsignedLongs.compare(dMin, ~maxDelta) > 0) + || (UnsignedLongs.compare(dMin + maxDelta, dMax) >= 0); + } + + /** + * Given a vector of 64-bit values to be encoded and an {@link S2CellId} level, returns the + * optimal encoding parameters that should be used to encode each block. + */ + private static void getBlockCode( + MutableBlockCode code, ImmutableLongArray values, long base, boolean haveExceptions) { + // "bMin" and "bMax"n are the minimum and maximum values within this block. + long bMin = EXCEPTION; + long bMax = 0; + for (int i = 0; i < values.length(); i++) { + long v = values.get(i); + if (v != EXCEPTION) { + bMin = UnsignedLongs.min(bMin, v); + bMax = UnsignedLongs.max(bMax, v); + } + } + if (bMin == EXCEPTION) { + // All values in this block are exceptions. + code.set(4, 0, 0); + return; + } + + // Adjust the min/max values so that they are relative to "base". + bMin -= base; + bMax -= base; + + // Determine the minimum possible delta length and overlap that can be used to encode this + // block. The block will usually be encodable using the number of bits in (bMax - bMin) + // rounded up to a multiple of 4. If this is not possible, the preferred solution is to shift + // "offset" so that the delta and offset values overlap by 4 bits (since this only costs an + // average of 4 extra bits per block). Otherwise we increase the delta size by 4 bits. Certain + // cases require that both of these techniques are used. + // + // Example 1: bMin = 0x72, bMax = 0x7e. The range is 0x0c. This can be encoded using + // deltaBits = 4 and overlapBits = 0, which allows us to represent an offset of 0x70 and a + // maximum delta of 0x0f, so that we can encode values up to 0x7f. + // + // Example 2: bMin = 0x78, bMax = 0x84. The range is 0x0c, but in this case it is not + // sufficient to use deltaBits = 4 and overlapBits = 0 because we can again only represent an + // offset of 0x70, so the maximum delta of 0x0f only lets us encode values up to 0x7f. However + // if we increase the overlap to 4 bits then we can represent an offset of 0x78, which lets us + // encode values up to 0x78 + 0x0f = 0x87. + // + // Example 3: bMin = 0x08, bMax = 0x104. The range is 0xfc, so we should be able to use 8-bit + // deltas. But even with a 4-bit overlap, we can still only encode offset = 0 and a maximum + // value of 0xff. (We don't allow bigger overlaps because statistically they are not + // worthwhile). Instead we increase the delta size to 12 bits, which handles this case easily. + // + // Example 4: bMin = 0xf08, bMax = 0x1004. The range is 0xfc, so we should be able to use + // 8-bit deltas. With 8-bit deltas and no overlap, we have offset = 0xf00 and a maximum + // encodable value of 0xfff. With 8-bit deltas and a 4-bit overlap, we still have + // offset = 0xf00 and a maximum encodable value of 0xfff. Even with 12-bit deltas, we have + // offset = 0 and we can still only represent 0xfff. However with deltaBits = 12 and + // overlapBits = 4, we can represent offset = 0xf00 and a maximum encodable value of + // 0xf00 + 0xfff = 0x1eff. + // + // It is possible to show that this last example is the worst case, i.e. we do not need to + // consider increasing deltaBits or overlapBits further. + int deltaBits = (Math.max(1, 63 - Long.numberOfLeadingZeros(bMax - bMin)) + 3) & ~3; + int overlapBits = 0; + if (!canEncode(bMin, bMax, deltaBits, 0, haveExceptions)) { + if (canEncode(bMin, bMax, deltaBits, 4, haveExceptions)) { + overlapBits = 4; + } else { + Preconditions.checkArgument(deltaBits <= 60); + deltaBits += 4; + if (!canEncode(bMin, bMax, deltaBits, 0, haveExceptions)) { + Preconditions.checkArgument(canEncode(bMin, bMax, deltaBits, 4, haveExceptions)); + overlapBits = 4; + } + } + } + + // Avoid wasting 4 bits of delta when the block size is 1. This reduces the encoding size for + // single leaf cells by one byte. + if (values.length() == 1) { + Preconditions.checkArgument(deltaBits == 4 && overlapBits == 0); + deltaBits = 8; + } + + // Now determine the number of bytes needed to encode "offset", given the chosen delta length. + long maxDelta = bitMask(deltaBits) - (haveExceptions ? BLOCK_SIZE : 0); + int offsetBits = 0; + if (UnsignedLongs.compare(bMax, maxDelta) > 0) { + // At least one byte of offset is required. Round up the minimum offset to the next + // encodable value, and determine how many bits it has. + int offsetShift = deltaBits - overlapBits; + long mask = bitMask(offsetShift); + long minOffset = (bMax - maxDelta + mask) & ~mask; + Preconditions.checkArgument(minOffset != 0); + offsetBits = ((63 - Long.numberOfLeadingZeros(minOffset)) + 1 - offsetShift + 7) & ~7; + // A 64-bit offset can only be encoded with an overlap of 4 bits. + if (offsetBits == 64) { + overlapBits = 4; + } + } + code.set(deltaBits, offsetBits, overlapBits); + } +} diff --git a/core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/S2Polygon.java b/core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/S2Polygon.java new file mode 100644 index 0000000000..2da168e7ef --- /dev/null +++ b/core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/S2Polygon.java @@ -0,0 +1,2631 @@ +/* + * Copyright 2006 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.mogo.eagle.core.utilcode.geometry; + +import static com.mogo.eagle.core.utilcode.geometry.S2Projections.PROJ; + +import com.google.common.annotations.GwtCompatible; +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Preconditions; +import com.google.common.base.Predicate; +import com.google.common.base.Predicates; +import com.google.common.collect.Lists; +import com.google.common.collect.Maps; +import com.google.common.collect.Sets; +import com.google.common.collect.TreeMultimap; +import com.mogo.eagle.core.utilcode.geometry.S2ContainsPointQuery.S2VertexModel; +import com.mogo.eagle.core.utilcode.geometry.S2Projections.FaceSiTi; +import com.mogo.eagle.core.utilcode.geometry.S2Shape.MutableEdge; +import com.mogo.eagle.core.utilcode.geometry.S2ShapeIndex.S2ClippedShape; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.Serializable; +import java.util.AbstractList; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.IdentityHashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.logging.Logger; + +/** + * An S2Polygon is an S2Region object that represents a polygon. A polygon is defined by zero or + * more loops; recall that the interior of a loop is defined to be its left-hand side (see {@link + * S2Loop}.) + * + *

        There are two different conventions for creating an S2Polygon: + * + *

        First, {@link #initNested(List)} expects the input loops to be nested hierarchically. The + * polygon interior then consists of the set of points contained by an odd number of loops. So for + * example, a circular region with a hole in it would be defined as two CCW loops, with one loop + * containing the other. The loops can be provided in any order. + * + *

        When the orientation of the input loops is unknown, the nesting requirement is typically met + * by calling {@link S2Loop#normalize()} on each loop (which inverts the loop if necessary so that + * it encloses at most half the sphere). But in fact any set of loops can be used as long as (1) + * there is no pair of loops that cross, and (2) there is no pair of loops whose union is the entire + * sphere. + * + *

        Second, {@link #initOriented(List)} expects the input loops to be oriented such that the + * polygon interior is on the left-hand side of every loop. So for example, a circular region with a + * hole in it would be defined using a CCW outer loop and a CW inner loop. The loop orientations + * must all be consistent; for example, it is not valid to have one CCW loop nested inside another + * CCW loop, because the region between the two loops is on the left-hand side of one loop and the + * right-hand side of the other. + * + *

        Most clients will not call these methods directly; instead they should use {@link + * S2PolygonBuilder}, which has better support for dealing with imperfect data. + * + *

        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; initOriented may also invert some loops. + * + *

        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 + * {@link S2Loop#full()}), whereas the empty polygon has no loops at all. + * + *

        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 S2Loop). + *
        • No loop may be empty. The full loop may appear only in the full polygon. + *
        + * + */ +@GwtCompatible(serializable = true) +public final strictfp class S2Polygon implements S2Region, Comparable, Serializable { + private static final Logger log = Platform.getLoggerForClass(S2Polygon.class); + + /** Version number of the lossless encoding format for S2Polygon. */ + private static final byte LOSSLESS_ENCODING_VERSION = 1; + + /** Version number of the compressed encoding format for S2Polygon. */ + private static final byte COMPRESSED_ENCODING_VERSION = 4; + + /** Returns false for all shapes. */ + private static final Predicate REVERSE_NONE = Predicates.alwaysFalse(); + + /** Returns true for S2Loops for which {@link S2Loop#isHole()} is true. */ + private static final Predicate REVERSE_HOLES = + new Predicate() { + @Override + public boolean apply(S2Shape input) { + if (input instanceof S2Loop) { + S2Loop loop = (S2Loop) input; + return loop.isHole(); + } else { + return false; + } + } + }; + + /** + * The loops of this polygon. There is no total ordering of the loops, but a nested loop always + * follows its containing loop, and all loops between parent and child are nested somewhere under + * the parent. + */ + private final List loops = Lists.newArrayList(); + + /** + * {@code bound} is a conservative bound on all points contained by this polygon: If + * A.contains(P), then A.bound.contains(new S2LatLng(P)). + */ + private S2LatLngRect bound; + + /** + * Since "bound" is not exact, it is possible that a polygon A contains another polygon 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). + */ + private S2LatLngRect subregionBound; + + /** The spatial index for this S2Polygon. */ + @VisibleForTesting transient S2ShapeIndex index; + + /** + * In general we build the index the first time it is needed, but we make an exception for + * contains(S2Point) because this method has a simple brute force implementation that is + * relatively cheap. For this one method we keep track of the number of calls made and only build + * the index once enough calls have been made that we think an index would be worthwhile. + */ + private AtomicInteger unindexedContainsCalls = new AtomicInteger(); + + /** True if this polygon has at least one hole. */ + private boolean hasHoles = false; + + /** Total number of vertices in all loops. */ + private int numVertices = 0; + + /** Creates an empty polygon. It can be made non-empty by calling {@link #init(List)}. */ + public S2Polygon() { + bound = S2LatLngRect.empty(); + subregionBound = S2LatLngRect.empty(); + initIndex(); + } + + /** Creates an S2Polygon for a given cell. */ + public S2Polygon(S2Cell cell) { + loops.add(new S2Loop(cell)); + initOneLoop(); + } + + /** + * Creates an empty polygon and then calls {@link #initNested(List)} with the given loops. Clears + * the given list. + */ + public S2Polygon(List loops) { + initNested(loops); + } + + /** Copy constructor. */ + public S2Polygon(S2Loop loop) { + this.numVertices = loop.numVertices(); + this.bound = loop.getRectBound(); + this.subregionBound = loop.getSubregionBound(); + loops.add(loop); + initIndex(); + } + + /** Copy constructor. */ + public S2Polygon(S2Polygon src) { + copy(src); + } + + /** Initializes this polygon to a copy of the given polygon. */ + void copy(S2Polygon src) { + this.bound = src.bound; + this.subregionBound = src.subregionBound; + this.hasHoles = src.hasHoles; + this.numVertices = src.numVertices; + for (int i = 0; i < src.numLoops(); ++i) { + loops.add(new S2Loop(src.loop(i))); + } + initIndex(); + } + + private void initIndex() { + // See S2Loop for the details behind 'maxUnindexedContainsCalls'. + int maxUnindexedContainsCalls; + if (numVertices <= 8) { + maxUnindexedContainsCalls = 10; + } else if (numVertices <= 8192) { + maxUnindexedContainsCalls = 50; + } else if (numVertices <= 50000) { + maxUnindexedContainsCalls = 10; + } else { + maxUnindexedContainsCalls = 2; + } + this.unindexedContainsCalls.set(maxUnindexedContainsCalls); + + // assert (index == null || index.numShapeIds() == 0); + index = new S2ShapeIndex(); + for (int i = 0; i < numLoops(); ++i) { + index.add(loop(i)); + } + // assert (isValid()); + } + + /** Returns the same instance after initializing transient fields. */ + private Object readResolve() { + initIndex(); + return this; + } + + @Override + public boolean equals(Object o) { + if (o instanceof S2Polygon) { + S2Polygon that = (S2Polygon) o; + return this.numVertices == that.numVertices + && this.bound.equals(that.bound) + && this.loops.equals(that.loops); + } else { + return false; + } + } + + @Override + public int hashCode() { + return bound.hashCode(); + } + + /** + * Comparator (needed by Comparable interface). For two polygons to be compared as equal: + * + *
          + *
        • They must have the same number of loops + *
        • The loops must be ordered in the same way (this is guaranteed by the total ordering + * imposed by {@link #sortValueLoops}) + *
        • Loops must be logically equivalent (even if ordered with a different starting point, e.g. + * ABCD and BCDA). + *
        + */ + @Override + public int compareTo(S2Polygon other) { + // If number of loops differ, use that. + if (this.numLoops() != other.numLoops()) { + return this.numLoops() - other.numLoops(); + } + for (int i = 0; i < this.numLoops(); ++i) { + int compare = this.loops.get(i).compareTo(other.loops.get(i)); + if (compare != 0) { + return compare; + } + } + return 0; + } + + /** Initializes a polygon by calling {@link #initNested(List)}. */ + public void init(List loops) { + initNested(loops); + } + + /** + * Initializes this polygon from a set of 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 takes ownership of the given loops and clears the given list. It then figures + * out the loop nesting hierarchy and assigns every loop a depth. Shells have even depths, and + * holes have odd depths. Note that the loops are reordered so the hierarchy can be traversed more + * easily (see {@link #getParent(int)}, {@link #getLastDescendant(int)}, and {@link + * S2Loop#depth()}). + * + *

        This method may be called more than once, in which case any existing loops are deleted + * before being replaced by the input loops. + */ + public void initNested(List loops) { + // assert isValid(loops); + clearLoops(); + + if (loops.size() == 1) { + this.loops.clear(); + // Since we know size()==1, use remove(0) instead of get(0) followed by clear(). + this.loops.add(loops.remove(0)); + initOneLoop(); + return; + } + + IdentityHashMap> loopMap = Maps.newIdentityHashMap(); + // Yes, a null key is valid. It is used here to refer to the root of the loopMap. + loopMap.put(null, Lists.newArrayList()); + + for (S2Loop loop : loops) { + insertLoop(loop, null, loopMap); + } + loops.clear(); + + // Sort all of the lists of loops; in this way we guarantee a total ordering on loops in the + // polygon. Loops will be sorted by their natural ordering, while also preserving the + // requirement that each loop is immediately followed by its descendants in the nesting + // hierarchy. + // + // TODO(andriy): as per kirilll in CL 18750833 code review comments: This should work for now, + // but I think it's possible to guarantee the correct order inside insertLoop by searching for + // the correct position in the children list before inserting. + sortValueLoops(loopMap); + + // Reorder the loops in depth-first traversal order. Starting at null == starting at the root. + initLoop(null, -1, loopMap); + + // TODO(dbeaumont): Add tests or preconditions for these asserts (here and elsewhere). + // forall i != j : containsChild(loop(i), loop(j), loopMap) == loop(i).containsNested(loop(j))); + + initLoopProperties(); + } + + /** + * Like {@link #initNested(List)}, but expects loops to be oriented such that the polygon interior + * is on the left-hand side of all loops. This implies that shells and holes should have opposite + * orientations in the input to this method. (During initialization, loops representing holes will + * automatically be inverted.) + */ + public void initOriented(List loops) { + // Here is the algorithm: + // + // 1. Remember which of the given loops contain S2.origin(). + // + // 2. Invert loops as necessary to ensure that they are nestable (i.e., no loop contains the + // complement of any other loop). This may result in a set of loops corresponding to the + // complement of the given polygon, but we will fix that problem later. + // + // We make the loops nestable by first normalizing all the loops (i.e., inverting any loops + // whose turning angle is negative). This handles all loops except those whose turning angle + // is very close to zero (within the maximum error tolerance). Any such loops are inverted + // if and only if they contain S2.origin(). (In theory this step is only necessary if there + // are at least two such loops.) The resulting set of loops is guaranteed to be nestable. + // + // 3. Build the polygon. This yields either the desired polygon or its complement. + // + // 4. If there is at least one loop, we find a loop L that is adjacent to S2.origin() (where + // "adjacent" means that there exists a path connecting S2.origin() to some vertex of L such + // that the path does not cross any loop). There may be a single such adjacent loop, or + // there may be several (in which case they should all have the same contains_origin() + // value). We choose L to be the loop containing the origin whose depth is greatest, or + // loop(0) (a top-level shell) if no such loop exists. + // + // 5. If (L originally contained origin) != (polygon contains origin), we invert the polygon. + // This is done by inverting a top-level shell whose turning angle is minimal and then fixing + // the nesting hierarchy. Note that because we normalized all the loops initially, this step + // is only necessary if the polygon requires at least one non-normalized loop to represent + // it. + Preconditions.checkState(this.loops.isEmpty()); + + Set containedOrigin = Sets.newIdentityHashSet(); + for (S2Loop loop : loops) { + if (loop.containsOrigin()) { + containedOrigin.add(loop); + } + double angle = loop.getTurningAngle(); + if (Math.abs(angle) > S2.getTurningAngleMaxError(loop.numVertices())) { + // Normalize the loop. + if (angle < 0) { + loop.invert(); + } + } else { + // Ensure that the loop does not contain the origin. + if (loop.containsOrigin()) { + loop.invert(); + } + } + } + initNested(loops); + if (numLoops() > 0) { + S2Loop originLoop = loop(0); + boolean polygonContainsOrigin = false; + for (int i = 0; i < numLoops(); ++i) { + if (loop(i).containsOrigin()) { + polygonContainsOrigin ^= true; + originLoop = loop(i); + } + } + if (containedOrigin.contains(originLoop) != polygonContainsOrigin) { + invert(); + } + } + + // Verify that the original loops had consistent shell/hole orientations. Each original loop L + // should have been inverted if and only if it now represents a hole. + for (S2Loop loop : loops) { + assert (containedOrigin.contains(loop) != loop.containsOrigin()) == loop.isHole(); + } + } + + /** Computes hasHoles, numVertices, bound, subregionBound, and the index.. */ + private void initLoopProperties() { + hasHoles = false; + numVertices = 0; + S2LatLngRect.Builder builder = S2LatLngRect.Builder.empty(); + for (S2Loop loop : loops) { + if (loop.isHole()) { + hasHoles = true; + } else { + builder.union(loop.getRectBound()); + } + numVertices += loop.numVertices(); + } + bound = builder.build(); + subregionBound = S2EdgeUtil.RectBounder.expandForSubregions(bound); + initIndex(); + } + + /** Given that loops contains a single loop, initializes all other fields. */ + private void initOneLoop() { + assert 1 == loops.size(); + S2Loop loop = loops.get(0); + loop.setDepth(0); + hasHoles = false; + numVertices = loop.numVertices(); + bound = loop.getRectBound(); + subregionBound = loop.getSubregionBound(); + initIndex(); + } + + /** + * Initializes a polygon from a set of {@link S2Loop}s. + * + *

        Unlike {@link #init} this method assumes the caller already knows the nesting of loops + * within other loops. The passed-in map maps from parents to their immediate child loops, with + * {@code null} mapping to the list of top-most shell loops. Immediate child loops must be + * completely spatially contained within their parent loop, but not contained in any other loop, + * except for ancestors of the parent. This method avoids the cost of determining nesting + * internally, but if the passed in nesting is wrong, future operations on the S2Polygon may be + * arbitrarily incorrect. + * + *

        Note that unlike {@link #init}, the passed-in container of loops is not cleared; however, + * the passed-in loops become owned by the S2Polygon and should not be modified by the caller + * after calling this method. + * + * @param nestedLoops loops with nesting. + */ + public void initWithNestedLoops(Map> nestedLoops) { + Preconditions.checkState(numLoops() == 0); + initLoop(null, -1, nestedLoops); + + // Empty the map as an indication we have taken ownership of the loops. + nestedLoops.clear(); + + initLoopProperties(); + } + + /** Appends the loops of this polygon to the given list and resets this polygon to be empty. */ + public void release(List loops) { + loops.addAll(this.loops); + // Reset the polygon to be empty. + this.loops.clear(); + bound = S2LatLngRect.empty(); + subregionBound = S2LatLngRect.empty(); + hasHoles = false; + numVertices = 0; + initIndex(); + } + + private void clearLoops() { + loops.clear(); + initIndex(); + } + + /** + * Returns true if the given loops form a valid polygon, including checking whether the loops + * themselves are valid. + */ + public static boolean isValid(List loops) { + return new S2Polygon(Lists.newArrayList(loops)).isValid(); + } + + /** + * Returns true if each loop on this polygon is valid, and if the relationships between all loops + * are valid. + * + *

        Specifically, this verifies that {@link S2Loop#isValid} is true for each {@link S2Loop}, and + * that {@link S2Polygon#isValid(List)} is true for the whole list of loops. + */ + public boolean isValid() { + S2Error error = new S2Error(); + return !findValidationError(error); + } + + /** + * Returns true if this is *not* a valid polygon and sets {@code error} appropriately. Otherwise, + * returns false and leaves {@code error} unchanged. + */ + public boolean findValidationError(S2Error error) { + for (int i = 0; i < numLoops(); ++i) { + // Check for loop errors that don't require building an S2ShapeIndex. + if (loop(i).findValidationErrorNoIndex(error)) { + error.init(error.code(), "Loop " + i + ": " + error.text()); + return true; + } + // Check that no loop is empty, and that the full loop only appears in the full polygon. + if (loop(i).isEmpty()) { + error.init(S2Error.Code.POLYGON_EMPTY_LOOP, "Loop " + i + ": empty loops are not allowed."); + return true; + } + if (loop(i).isFull() && numLoops() > 1) { + error.init( + S2Error.Code.POLYGON_EXCESS_FULL_LOOP, + "Loop " + i + ": full loop appears in non-full polygon"); + return true; + } + } + // Finally, check for loop self-intersections and loop pairs that cross (including duplicate + // edges and vertices). + if (S2ShapeUtil.findAnyCrossing(index, loops, error)) { + return true; + } + + // Finally, verify the loop nesting hierarchy. + return findLoopNestingError(error); + } + + /** Returns true if there is an error in the loop nesting hierarchy. */ + private boolean findLoopNestingError(S2Error error) { + // First check that the loop depths make sense. + for (int lastDepth = -1, i = 0; i < numLoops(); i++) { + int depth = loop(i).depth(); + if (depth < 0 || depth > lastDepth + 1) { + error.init( + S2Error.Code.POLYGON_INVALID_LOOP_DEPTH, + "Loop %d: invalid loop depth (%d)", i, depth); + return true; + } + lastDepth = depth; + } + // Then check that they correspond to the actual loop nesting. This test + // is quadratic in the number of loops but the cost per iteration is small. + for (int i = 0; i < numLoops(); i++) { + S2Loop loop = loop(i); + int last = getLastDescendant(i); + for (int j = 0; j < numLoops(); j++) { + if (i == j) { + continue; + } + boolean nested = (j >= i + 1) && (j <= last); + boolean bReverse = false; + if (containsNonCrossingBoundary(loop, loop(j), bReverse) != nested) { + error.init( + S2Error.Code.POLYGON_INVALID_LOOP_NESTING, + "Invalid nesting: loop %d should %scontain loop %d", i, nested ? "" : "not ", j); + return true; + } + } + } + return false; + } + + public boolean isEmpty() { + return loops.isEmpty(); + } + + public boolean isFull() { + return loops.size() == 1 && loops.get(0).isFull(); + } + + public int numLoops() { + return loops.size(); + } + + /** + * Returns the loop at the given index. Note that during initialization, the given loops are + * reordered according to a preorder traversal of the loop nesting hierarchy. This implies that + * every loop is immediately followed by its descendants. This hierarchy can be traversed using + * the methods {@link #getParent(int)}, {@link #getLastDescendant(int)}, and {@link + * S2Loop#depth()}. + */ + public S2Loop loop(int k) { + return loops.get(k); + } + + /** Returns a view of the list of {@link S2Loop}s that make up this S2Polygon. */ + public List getLoops() { + return new AbstractList() { + @Override + public int size() { + return loops.size(); + } + + @Override + public S2Loop get(int index) { + return loop(index); + } + }; + } + + /** Returns the index of this polygon. */ + public S2ShapeIndex index() { + return index; + } + + /** Returns the index of the parent of loop {@code k}, or -1 if it has no parent. */ + public int getParent(int k) { + int depth = loop(k).depth(); + if (depth == 0) { + return -1; // Optimization. + } + while (--k >= 0 && loop(k).depth() >= depth) { + // spin + } + return k; + } + + /** + * Returns the index of the last loop that is contained within loop {@code k}. Returns {@code + * numLoops() - 1} if {@code k < 0}. Note that loops are indexed according to a preorder traversal + * of the nesting hierarchy, so the immediate children of loop {@code k} can be found by iterating + * over loops {@code (k+1)..getLastDescendant(k)} and selecting those whose depth is equal to + * {@code (loop(k).depth() + 1)}. + */ + public int getLastDescendant(int k) { + if (k < 0) { + return numLoops() - 1; + } + int depth = loop(k).depth(); + while (++k < numLoops() && loop(k).depth() > depth) { + // spin + } + return k - 1; + } + + private S2AreaCentroid getAreaCentroid(boolean doCentroid) { + double areaSum = 0; + S2Point centroidSum = S2Point.ORIGIN; + for (int i = 0; i < numLoops(); ++i) { + S2AreaCentroid areaCentroid = doCentroid ? loop(i).getAreaAndCentroid() : null; + double loopArea = doCentroid ? areaCentroid.getArea() : loop(i).getArea(); + + int loopSign = loop(i).sign(); + areaSum += loopSign * loopArea; + if (doCentroid && !loop(i).isEmptyOrFull()) { + S2Point currentCentroid = areaCentroid.getCentroid(); + if (currentCentroid != null) { + centroidSum = + new S2Point( + centroidSum.x + loopSign * currentCentroid.x, + centroidSum.y + loopSign * currentCentroid.y, + centroidSum.z + loopSign * currentCentroid.z); + } + } + } + + return new S2AreaCentroid(areaSum, doCentroid ? centroidSum : null); + } + + /** + * Returns the area of the polygon interior, i.e. the region on the left side of an odd number of + * loops (the area is between 0 and 4*Pi) and the true centroid of the polygon, weighted by the + * area of the polygon (see s2.h for details on centroids). Note that the centroid might not be + * contained by the polygon. + */ + public S2AreaCentroid getAreaAndCentroid() { + return getAreaCentroid(true); + } + + /** + * Returns the area of the polygon interior, i.e. the region on the left side of an odd number of + * loops. The return value is between 0 and 4*Pi. + */ + public double getArea() { + return getAreaCentroid(false).getArea(); + } + + /** + * Returns the true centroid of the polygon, weighted by the area of the polygon (see s2.h for + * details on centroids). Note that the centroid might not be contained by the polygon. + */ + public S2Point getCentroid() { + return getAreaCentroid(true).getCentroid(); + } + + /** + * If all of the polygon's vertices happen to be the centers of S2Cells at some level, then + * returns that level, otherwise returns -1. See also {@link #initToSnapped(S2Polygon, int)} and + * {@link S2PolygonBuilder.Options.Builder#setSnapToCellCenters(boolean)}. Returns -1 if the + * polygon has no vertices. + */ + public int getSnapLevel() { + int snapLevel = -1; + for (S2Loop loop : loops) { + for (int j = 0; j < loop.numVertices(); j++) { + S2Point p = loop.vertex(j); + FaceSiTi faceSiTi = S2Projections.PROJ.xyzToFaceSiTi(p); + int level = S2Projections.PROJ.levelIfCenter(faceSiTi, p); + if (level < 0) { + // Vertex is not a cell center. + return level; + } + if (level != snapLevel) { + if (snapLevel < 0) { + // First vertex. + snapLevel = level; + } else { + // Vertices at more than one cell level. + return -1; + } + } + } + } + return snapLevel; + } + + /** + * Computes the level at which most of the vertices are snapped. If multiple levels have the same + * maximum number of vertices snapped to it, the first one (lowest level number / largest area / + * smallest encoding length) will be chosen, so this is desired. Returns -1 for unsnapped + * polygons. + * + *

        See also {@link #initToSnapped(S2Polygon, int)} and {@link + * S2PolygonBuilder.Options.Builder#setSnapToCellCenters(boolean)}. + */ + int getBestSnapLevel() { + int[] histogram = new int[S2CellId.MAX_LEVEL + 1]; + for (S2Loop loop : loops) { + for (S2Point p : loop.vertices()) { + FaceSiTi faceSiTi = S2Projections.PROJ.xyzToFaceSiTi(p); + int level = S2Projections.PROJ.levelIfCenter(faceSiTi, p); + // Level is -1 for unsnapped points. + if (level >= 0) { + histogram[level]++; + } + } + } + int snapLevel = 0; + for (int i = 1; i < histogram.length; i++) { + if (histogram[i] > histogram[snapLevel]) { + snapLevel = i; + } + } + if (histogram[snapLevel] == 0 && !isEmpty()) { + // This is an unsnapped polygon. + return -1; + } + return snapLevel; + } + + /** + * Returns the shortest distance from a point P to this polygon, given as the angle formed between + * P, the origin, and the nearest point on the polygon to P. This angle in radians is equivalent + * to the arclength along the unit sphere. + * + *

        If the point is contained inside the polygon, the distance returned is 0. + */ + public S1Angle getDistance(S2Point p) { + if (contains(p)) { + return S1Angle.radians(0); + } + + // The farthest point from p on the sphere is its antipode, which is an angle of PI radians. + // This is an upper bound on the angle. + S1Angle minDistance = S1Angle.radians(Math.PI); + for (int i = 0; i < numLoops(); i++) { + minDistance = S1Angle.min(minDistance, loop(i).getDistance(p)); + } + + return minDistance; + } + + /** + * Returns the overlap fraction of polygon b on polygon a, i.e. the ratio of area of intersection + * to the area of polygon a. + */ + public static double getOverlapFraction(S2Polygon a, S2Polygon b) { + S2Polygon intersection = new S2Polygon(); + intersection.initToIntersection(a, b); + double intersectionArea = intersection.getArea(); + double aArea = a.getArea(); + if (aArea > 0) { + return intersectionArea >= aArea ? 1 : intersectionArea / aArea; + } else { + return 0; + } + } + + /** + * Returns a point on the polygon that is closest to point P. The distance between these two + * points should be the result of {@link #getDistance(S2Point)}. + * + *

        If point P is contained within the loop, it is returned. + * + *

        The polygon must not be empty. + */ + public S2Point project(S2Point p) { + Preconditions.checkState(!loops.isEmpty()); + if (contains(p)) { + return p; + } + S2Point normalized = S2Point.normalize(p); + + // The farthest point from p on the sphere is its antipode, which is an angle of PI radians. + // This is an upper bound on the angle. + S1Angle minDistance = S1Angle.radians(Math.PI); + int minLoopIndex = 0; + int minVertexIndex = 0; + for (int loopIndex = 0; loopIndex < loops.size(); loopIndex++) { + S2Loop loop = loops.get(loopIndex); + for (int vertexIndex = 0; vertexIndex < loop.numVertices(); vertexIndex++) { + S1Angle distanceToSegment = + S2EdgeUtil.getDistance( + normalized, loop.vertex(vertexIndex), loop.vertex(vertexIndex + 1)); + if (minDistance.greaterThan(distanceToSegment)) { + minDistance = distanceToSegment; + minLoopIndex = loopIndex; + minVertexIndex = vertexIndex; + } + } + } + S2Loop minLoop = loop(minLoopIndex); + S2Point closestPoint = + S2EdgeUtil.getClosestPoint( + p, minLoop.vertex(minVertexIndex), minLoop.vertex(minVertexIndex + 1)); + return closestPoint; + } + + /** + * Returns true if this polygon contains the given other polygon, i.e., if polygon A contains all + * points contained by polygon B. + */ + public boolean contains(S2Polygon b) { + // If both polygons have one loop, use the more efficient S2Loop method. + // Note that S2Loop.contains does its own bounding rectangle check. + if (numLoops() == 1 && b.numLoops() == 1) { + return loop(0).contains(b.loop(0)); + } + + // Otherwise if neither polygon has holes, we can still use the more efficient S2Loop.contains() + // method (rather than CompareBoundary), but it's worthwhile to do our own bounds check first. + if (!subregionBound.contains(b.getRectBound())) { + // Even though bound(A) does not contain bound(B), it is still possible that A contains B. + // This can only happen when the union of the two bounds spans all longitudes. For example, + // suppose that B consists of two shells with a longitude gap between them, while A consists + // of one shell that surrounds both shells of B but goes the other way around the sphere (so + // that it does not intersect the longitude gap). + if (!bound.lng().union(b.getRectBound().lng()).isFull()) { + return false; + } + } + + if (!hasHoles && !b.hasHoles) { + for (int j = 0; j < b.numLoops(); ++j) { + if (!anyLoopContains(b.loop(j))) { + return false; + } + } + return true; + } + + // Polygon A contains B iff B does not intersect the complement of A. From the intersection + // algorithm below, this means that the complement of A must exclude the entire boundary of B, + // and B must exclude all shell boundaries of the complement of A. (It can be shown that B must + // then exclude the entire boundary of the complement of A.) The first call below returns false + // if the boundaries cross, therefore the second call does not need to check for any crossing + // edges (which makes it cheaper). + return containsBoundary(b) && b.excludesNonCrossingComplementShells(this); + } + + /** + * Returns true if this polygon (A) approximately contains the given other polygon (B). This is + * true if it is possible to move the vertices of B no further than "vertexMergeRadius" such that + * A contains the modified B. + * + *

        For example, the empty polygon will contain any polygon whose maximum width is no more than + * vertexMergeRadius. + */ + public boolean approxContains(S2Polygon b, S1Angle vertexMergeRadius) { + S2Polygon difference = new S2Polygon(); + difference.initToDifferenceSloppy(b, this, vertexMergeRadius); + return difference.numLoops() == 0; + } + + /** + * Returns true if this polygon intersects the given other polygon, i.e., if there is a point that + * is contained by both polygons. + */ + public boolean intersects(S2Polygon b) { + // If both polygons have one loop, use the more efficient S2Loop method. + // Note that S2Loop.intersects does its own bounding rectangle check. + if (numLoops() == 1 && b.numLoops() == 1) { + return loop(0).intersects(b.loop(0)); + } + + // Otherwise if neither polygon has holes, we can still use the more efficient S2Loop.intersects + // method. The polygons intersect if and only if some pair of loop regions intersect. + if (!bound.intersects(b.getRectBound())) { + return false; + } + if (!hasHoles && !b.hasHoles) { + for (S2Loop loop : b.loops) { + if (anyLoopIntersects(loop)) { + return true; + } + } + return false; + } + + // Polygon A is disjoint from B if A excludes the entire boundary of B and B excludes all shell + // boundaries of A. (It can be shown that B must then exclude the entire boundary of A.) The + // first call below returns false if the boundaries cross, therefore the second call does not + // need to check for crossing edges. + return !excludesBoundary(b) || !b.excludesNonCrossingShells(this); + } + + /** + * Indexing structure to efficiently {@link #clipEdge} of a polygon. This is an abstract class + * because we need to use if for both polygons (for {@link #initToIntersection} and friends) and + * for sets of lists of points (for initToSimplified() future?). + * + *

        Usage: In your subclass, create an array of vertex counts for each loop in the loop sequence + * and pass it to this constructor. Overwrite {@link #edgeFromTo}, calling {@link #decodeIndex} + * and use the resulting two indices to access your vertices. + */ + private abstract static class S2LoopSequenceIndex extends S2EdgeIndex { + /** Map from the uni-dimensional edge index to the loop this edge belongs to. */ + private final int[] indexToLoop; + + /** + * Reverse of {@link #indexToLoop}: maps a loop index to the uni-dimensional index of the first + * edge in the loop. + */ + private final int[] loopToFirstIndex; + + /** + * Must be called by each subclass with the array of vertices per loop. The length of the array + * is the number of loops, and the {@code i} th loop's vertex count is in the {@code + * i} th index of the array. + */ + public S2LoopSequenceIndex(int[] numVertices) { + int totalEdges = 0; + for (int edges : numVertices) { + totalEdges += edges; + } + indexToLoop = new int[totalEdges]; + loopToFirstIndex = new int[numVertices.length]; + + totalEdges = 0; + for (int j = 0; j < numVertices.length; j++) { + loopToFirstIndex[j] = totalEdges; + for (int i = 0; i < numVertices[j]; i++) { + indexToLoop[totalEdges] = j; + totalEdges++; + } + } + } + + public final LoopVertexIndexPair decodeIndex(int index) { + int loopIndex = indexToLoop[index]; + int vertexInLoop = index - loopToFirstIndex[loopIndex]; + return new LoopVertexIndexPair(loopIndex, vertexInLoop); + } + + @Override + public final int getNumEdges() { + return indexToLoop.length; + } + + /** + * Mark the {@link #edgeFromTo} method abstract again, so children of this class must + * implement it without using {@link #edgeFrom(int)} and {@link #edgeTo(int)}. + */ + @Override + public abstract S2Edge edgeFromTo(int index); + + @Override + public S2Point edgeFrom(int index) { + return edgeFromTo(index).getStart(); + } + + @Override + public S2Point edgeTo(int index) { + return edgeFromTo(index).getEnd(); + } + } + + /** Indexing structure for an {@link S2Polygon}. */ + public static final class S2PolygonIndex extends S2LoopSequenceIndex { + private final S2Polygon poly; + private final boolean reverse; + + /** Returns number of vertices per loop. */ + private static int[] getVertices(S2Polygon poly) { + int[] vertices = new int[poly.numLoops()]; + for (int i = 0; i < vertices.length; i++) { + vertices[i] = poly.loop(i).numVertices(); + } + return vertices; + } + + public S2PolygonIndex(S2Polygon poly) { + this(poly, false); + } + + S2PolygonIndex(S2Polygon poly, boolean reverse) { + super(getVertices(poly)); + this.poly = poly; + this.reverse = reverse; + } + + @Override + public S2Edge edgeFromTo(int index) { + LoopVertexIndexPair indices = decodeIndex(index); + int loopIndex = indices.getLoopIndex(); + int vertexInLoop = indices.getVertexIndex(); + S2Loop loop = poly.loop(loopIndex); + int fromIndex; + int toIndex; + if (loop.isHole() ^ reverse) { + fromIndex = loop.numVertices() - 1 - vertexInLoop; + toIndex = 2 * loop.numVertices() - 2 - vertexInLoop; + } else { + fromIndex = vertexInLoop; + toIndex = vertexInLoop + 1; + } + S2Point from = loop.vertex(fromIndex); + S2Point to = loop.vertex(toIndex); + return new S2Edge(from, to); + } + } + + /** + * Clips the boundary of A to the interior of B, and adds the resulting edges to {@code builder}. + * Shells are directed CCW and holes are directed clockwise. If {@code reverseA} is true, these + * directions are reversed in polygon A. If {@code invertB} is true, the boundary of A is clipped + * to the exterior rather than the interior of B. If {@code addSharedEdges} is true, then the + * output will include any edges that are shared between A and B (both edges must be in the same + * direction after any edge reversals are taken into account). + */ + private static void clipBoundary( + final S2Polygon a, + boolean reverseA, + final S2Polygon b, + boolean invertB, + boolean addSharedEdges, + S2PolygonBuilder builder) { + EdgeClipper clipper = new EdgeClipper(b.index, addSharedEdges, REVERSE_HOLES); + List intersections = Lists.newArrayList(); + for (S2Loop aLoop : a.loops) { + int n = aLoop.numVertices(); + int dir = (aLoop.isHole() ^ reverseA) ? -1 : 1; + boolean inside = b.contains(aLoop.vertex(0)) ^ invertB; + for (int j = (dir > 0) ? 0 : n; n > 0; --n, j += dir) { + S2Point a0 = aLoop.vertex(j); + S2Point a1 = aLoop.vertex(j + dir); + clipper.clipEdge(a0, a1, intersections); + + if (inside) { + intersections.add(new ParametrizedS2Point(0, a0)); + } + inside = ((intersections.size() & 0x1) == 0x1); + // assert (b.contains(a1) ^ invertB == inside); + if (inside) { + intersections.add(new ParametrizedS2Point(1, a1)); + } + + Collections.sort(intersections); + for (int k = 0; k < intersections.size(); k += 2) { + S2Point x = intersections.get(k).getPoint(); + S2Point y = intersections.get(k + 1).getPoint(); + if (x.equalsPoint(y)) { + continue; + } + builder.addEdge(x, y); + } + intersections.clear(); + } + } + } + + /** Returns the total number of vertices in all loops. */ + public int getNumVertices() { + return this.numVertices; + } + + /** Initializes this polygon to the complement of the given polygon. */ + public void initToComplement(S2Polygon a) { + Preconditions.checkState(numLoops() == 0); + copy(a); + invert(); + } + + /** + * Use S2PolygonBuilder to build this polygon by assembling the edges of a given polygon after + * snapping its vertices to the center of leaf cells. This will simplify the polygon with a + * tolerance of {@code S2Projections.maxDiag.getValue(S2CellId.MAX_LEVEL)}, or approximately 0.13 + * microdegrees, or 1.5cm on the surface of the Earth. Such a polygon can be efficiently + * compressed when serialized. The snap level can be changed to a non-leaf level if needed. + */ + public void initToSnapped(final S2Polygon a, int snapLevel) { + // TODO(user): Remove (tolerance * 0.1) from initToSimplified and use that instead. + // Ensure that there will be no two vertices within the max leaf cell diagonal of each other, + // therefore no two vertices in the same leaf cell, and that no vertex will cross an edge after + // the points have been snapped to the centers of leaf cells. Add 1e-15 to the tolerance so we + // don't set a tighter than leaf cell level because of numerical inaccuracy.n + S2PolygonBuilder.Options options = + S2PolygonBuilder.Options.builder() + .setRobustnessRadius(S1Angle.radians(PROJ.maxDiag.getValue(snapLevel) / 2.0 + 1e-15)) + .setSnapToCellCenters(true) + .build(); + + S2PolygonBuilder polygonBuilder = new S2PolygonBuilder(options); + polygonBuilder.addPolygon(a); + + if (!polygonBuilder.assemblePolygon(this, null)) { + log.severe("assemblePolygon failed in initToSnapped"); + } + + // If there are no loops, check whether the result should be the full + // polygon rather than the empty one. (See InitToIntersectionSloppy.) + if (numLoops() == 0) { + if (a.bound.area() > 2.0 * S2.M_PI && a.getArea() > 2.0 * S2.M_PI) { + invert(); + } + } + } + + /** Inverts this polygon (replacing it by its complement.) */ + private void invert() { + // Inverting any one loop will invert the polygon. The best loop to invert is the one whose + // area is largest, since this yields the smallest area after inversion. The loop with the + // largest area is always at depth 0. The descendants of this loop all have their depth reduced + // by 1, while the former siblings of this loop all have their depth increased by 1. + + // The empty and full polygons are handled specially. + if (isEmpty()) { + loops.add(S2Loop.full()); + } else if (isFull()) { + clearLoops(); + } else { + // Find the loop whose area is largest (i.e., whose turning angle is smallest), minimizing + // calls to getTurningAngle(). In particular, for polygons with a single shell at level 0 + // there is not need to call GetTurningAngle() at all. (This method is relatively expensive.) + int best = -1; + double bestAngle = 0; + for (int i = 1; i < numLoops(); ++i) { + S2Loop loop = loop(i); + if (loop.depth() == 0) { + // We defer computing the turning angle of loop 0 until we discover that the polygon has + // another top-level shell. + if (best == -1) { + best = 0; + bestAngle = loop(best).getTurningAngle(); + } + double angle = loop.getTurningAngle(); + if (angle < bestAngle) { + best = i; + bestAngle = angle; + } + } + } + + if (best < 0) { + best = 0; + } + + // Build the new loops vector, starting with the inverted loop. + loop(best).invert(); + List newLoops = Lists.newArrayListWithCapacity(numLoops()); + newLoops.add(loop(best)); + + // Add the former siblings of this loop as descendants. + int lastBest = getLastDescendant(best); + for (int i = 0; i < numLoops(); ++i) { + if (i < best || i > lastBest) { + S2Loop loop = loop(i); + loop.setDepth(loop.depth() + 1); + newLoops.add(loop); + } + } + + // Add the former children of this loop as siblings. + for (int i = 0; i < numLoops(); ++i) { + if (i > best && i <= lastBest) { + S2Loop loop = loop(i); + loop.setDepth(loop.depth() - 1); + newLoops.add(loop); + } + } + Preconditions.checkState(loops.size() == newLoops.size()); + loops.clear(); + loops.addAll(newLoops); + } + + initLoopProperties(); + } + + /** + * Initializes this polygon to the intersection, union, or difference (A - B) of the given two + * polygons. The {@code vertexMergeRadius} determines how close two vertices must be to be merged + * together and how close a vertex must be to an edge in order to be spliced into it (see {@link + * S2PolygonBuilder} for details). By default, the merge radius is just large enough to compensate + * for errors that occur when computing intersection points between edges ({@link + * S2EdgeUtil#DEFAULT_INTERSECTION_TOLERANCE}). + * + *

        If you are going to convert the resulting polygon to a lower-precision format, it is + * necessary to increase the merge radius in order to get a valid result after rounding (i.e., no + * duplicate vertices, etc). For example, if you are going to convert them to {@code + * geostore.PolygonProto} format, then {@code S1Angle.e7(1)} is a good value for {@code + * vertexMergeRadius}. + */ + public void initToIntersection(final S2Polygon a, final S2Polygon b) { + initToIntersectionSloppy(a, b, S2EdgeUtil.DEFAULT_INTERSECTION_TOLERANCE); + } + + public void initToIntersectionSloppy( + final S2Polygon a, final S2Polygon b, S1Angle vertexMergeRadius) { + Preconditions.checkState(numLoops() == 0); + if (a.isEmpty() || b.isEmpty()) { + // From the check above we are already empty. + } else if (a.isFull()) { + copy(b); + } else if (b.isFull()) { + copy(a); + } else { + if (!a.bound.intersects(b.bound)) { + return; + } + + // We want the boundary of A clipped to the interior of B, plus the boundary of B clipped to + // the interior of A, plus one copy of any directed edges that are in both boundaries. + + S2PolygonBuilder.Options options = + S2PolygonBuilder.Options.DIRECTED_XOR + .toBuilder() + .setMergeDistance(vertexMergeRadius) + .build(); + S2PolygonBuilder builder = new S2PolygonBuilder(options); + clipBoundary(a, false, b, false, true, builder); + clipBoundary(b, false, a, false, false, builder); + if (!builder.assemblePolygon(this, null)) { + // TODO(andriy): do something more meaningful here. + log.severe("Bad directed edges"); + } + + // If the result had a non-empty boundary then we are done. Unfortunately, if the boundary is + // empty then there are two possible results: the empty polygon or the full polygon. This + // choice would be trivial to resolve except for the existence of "vertex_merge_radius" and + // also numerical errors when computing edge intersection points. In particular: + // + // - The intersection of two non-full polygons may be full. For example, one or both + // polygons may have tiny cracks that are eliminated due to vertex merging/edge splicing. + // + // - The intersection of two polygons that both contain S2.origin() (or any other point) may + // be empty. For example, both polygons may have tiny shells that surround the common + // point but that are eliminated. + // + // - Even before any vertex merging/edge splicing, the computed boundary edges are not useful + // in distinguishing almost-full polygons from almost-empty due to numerical errors in + // computing edge intersections. Such errors can reverse the orientation of narrow cracks + // or slivers. + // + // So instead we fall back to heuristics. Essentially we compute the minimum and maximum + // intersection area based on the areas of the two input polygons. If only one of {0, 4*Pi} + // is possible then we return that result. If neither is possible (before vertex merging, + // etc) then we return the one that is closest to being possible. (It never true that + // both are possible.) + if (numLoops() == 0) { + // We know that both polygons are non-empty due to the initial bounds check. By far the + // most common case is that the intersection is empty, so we want to make that case fast. + // The intersection area satisfies: + // + // max(0, A + B - 4*Pi) <= Intersection(A, B) <= min(A, B) + // + // where A, B refer to a polygon and/or its area. Note that if either A or B is at most + // 2*Pi, the result must be "empty". We can use the bounding rectangle areas as upper + // bounds on the polygon areas. + if (a.bound.area() <= 2 * S2.M_PI || b.bound.area() <= 2 * S2.M_PI) { + return; + } + double aArea = a.getArea(); + double bArea = b.getArea(); + double minArea = Math.max(0.0, aArea + bArea - 4 * S2.M_PI); + double maxArea = Math.min(aArea, bArea); + if (minArea > 4 * S2.M_PI - maxArea) { + invert(); + } + } + } + } + + public void initToUnion(final S2Polygon a, final S2Polygon b) { + initToUnionSloppy(a, b, S2EdgeUtil.DEFAULT_INTERSECTION_TOLERANCE); + } + + public void initToUnionSloppy(final S2Polygon a, final S2Polygon b, S1Angle vertexMergeRadius) { + Preconditions.checkState(numLoops() == 0); + if (a.isEmpty() || b.isFull()) { + copy(b); + } else if (b.isEmpty() || a.isFull()) { + copy(a); + } else { + // We want the boundary of A clipped to the exterior of B, plus the boundary of B clipped to + // the exterior of A, plus one copy of any directed edges that are in both boundaries. + S2PolygonBuilder.Options options = + S2PolygonBuilder.Options.DIRECTED_XOR + .toBuilder() + .setMergeDistance(vertexMergeRadius) + .build(); + S2PolygonBuilder builder = new S2PolygonBuilder(options); + clipBoundary(a, false, b, true, true, builder); + clipBoundary(b, false, a, true, false, builder); + if (!builder.assemblePolygon(this, null)) { + // TODO(andriy): do something more meaningful here. + log.severe("Bad directed edges"); + } + + if (numLoops() == 0) { + // See comments in InitToIntersectionSloppy(). In this case, the union area satisfies: + // + // max(A, B) <= Union(A, B) <= min(4*Pi, A + B) + // + // The most common case is that neither input polygon is empty, but the union is empty due + // to vertex merging/simplification. + if (a.bound.area() + b.bound.area() <= 2 * S2.M_PI) { + return; + } + double aArea = a.getArea(); + double bArea = b.getArea(); + double minArea = Math.max(aArea, bArea); + double maxArea = Math.min(4 * S2.M_PI, aArea + bArea); + if (minArea > 4 * S2.M_PI - maxArea) { + invert(); + } + } + } + } + + public void initToDifference(final S2Polygon a, final S2Polygon b) { + initToDifferenceSloppy(a, b, S2EdgeUtil.DEFAULT_INTERSECTION_TOLERANCE); + } + + public void initToDifferenceSloppy( + final S2Polygon a, final S2Polygon b, S1Angle vertexMergeRadius) { + Preconditions.checkState(numLoops() == 0); + if (a.isEmpty() || b.isFull()) { + // From the check above, this polygon is already empty. + } else if (a.isFull()) { + initToComplement(b); + } else { + // Note that we cannot short circuit the b.isEmpty() case because even something and nothing + // might have no difference if the difference falls within the merge distance. + + // We want the boundary of A clipped to the exterior of B, plus the reversed boundary of B + // clipped to the interior of A, plus one copy of any edge in A that is also a reverse edge in + // B. + S2PolygonBuilder.Options options = + S2PolygonBuilder.Options.DIRECTED_XOR + .toBuilder() + .setMergeDistance(vertexMergeRadius) + .build(); + S2PolygonBuilder builder = new S2PolygonBuilder(options); + clipBoundary(a, false, b, true, true, builder); + clipBoundary(b, true, a, false, false, builder); + if (!builder.assemblePolygon(this, null)) { + log.severe("Bad directed edges"); + } + + if (numLoops() == 0) { + // See comments in InitToIntersectionSloppy(). In this case, the difference area satisfies: + // + // max(0, A - B) <= Difference(A, B) <= min(A, 4*Pi - B) + // + // By far the most common case is that result is empty. + if (a.bound.area() <= 2 * S2.M_PI || b.bound.area() >= 2 * S2.M_PI) { + return; + } + double aArea = a.getArea(); + double bArea = b.getArea(); + double minArea = Math.max(0.0, aArea - bArea); + double maxArea = Math.min(aArea, 4 * S2.M_PI - bArea); + if (minArea > 4 * S2.M_PI - maxArea) { + invert(); + } + } + } + } + + /** + * Initializes this polygon to a polygon that contains fewer vertices and is within tolerance of + * the polygon a, with some caveats. + * + *

        If {@code snapToCellCenters} is true, the vertices of this polygon will be snapped to the + * centers of cells at the smallest level that is guaranteed to result in a valid polygon given + * the specified tolerance. + * + *

          + *
        • If there is a very small island in the original polygon, it may disappear completely. + * Thus some parts of the original polygon may not be close to the simplified polygon. Those + * parts are small, though, and arguably don't need to be kept. + *
        • However, if there are dense islands, they may all disappear, instead of replacing them by + * a big simplified island. + *
        • For small tolerances (compared to the polygon size), it may happen that the simplified + * polygon has more vertices than the original, if the first step of the simplification + * creates too many self-intersections. One can construct unrealistic cases where that + * happens to an extreme degree. + *
        + */ + public void initToSimplified(S2Polygon a, S1Angle tolerance, boolean snapToCellCenters) { + initToSimplifiedInternal(a, tolerance, snapToCellCenters, null); + } + + /** + * Initializes this polygon to a polygon that contains fewer vertices and is within tolerance of + * the polygon a, while ensuring that the vertices on the cell boundary are preserved. + * + *

        Precondition: Polygon a is contained in the cell. + */ + public void initToSimplifiedInCell(S2Polygon a, final S2Cell cell, S1Angle tolerance) { + initToSimplifiedInternal( + a, + tolerance, + false, + new Predicate() { + /** Edges of the cell to test against. */ + private final S2Point[] corners = { + cell.getVertex(0), cell.getVertex(1), cell.getVertex(2), cell.getVertex(3) + }; + /** Max distance to test against. */ + private final S1Angle d = S1Angle.radians(1e-15); + /** Returns true if the vertex is close to any edge of 'cell'. */ + @Override + public boolean apply(S2Point vertex) { + for (int i = 0; i < 4; i++) { + if (S2EdgeUtil.getDistance(vertex, corners[i], corners[(i + 1) % 4]).lessThan(d)) { + return true; + } + } + return false; + } + }); + } + + /** + * Simplifies the polygon. The algorithm is straightforward and naive: + * + *

          + *
        1. Simplify each loop by removing points while staying in the tolerance zone. This uses + * {@link S2Polyline#subsampleVertices(S1Angle)}, which is not guaranteed to be optimal in + * terms of number of points. + *
        2. Break any edge in pieces such that no piece intersects any other. + *
        3. Use the polygon builder to regenerate the full polygon. + *
        4. If {@code vertexFilter} is not null, the vertices for which it returns true are kept in + * the simplified polygon. + *
        + */ + private void initToSimplifiedInternal( + S2Polygon a, S1Angle tolerance, boolean snapToCellCenters, Predicate vertexFilter) { + S2PolygonBuilder.Options.Builder options = S2PolygonBuilder.Options.UNDIRECTED_XOR.toBuilder(); + options.setValidate(false); + if (vertexFilter != null) { + // If there is a vertex filter, then we want to do as little vertex merging as possible so + // that the vertices we want to keep don't move. But on the other hand, when we break + // intersecting edges into pieces there is some error in the intersection point. + // S2PolygonBuilder needs to be able to move vertices by up to this amount in order to produce + // valid output. + options.setMergeDistance(S2EdgeUtil.DEFAULT_INTERSECTION_TOLERANCE); + } else { + // Ideally, we would want to set the vertex merge radius of the builder roughly to tolerance + // (and in fact forego the edge splitting step). Alas, if we do that, we are liable to the + // 'chain effect', where vertices are merged with close by vertices and so on, so that a + // vertex can move by an arbitrary distance. So we remain conservative: + options.setMergeDistance(S1Angle.radians(tolerance.radians() * 0.10)); + options.setSnapToCellCenters(snapToCellCenters); + } + + // Simplify each loop separately and add to the edge index. + S2PolygonBuilder builder = new S2PolygonBuilder(options.build()); + S2ShapeIndex index = new S2ShapeIndex(); + for (int i = 0; i < a.numLoops(); i++) { + S2Loop simpler = a.loop(i).simplify(tolerance, vertexFilter); + if (simpler != null) { + index.add(simpler); + } + } + + if (!index.shapes.isEmpty()) { + breakEdgesAndAddToBuilder(index, builder); + if (!builder.assemblePolygon(this, null)) { + log.warning("Bad edges in InitToSimplified."); + } + // If there are no loops, check whether the result should be the full + // polygon rather than the empty one. (See InitToIntersectionSloppy.) + if (numLoops() == 0) { + if (a.bound.area() > 2 * S2.M_PI && a.getArea() > 2 * S2.M_PI) { + invert(); + } + } + } + } + + /** + * Takes a set of possibly intersecting edges, stored in the S2ShapeIndex, and breaks the edges + * into small pieces so that there is no intersection anymore, and adds all these edges to the + * builder. + */ + public static void breakEdgesAndAddToBuilder(S2ShapeIndex index, S2PolygonBuilder builder) { + // If there are self intersections, we add the pieces separately. + // add_shared_edges ("true" below) can be false or true: it makes no + // difference due to the way we call ClipEdge. + EdgeClipper clipper = new EdgeClipper(index, true, REVERSE_NONE); + List intersections = Lists.newArrayList(); + MutableEdge edge = new MutableEdge(); + for (S2Shape shape : index.getShapes()) { + int numEdges = shape.numEdges(); + for (int e = 0; e < numEdges; e++) { + shape.getEdge(e, edge); + if (!edge.a.equalsPoint(edge.b)) { + intersections.add(new ParametrizedS2Point(0, edge.a)); + clipper.clipEdge(edge.a, edge.b, intersections); + intersections.add(new ParametrizedS2Point(1, edge.b)); + Collections.sort(intersections); + for (int k = 0; k + 1 < intersections.size(); ++k) { + S2Point p1 = intersections.get(k).getPoint(); + S2Point p2 = intersections.get(k + 1).getPoint(); + if (!p1.equalsPoint(p2)) { + builder.addEdge(p1, p2); + } + } + intersections.clear(); + } + } + } + } + + /** Returns a polygon that is the union of the given polygons. */ + public static S2Polygon union(Iterable polygons) { + return unionSloppy(polygons, S2EdgeUtil.DEFAULT_INTERSECTION_TOLERANCE); + } + + /** + * Returns a polygon that is the union of the given polygons; combines vertices that form edges + * that are almost identical, as defined by {@code vertexMergeRadius}. + */ + public static S2Polygon unionSloppy(Iterable polygons, S1Angle vertexMergeRadius) { + // Effectively create a priority queue of polygons in order of number of vertices. Repeatedly + // union the two smallest polygons and add the result to the queue until we have a single + // polygon to return. + + // map: # of vertices -> polygon + TreeMultimap queue = TreeMultimap.create(); + + for (S2Polygon polygon : polygons) { + queue.put(polygon.getNumVertices(), polygon); + } + + Set> queueSet = queue.entries(); + while (queueSet.size() > 1) { + // Pop two simplest polygons from queue. + queueSet = queue.entries(); + Iterator> smallestIter = queueSet.iterator(); + + Entry smallest = smallestIter.next(); + int aSize = smallest.getKey().intValue(); + S2Polygon aPolygon = smallest.getValue(); + smallestIter.remove(); + + smallest = smallestIter.next(); + int bSize = smallest.getKey().intValue(); + S2Polygon bPolygon = smallest.getValue(); + smallestIter.remove(); + + // Union and add result back to queue. + S2Polygon unionPolygon = new S2Polygon(); + unionPolygon.initToUnionSloppy(aPolygon, bPolygon, vertexMergeRadius); + int unionSize = aSize + bSize; + queue.put(unionSize, unionPolygon); + // We assume that the number of vertices in the union polygon is the sum of the number of + // vertices in the original polygons, which is not always true, but will almost always be a + // decent approximation, and faster than recomputing. + } + + if (queue.isEmpty()) { + return new S2Polygon(); + } else { + return queue.get(queue.asMap().firstKey()).first(); + } + } + + /** + * Intersects this polygon with the {@link S2Polyline} {@code in} and returns the resulting zero + * or more polylines. The polylines are ordered in the order they would be encountered by + * traversing {@code in} from beginning to end. Note that the output may include polylines with + * only one vertex, but there will not be any zero-vertex polylines. + * + *

        This is equivalent to calling {@link #intersectWithPolylineSloppy} with the {@code + * vertexMergeRadius} set to {@link S2EdgeUtil#DEFAULT_INTERSECTION_TOLERANCE}. + */ + public List intersectWithPolyline(S2Polyline in) { + return intersectWithPolylineSloppy(in, S2EdgeUtil.DEFAULT_INTERSECTION_TOLERANCE); + } + + /** + * Similar to {@link #intersectWithPolyline}, except that vertices will be dropped as necessary to + * ensure that all adjacent vertices in the sequence obtained by concatenating the output + * polylines will be farther than {@code vertexMergeRadius} apart. Note that this can change the + * number of output polylines and/or yield single-vertex polylines. + */ + public List intersectWithPolylineSloppy(S2Polyline in, S1Angle vertexMergeRadius) { + return internalClipPolyline(false, in, vertexMergeRadius); + } + + /** Same as {@link #intersectWithPolyline}, but subtracts this polygon from the given polyline. */ + public List subtractFromPolyline(S2Polyline in) { + return subtractFromPolylineSloppy(in, S2EdgeUtil.DEFAULT_INTERSECTION_TOLERANCE); + } + + /** + * Same as {@link #intersectWithPolylineSloppy}, but subtracts this polygon from the given + * polyline. + */ + public List subtractFromPolylineSloppy(S2Polyline in, S1Angle vertexMergeRadius) { + return internalClipPolyline(true, in, vertexMergeRadius); + } + + /** + * Clips the {@link S2Polyline} {@code a} to the interior of this polygon. The resulting + * polyline(s) will be returned. If {@code invert} is {@code true}, we clip {@code a} to the + * exterior of this polygon instead. Vertices will be dropped such that adjacent vertices will not + * be closer than {@code mergeRadius}. + * + *

        We do the intersection/subtraction by walking the polyline edges. For each edge, we compute + * all intersections with the polygon boundary and sort them in increasing order of distance along + * that edge. We then divide the intersection points into pairs, and output a clipped polyline + * segment for each one. We keep track of whether we're inside or outside of the polygon at all + * times to decide which segments to output. + */ + private List internalClipPolyline(boolean invert, S2Polyline a, S1Angle mergeRadius) { + + List out = Lists.newArrayList(); + EdgeClipper clipper = new EdgeClipper(index, true, REVERSE_NONE); + List intersections = Lists.newArrayList(); + List vertices = Lists.newArrayList(); + int n = a.numVertices(); + boolean inside = contains(a.vertex(0)) ^ invert; + for (int j = 0; j < n - 1; j++) { + S2Point a0 = a.vertex(j); + S2Point a1 = a.vertex(j + 1); + clipper.clipEdge(a0, a1, intersections); + if (inside) { + intersections.add(new ParametrizedS2Point(0, a0)); + } + inside = (intersections.size() & 1) != 0; + // assert ((contains(a1) ^ invert) == inside); + if (inside) { + intersections.add(new ParametrizedS2Point(1, a1)); + } + Collections.sort(intersections); + // At this point we have a sorted array of vertex pairs representing the edge(s) obtained + // after clipping (a0, a1) against the polygon. + for (int k = 0; k < intersections.size(); k += 2) { + S2Point v0 = intersections.get(k).getPoint(); + S2Point v1 = intersections.get(k + 1).getPoint(); + if (!v0.equalsPoint(v1)) { + // If the gap from the previous vertex to this one is large enough, start a new polyline. + if (!vertices.isEmpty() + && vertices.get(vertices.size() - 1).angle(v0) > mergeRadius.radians()) { + out.add(new S2Polyline(vertices)); + vertices.clear(); + } + // Append this segment to the current polyline, ignoring any vertices that are too close + // to the previous vertex. + if (vertices.isEmpty()) { + vertices.add(v0); + } + if (vertices.get(vertices.size() - 1).angle(v1) > mergeRadius.radians()) { + vertices.add(v1); + } + } + } + intersections.clear(); + } + if (!vertices.isEmpty()) { + out.add(new S2Polyline(vertices)); + } + return out; + } + + /** + * Return true if every loop of this polygon shares at most one vertex with its parent loop. Every + * polygon has a unique normalized form. A polygon can be normalized by passing it through + * S2Builder (with no snapping) in order to reconstruct the polygon from its edges. + * + *

        Generally there is no reason to convert polygons to normalized form. It is mainly useful for + * testing in order to compare whether two polygons have exactly the same interior, even when they + * have a different loop structure. For example, a diamond nested within a square (touching at + * four points) could be represented as a square with a diamond-shaped hole, or as four triangles. + * Methods such as {@link #boundaryApproxEquals(S2Polygon, double)} will report these polygons as + * being different (because they have different boundaries) even though they contain the same + * points. However if they are both converted to normalized form (the "four triangles" version) + * then they can be compared more easily. + */ + public boolean isNormalized() { + // TODO(user): The condition tested here is insufficient. The correct condition is that each + // *connected component* of child loops can share at most one vertex with their parent loop. + // Example: suppose loop A has children B, C, D, and the following pairs are connected: AB, BC, + // CD, DA. Then the polygon is not normalized. + Set vertices = Sets.newHashSet(); + S2Loop lastParent = null; + for (int i = 0; i < numLoops(); ++i) { + S2Loop child = loop(i); + if (child.depth() == 0) { + continue; + } + S2Loop parent = loop(getParent(i)); + // Test if the loops are different; we can use identity since any two loops in a valid polygon + // can only be equal by value if they're equal by reference. + if (parent != lastParent) { + vertices.clear(); + for (int j = 0; j < parent.numVertices(); ++j) { + vertices.add(parent.vertex(j)); + } + lastParent = parent; + } + int count = 0; + for (int j = 0; j < child.numVertices(); ++j) { + if (vertices.contains(child.vertex(j))) { + ++count; + } + } + if (count > 1) { + return false; + } + } + return true; + } + + /** + * Returns true if two polygons have the same boundary, except for vertex perturbations. Both + * polygons must have loops with the same cyclic vertex order and the same nesting hierarchy, but + * the vertex locations are allowed to differ by up to {@code maxError}. Note: This method mostly + * useful only for testing purposes. + */ + boolean boundaryApproxEquals(S2Polygon b, double maxError) { + if (numLoops() != b.numLoops()) { + log.severe( + "!= loops: " + Integer.toString(numLoops()) + " vs. " + Integer.toString(b.numLoops())); + return false; + } + + // For now, we assume that there is at most one candidate match for each loop. (So far this + // method is just used for testing.) + for (int i = 0; i < numLoops(); ++i) { + S2Loop aLoop = loop(i); + boolean success = false; + for (int j = 0; j < numLoops(); ++j) { + S2Loop bLoop = b.loop(j); + if (bLoop.depth() == aLoop.depth() && bLoop.boundaryApproxEquals(aLoop, maxError)) { + success = true; + break; + } + } + if (!success) { + return false; + } + } + return true; + } + + /** + * Returns true if two polygons have boundaries that are within {@code maxError} of each other + * along their entire lengths. More precisely, there must be a bijection between the two sets of + * loops such that {@code aLoop.boundaryNear(bLoop)} is true for each aLoop in {@code this} and + * each bLoop in {@code b}. + */ + boolean boundaryNear(S2Polygon b, double maxError) { + if (numLoops() != b.numLoops()) { + return false; + } + + // For now, we assume that there is at most one candidate match for each loop. + for (int i = 0; i < numLoops(); ++i) { + S2Loop aLoop = loop(i); + boolean success = false; + for (int j = 0; j < numLoops(); ++j) { + S2Loop bLoop = b.loop(j); + if (bLoop.depth() == aLoop.depth() && bLoop.boundaryNear(aLoop, maxError)) { + success = true; + break; + } + } + if (!success) { + return false; + } + } + return true; + } + + // S2Region interface (see S2Region.java for details): + + /** + * Returns a spherical cap that bounds this loop. It may be expanded slightly such that if the + * loop contains a point P, then the bound contains P also. + */ + @Override + public S2Cap getCapBound() { + return bound.getCapBound(); + } + + /** + * Returns a fairly tight bounding latitude-longitude rectangle. It is not guaranteed to be as + * tight as possible, to ensure that if the loop contains a point P, then the bound contains P + * also. + */ + @Override + public S2LatLngRect getRectBound() { + return bound; + } + + /** + * If this method returns true, the region completely contains the given cell. Otherwise, either + * the region does not contain the cell or the containment relationship could not be determined. + */ + @Override + public boolean contains(S2Cell cell) { + S2Iterator it = index.iterator(); + S2ShapeIndex.CellRelation relation = it.locate(cell.id()); + + // If 'cell' is disjoint from all index cells, it is not contained. Similarly, if 'cell' is + // subdivided into one or more index cells, then it is not contained, since index cells are + // subdivided only if they (nearly) intersect a sufficient number of edges. (But note that if + // 'cell' itself is an index cell, then it may be contained, since it could be a cell with no + // indexed edges in the polygon interior.) + if (relation != S2ShapeIndex.CellRelation.INDEXED) { + return false; + } + + // Otherwise check if any edges intersect 'cell'. At this point, the iterator is guaranteed to + // to point to an index cell containing 'cell'. + if (boundaryApproxIntersects(it, cell)) { + return false; + } + + // Otherwise check if the polygon contains the center of 'cell'. + return contains(it, cell.getCenter()); + } + + /** + * If this method returns false, the region does not intersect the given cell. Otherwise, either + * region intersects the cell, or the intersection relationship could not be determined. + */ + @Override + public boolean mayIntersect(S2Cell target) { + S2Iterator it = index.iterator(); + S2ShapeIndex.CellRelation relation = it.locate(target.id()); + + // If 'target' does not overlap any index cell, there is no intersection. + if (relation == S2ShapeIndex.CellRelation.DISJOINT) { + return false; + } + + // If 'target' properly contains an index cell, then there is an intersection to within the + // S2ShapeIndex error bound (see contains). But if 'target' itself is an index cell, then it may + // not be contained, since it could be a cell with no indexed edges that is contained by both a + // shell and a hole, and is therefore in the polygon exterior. + if (relation == S2ShapeIndex.CellRelation.SUBDIVIDED) { + return true; + } + + // Otherwise check if any edges intersect 'target'. + if (boundaryApproxIntersects(it, target)) { + return true; + } + + // Otherwise check if the polygon contains the center of 'target'. + return contains(it, target.getCenter()); + } + + /** + * Returns true if the polygon boundary intersects {@code target}. It may also return true when + * the polygon boundary does not intersect {@code target} but some edge comes within the + * worst-case error tolerance. + * + *

        Requires: {@code it.id().contains(target.id())} (This condition is true whenever {@code + * it.locate(target)} returns INDEXED. + */ + private boolean boundaryApproxIntersects(S2Iterator it, S2Cell target) { + // assert (it.id().contains(target.id())) + S2ShapeIndex.Cell cell = it.entry(); + R2Rect bound = target.getBoundUV().expanded(S2EdgeUtil.MAX_CELL_EDGE_ERROR); + R2Vector v0 = new R2Vector(); + R2Vector v1 = new R2Vector(); + for (int a = 0; a < cell.numShapes(); ++a) { + S2ShapeIndex.S2ClippedShape aClipped = cell.clipped(a); + int aNumClipped = aClipped.numEdges(); + if (aNumClipped == 0) { + continue; + } + + // We can save some work if 'target' is the index cell itself (given that there is at least + // one indexed edge). + if (it.compareTo(target.id()) == 0) { + return true; + } + + // Otherwise, check whether any of the edges intersect 'target'. + S2Loop aLoop = (S2Loop) aClipped.shape(); + for (int i = 0; i < aNumClipped; ++i) { + int ai = aClipped.edge(i); + if (S2EdgeUtil.clipToPaddedFace( + aLoop.vertex(ai), + aLoop.vertex(ai + 1), + target.face(), + S2EdgeUtil.MAX_CELL_EDGE_ERROR, + v0, + v1) + && S2EdgeUtil.intersectsRect(v0, v1, bound)) { + return true; + } + } + } + return false; + } + + /** The point {@code p} does not need to be normalized. */ + @Override + public boolean contains(S2Point p) { + // Note(user): A bounds check slows down the C++ version of this method by about 50%. It is + // worthwhile only when it might allow us to delay building the index. + if (!index.isFresh() && !bound.contains(p)) { + return false; + } + + // For small polygons it is faster to just check all the crossings. Otherwise we keep track of + // the number of calls to contains() and only build the index once enough calls have been made + // so that we think it is worth the effort. See S2Loop.contains(S2Point) for detailed comments. + int maxBruteForceVertices = 32; + if (getNumVertices() <= maxBruteForceVertices + || (!index.isFresh() && unindexedContainsCalls.decrementAndGet() > 0)) { + boolean inside = false; + for (int i = 0; i < numLoops(); ++i) { + // Use brute force to avoid building the loops' S2ShapeIndex. + boolean loopContainsP = loop(i).bruteForceContains(p); + // Set 'inside' to the bitwise XOR of 'inside' and 'loopContainsP'. + inside ^= loopContainsP; + } + return inside; + } + + // Otherwise we look up the S2ShapeIndex cell containing this point. + S2Iterator it = index.iterator(); + if (!it.locate(p)) { + return false; + } + return contains(it, p); + } + + /** + * Given an iterator that is already positioned at the S2ShapeIndex.Cell containing {@code p}, + * return {@code contains(p)}. + */ + private boolean contains(S2Iterator it, S2Point p) { + // Test containment by drawing a line segment from the cell center to the given point and + // counting edge crossings. + S2ShapeIndex.Cell cell = it.entry(); + boolean inside = false; + S2EdgeUtil.EdgeCrosser crosser = new S2EdgeUtil.EdgeCrosser(it.center(), p); + for (int a = 0; a < cell.numShapes(); ++a) { + S2ShapeIndex.S2ClippedShape aClipped = cell.clipped(a); + inside ^= aClipped.containsCenter(); + int aNumClipped = aClipped.numEdges(); + if (aNumClipped > 0) { + int aiPrev = -2; + S2Loop aLoop = (S2Loop) aClipped.shape(); + for (int i = 0; i < aNumClipped; ++i) { + int ai = aClipped.edge(i); + if (ai != aiPrev + 1) { + crosser.restartAt(aLoop.vertex(ai)); + } + aiPrev = ai; + inside ^= crosser.edgeOrVertexCrossing(aLoop.vertex(ai + 1)); + } + } + } + return inside; + } + + /** For each map entry, sorts the value list. */ + private static void sortValueLoops(Map> loopMap) { + for (S2Loop key : loopMap.keySet()) { + Collections.sort(loopMap.get(key)); + } + } + + private static void insertLoop(S2Loop newLoop, S2Loop parent, Map> loopMap) { + List children = loopMap.get(parent); + + if (children == null) { + children = Lists.newArrayList(); + loopMap.put(parent, children); + } + + for (S2Loop child : children) { + if (child.containsNested(newLoop)) { + insertLoop(newLoop, child, loopMap); + return; + } + } + + // No loop may contain the complement of another loop. (Handling this case is significantly more + // complicated.) + // assert (parent == null || !newLoop.containsNested(parent)); + + // Some of the children of the parent loop may now be children of the new loop. + List newChildren = loopMap.get(newLoop); + for (int i = 0; i < children.size(); ) { + S2Loop child = children.get(i); + if (newLoop.containsNested(child)) { + if (newChildren == null) { + newChildren = Lists.newArrayList(); + loopMap.put(newLoop, newChildren); + } + newChildren.add(child); + children.remove(i); + } else { + ++i; + } + } + children.add(newLoop); + } + + private void initLoop(S2Loop loop, int depth, Map> loopMap) { + if (loop != null) { + loop.setDepth(depth); + loops.add(loop); + } + List children = loopMap.get(loop); + if (children != null) { + for (S2Loop child : children) { + initLoop(child, depth + 1, loopMap); + } + } + } + + /** + * Returns +1 if this polygon (A) contains the boundary of B, -1 if A excludes the boundary of B, + * and 0 if the boundaries of A and B cross. + */ + int compareBoundary(S2Loop b) { + int result = -1; + for (S2Loop loop : loops) { + // If B crosses any loop of A, the result is 0. Otherwise the result changes sign each time B + // is contained by a loop of A. + result *= -loop.compareBoundary(b); + if (result == 0) { + break; + } + } + return result; + } + + /** Returns true if this polygon (A) contains the entire boundary of B. */ + private boolean containsBoundary(S2Polygon b) { + for (S2Loop loop : b.loops) { + if (compareBoundary(loop) <= 0) { + return false; + } + } + return true; + } + + /** Returns true if this polygon (A) excludes the entire boundary of B. */ + private boolean excludesBoundary(S2Polygon b) { + for (S2Loop loop : b.loops) { + if (compareBoundary(loop) >= 0) { + return false; + } + } + return true; + } + + /** + * Given a polygon A and a loop B whose boundaries do not cross, returns true if A contains the + * boundary of B. Shared edges are handled according to the rule described in {@link + * #containsNonCrossingBoundary(S2Loop, S2Loop, boolean)}. + */ + private boolean containsNonCrossingBoundary(S2Loop b, boolean bReverse) { + boolean inside = false; + for (S2Loop a : loops) { + inside ^= containsNonCrossingBoundary(a, b, bReverse); + } + return inside; + } + + /** + * Given two loops whose boundaries do not cross (see {@link #compareBoundary(S2Loop)}, returns + * true if A contains the boundary of B. + * + *

        This method is cheaper than compareBoundary() because it does not test for edge + * intersections, but it does trigger building the index if there are edges in both polygons. + * + * @param b the loop to test for containment by this loop; neither loop may be empty or have an + * edge crossing with the other, and if b is full then bReverse must be false. + * @param bReverse If true, the boundary of B is reversed first (which only affects the result + * when there are shared edges). + */ + private boolean containsNonCrossingBoundary(S2Loop a, S2Loop b, boolean bReverse) { + assert (!a.isEmpty() && !b.isEmpty()); + assert (!b.isFull() || !bReverse); + + // The bounds must intersect for containment. + if (!a.getRectBound().intersects(b.getRectBound())) { + return false; + } + + // Full loops are handled as though the loop surrounded the entire sphere. + if (a.isFull()) { + return true; + } + if (b.isFull()) { + return false; + } + + // Return false if the index doesn't have a cell containing b0. + S2Iterator it = index.iterator(); + S2Point b0 = b.vertex(0); + if (!it.locate(b0)) { + return false; + } + S2ClippedShape shape = it.entry().findClipped(a); + if (shape == null) { + return false; + } + + // If b0 is a shared vertex, then check whether the edge (b0, b1) is contained by A. + for (int i = 0; i < shape.numEdges(); i++) { + int edge = shape.edge(i); + if (b0.equalsPoint(a.vertex(edge))) { + return S2Loop.wedgeContainsSemiwedge( + a.vertex((edge == 0 ? a.numVertices() : edge) - 1), + a.vertex(edge), + a.vertex(edge + 1), b.vertex(1), bReverse); + } + } + + // Otherwise check whether A contains b0. + return S2VertexModel.OPEN.shapeContains(it.center(), shape, b.vertex(0)); + } + + /** + * Given two polygons A and B such that the boundary of A does not cross any loop of B, returns + * true if A excludes all shell boundaries of B. + */ + private boolean excludesNonCrossingShells(S2Polygon b) { + for (S2Loop loop : b.loops) { + if (!loop.isHole()) { + if (containsNonCrossingBoundary(loop, false)) { + return false; + } + } + } + return true; + } + + /** + * Given two polygons A and B such that the boundary of A does not cross any loop of B, returns + * true if A excludes all shell boundaries of the complement of B. + */ + private boolean excludesNonCrossingComplementShells(S2Polygon b) { + // Special case to handle the complement of the empty or full polygons. + if (b.isEmpty()) { + return !isFull(); + } + + if (b.isFull()) { + return true; + } + + // Otherwise the complement of B may be obtained by inverting loop(0) and then swapping the + // shell/hole status of all other loops. This implies that the shells of the complement consist + // of loop 0 plus all the holes of the original polygon. + for (int j = 0; j < b.numLoops(); ++j) { + if (j > 0 && !b.loop(j).isHole()) { + continue; + } + + // The interior of the complement is to the right of loop 0, and to the left of the loops that + // were originally holes. + if (containsNonCrossingBoundary(b.loop(j), j == 0)) { + return false; + } + } + + return true; + } + + /** Returns true if any loop contains the given loop. */ + private boolean anyLoopContains(S2Loop b) { + for (S2Loop loop : loops) { + if (loop.contains(b)) { + return true; + } + } + return false; + } + + /** Returns true if any loop intersects the given loop. */ + private boolean anyLoopIntersects(S2Loop b) { + for (S2Loop loop : loops) { + if (loop.intersects(b)) { + return true; + } + } + return false; + } + + /** Returns a human readable representation of the polygon. */ + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("Polygon: (").append(numLoops()).append(") loops:\n"); + for (int i = 0; i < numLoops(); ++i) { + S2Loop s2Loop = loop(i); + sb.append("loop <\n"); + for (int v = 0; v < s2Loop.numVertices(); ++v) { + S2Point s2Point = s2Loop.vertex(v); + sb.append(s2Point.toDegreesString()); + // end of vertex + sb.append("\n"); + } + // end of loop + sb.append(">\n"); + } + return sb.toString(); + } + + /** + * Encodes the polygon into an efficient, lossless binary representation, which can be decoded by + * calling {@link S2Polygon#decode}. The encoding is byte-compatible with the C++ version of the + * S2 library. + * + * @param output The output stream into which the encoding should be written. + * @throws IOException if there was a problem writing into the output stream. + */ + public void encode(OutputStream output) throws IOException { + int level = getNumVertices() == 0 ? S2CellId.MAX_LEVEL : getBestSnapLevel(); + LittleEndianOutput encoder = new LittleEndianOutput(output); + if (level == -1) { + encodeUncompressed(encoder); + } else { + encodeCompressed(level, encoder); + } + } + + /** + * Encodes the polygon into an uncompressed binary representation, which can be decoded by calling + * {@link S2Polygon#decode(InputStream)}. The encoding is byte-compatible with the C++ version of + * the S2 library. + * + * @param encoder The output stream into which the encoding should be written. + * @throws IOException if there was a problem writing into the output stream. + */ + void encodeUncompressed(LittleEndianOutput encoder) throws IOException { + encoder.writeByte(LOSSLESS_ENCODING_VERSION); + // Placeholder value for backward compatibility; previously stored the "owns_loops_" value. + encoder.writeByte((byte) 1); + encoder.writeByte((byte) (hasHoles ? 1 : 0)); + encoder.writeInt(loops.size()); + for (S2Loop loop : loops) { + loop.encode(encoder); + } + bound.encode(encoder); + } + + private void encodeCompressed(int level, LittleEndianOutput encoder) throws IOException { + encoder.writeByte(COMPRESSED_ENCODING_VERSION); + encoder.writeByte((byte) level); + encoder.writeVarint32(numLoops()); + for (int i = 0; i < numLoops(); i++) { + loop(i).encodeCompressed(level, encoder); + } + // Do not write the bound, numVertices, or hasHoles as they can be cheaply recomputed by + // decodeCompressed. Microbenchmarks show the speed difference is inconsequential. + } + + /** + * Decodes a polygon that was encoded using {@link S2Polygon#encode}. + * + *

        This method will never return null. It will either throw an exception or return a valid + * {@link S2Polygon}. + * + * @param input The input stream containing the encoded polygon data. + * @return the decoded {@link S2Polygon}. + * @throws IOException if there was a problem reading from the input stream. + */ + public static S2Polygon decode(InputStream input) throws IOException { + LittleEndianInput decoder = new LittleEndianInput(input); + byte version = decoder.readByte(); + switch (version) { + case LOSSLESS_ENCODING_VERSION: + return decodeLossless(decoder); + case COMPRESSED_ENCODING_VERSION: + return decodeCompressed(decoder); + default: + throw new IOException("Unsupported S2Polygon encoding version " + version); + } + } + + private static S2Polygon decodeLossless(LittleEndianInput decoder) throws IOException { + // Ignore irrelevant serialized owns_loops_ value. + decoder.readByte(); + // Whether or not there are holes -- we can ignore this as it is set by the call later to + // initLoopProperties. + decoder.readByte(); + // Polygons with no loops are explicitly allowed here: a newly created + // polygon has zero loops and such polygons encode and decode properly. + int numLoops = decoder.readInt(); + Preconditions.checkState(numLoops >= 0, "Can only deccode polygons with up to 2^31 - 1 loops"); + S2Polygon result = new S2Polygon(); + for (int i = 0; i < numLoops; i++) { + result.loops.add(S2Loop.decode(decoder)); + } + + // Bound will be recomputed in the next step but we have to read it to consume all the bytes + // in the input stream. + result.bound = S2LatLngRect.decode(decoder); + + // TODO(eengle): Modify initLoopProperties to not compute a bounding box when one has already + // been read from disk. + + // Recompute the other properties, like bound, numVertices and hasHoles. + result.initLoopProperties(); + return result; + } + + private static S2Polygon decodeCompressed(LittleEndianInput decoder) throws IOException { + int level = decoder.readByte(); + if (level > S2CellId.MAX_LEVEL) { + throw new IOException("Invalid level"); + } + // Polygons with no loops are explicitly allowed here: a newly created polygon has zero loops + // and such polygons encode and decode properly. + int numLoops = decoder.readVarint32(); + List loops = new ArrayList<>(numLoops); + for (int i = 0; i < numLoops; i++) { + loops.add(S2Loop.decodeCompressed(level, decoder)); + } + // Instantiating with the S2Polygon(List) constructor might cause loops to get + // reordered, which would break idempotence across platforms. The loops are in exactly the order + // that we want. + S2Polygon result = new S2Polygon(); + result.loops.addAll(loops); + // Recompute the other properties, like bound, numVertices and hasHoles. + result.initLoopProperties(); + return result; + } + + @GwtCompatible(serializable = false) + private static final class LoopVertexIndexPair { + private final int loopIndex; + private final int vertexIndex; + + public LoopVertexIndexPair(int loopIndex, int vertexIndex) { + this.loopIndex = loopIndex; + this.vertexIndex = vertexIndex; + } + + public int getLoopIndex() { + return loopIndex; + } + + public int getVertexIndex() { + return vertexIndex; + } + } + + /** + * EdgeClipper finds all the intersections of a given edge with the edges contained in an + * S2ShapeIndex. It is used to implement polygon operations such as intersection and union. + */ + private static class EdgeClipper { + private final S2EdgeQuery query; + private final boolean addSharedEdges; + private final Predicate reverseEdges; + + /** + * Initialize an EdgeClipper for the given S2ShapeIndex. If the query edge is the same as an + * index edge (a "shared edge"), then the edge will be included in the output if and only if + * {@code addSharedEdges} is true. The {@code reverseEdges} function allows the edges of any + * index shape to be reversed before this test is performed (this is used to reverse the loop + * orientation of "holes" in certain algorithms). + */ + public EdgeClipper( + S2ShapeIndex index, boolean addSharedEdges, Predicate reverseEdges) { + query = new S2EdgeQuery(index); + this.addSharedEdges = addSharedEdges; + this.reverseEdges = reverseEdges; + } + + /** + * Finds all points where the polygon B intersects the edge (a0, a1), and add the corresponding + * parameter values (in the range [0,1]) to {@code intersections}. The result is unsorted. + */ + public void clipEdge(S2Point a0, S2Point a1, List intersections) { + Map edgeMap = query.getCandidates(a0, a1); + if (edgeMap.isEmpty()) { + return; + } + + // Iterate through the candidate loops, and then the candidate edges within each loop. + S2EdgeUtil.EdgeCrosser crosser = new S2EdgeUtil.EdgeCrosser(a0, a1); + MutableEdge result = new MutableEdge(); + for (Entry entry : edgeMap.entrySet()) { + final S2Shape bShape = entry.getKey(); + S2EdgeQuery.Edges edges = entry.getValue(); + int b1Prev = -2; + while (!edges.isEmpty()) { + int edge = edges.nextEdge(); + bShape.getEdge(edge, result); + if (edge != b1Prev + 1) { + crosser.restartAt(result.getStart()); + } + int crossing = crosser.robustCrossing(result.getEnd()); + if (crossing >= 0) { + addIntersection( + a0, a1, result.getStart(), result.getEnd(), bShape, crossing, intersections); + b1Prev = edge; + } + } + } + } + + /** + * Given two edges A and B such that robustCrossing(A, B) >= 0, determines if they intersect and + * adds any intersection point to {@code intersections}. {@code bShape} is the S2Shape + * containing edge B, and {@code crossing} is the result of {@code robustCrossing(A, B)}. + */ + private void addIntersection( + S2Point a0, + S2Point a1, + S2Point b0, + S2Point b1, + S2Shape bShape, + int crossing, + List intersections) { + // assert (crossing >= 0); + if (crossing > 0) { + // There is a proper edge crossing. + S2Point x = S2EdgeUtil.getIntersection(a0, a1, b0, b1); + double t = S2EdgeUtil.getDistanceFraction(x, a0, a1); + intersections.add(new ParametrizedS2Point(t, x)); + } else if (S2EdgeUtil.vertexCrossing(a0, a1, b0, b1)) { + // There is a crossing at one of the vertices. The basic rule is simple: if a0 equals one of + // the 'b' vertices, the crossing occurs at t = 0; otherwise, it occurs at t = 1. + // + // This has the effect that when two reversed edges exist (i.e., a0 == b1 and a1 == b0) and + // each edge is clipped against the other, neither one is included in the output (which is + // correct). However, when two shared edges exist (i.e., a0 == b0 and a1 == b1), both are + // included in the output (which is incorrect). The 'addSharedEdges' flag gives explicit + // over whether these shared edges are included in the output; if it is false, shared edges + // are excluded by changing their intersection parameter from 0 to 1. This allows exactly + // one copy of shared edges to be preserved, by calling ClipBoundary() twice with opposite + // values of the 'addSharedEdges' flag. + double t = (a0.equalsPoint(b0) || a0.equalsPoint(b1)) ? 0 : 1; + if (!addSharedEdges && a1.equalsPoint(reverseEdges.apply(bShape) ? b0 : b1)) { + // Excludes (a0, a1) from the output. + t = 1; + } + intersections.add(new ParametrizedS2Point(t, t == 0 ? a0 : a1)); + } + } + } + + /** Returns a shape wrapping this polygon. */ + public Shape shape() { + if (numLoops() > Shape.MAX_LINEAR_SEARCH_LOOPS) { + return binarySearchShape(); + } else { + return linearSearchShape(); + } + } + + /** + * Returns an implementation that does a binary search to map an edge to one of a large number of + * loops. + */ + Shape binarySearchShape() { + final int[] cumulativeEdges = new int[numLoops()]; + int numEdges = 0; + for (int i = 0; i < loops.size(); i++) { + cumulativeEdges[i] = numEdges; + numEdges += loops.get(i).numVertices(); + } + return new Shape() { + private static final long serialVersionUID = 1L; + + @Override + public void getEdge(int edgeId, MutableEdge result) { + int start = Arrays.binarySearch(cumulativeEdges, edgeId); + if (start < 0) { + // Found a position just beyond the loop we want. + start = -start - 2; + } + S2Loop loop = loop(start); + edgeId -= cumulativeEdges[start]; + result.set(loop.orientedVertex(edgeId), loop.orientedVertex(edgeId + 1)); + } + + @Override + public int getChainStart(int chainId) { + Preconditions.checkElementIndex(chainId, numChains()); + return cumulativeEdges[chainId]; + } + }; + } + + /** + * Returns an implementation that does a linear search to map an edge to one of a number of loops. + * + *

        When the number of loops is small, linear search is faster. Most often there is exactly one + * loop and the getEdge and getChainStart loops execute zero times. + */ + Shape linearSearchShape() { + return new Shape() { + private static final long serialVersionUID = 1L; + + @Override + public void getEdge(int edgeId, MutableEdge result) { + S2Loop loop = loop(0); + for (int i = 1; edgeId >= loop.numVertices(); loop = loop(i++)) { + edgeId -= loop.numVertices(); + } + result.set(loop.orientedVertex(edgeId), loop.orientedVertex(edgeId + 1)); + } + + @Override + public int getChainStart(int chainId) { + Preconditions.checkElementIndex(chainId, numChains()); + int start = 0; + for (int i = 0; i < chainId; i++) { + start += loops.get(i).numVertices(); + } + return start; + } + }; + } + + /** + * Wrapper class for indexing a polygon via {@link S2ShapeIndex}. This class has several subtypes + * to store additional data based on the kind of polygon being indexed. + * + *

        Note that unlike S2Polygon, the edges of this shape are directed such that the polygon + * interior is always on the left. + */ + public abstract class Shape implements S2Shape, Serializable { + // TODO measure this with benchmark. + private static final int MAX_LINEAR_SEARCH_LOOPS = 5; // From benchmarks. + private static final long serialVersionUID = 1L; + + public S2Polygon polygon() { + return S2Polygon.this; + } + + @Override + public int numEdges() { + return isFull() ? 0 : numVertices; + } + + @Override + public boolean hasInterior() { + return true; + } + + @Override + public boolean containsOrigin() { + boolean containsOrigin = false; + for (S2Loop loop : loops) { + containsOrigin ^= loop.containsOrigin(); + } + return containsOrigin; + } + + @Override + public int numChains() { + return numLoops(); + } + + @Override + public int getChainLength(int chainId) { + Preconditions.checkElementIndex(chainId, numChains()); + return isFull() ? 0 : loops.get(chainId).numVertices(); + } + + @Override + public void getChainEdge(int chainId, int offset, MutableEdge result) { + Preconditions.checkElementIndex(offset, getChainLength(chainId)); + S2Loop loop = loop(chainId); + result.set(loop.orientedVertex(offset), loop.orientedVertex(offset + 1)); + } + + @Override + public S2Point getChainVertex(int chainId, int edgeOffset) { + Preconditions.checkElementIndex(edgeOffset, getChainLength(chainId) + 1); + S2Loop loop = loop(chainId); + return loop.orientedVertex(edgeOffset); + } + + @Override + public int dimension() { + return 2; + } + } +} diff --git a/core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/S2PolygonBuilder.java b/core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/S2PolygonBuilder.java new file mode 100644 index 0000000000..1bd6a95545 --- /dev/null +++ b/core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/S2PolygonBuilder.java @@ -0,0 +1,929 @@ +/* + * Copyright 2006 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.mogo.eagle.core.utilcode.geometry; + +import static com.mogo.eagle.core.utilcode.geometry.S2Projections.PROJ; + +import com.google.common.annotations.GwtCompatible; +import com.google.common.base.Preconditions; +import com.google.common.collect.HashMultiset; +import com.google.common.collect.Lists; +import com.google.common.collect.Maps; +import com.google.common.collect.Multiset; +import com.mogo.eagle.core.utilcode.geometry.S2ClosestPointQuery.Result; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Stack; +import javax.annotation.Nullable; + +/** + * This is a simple class for assembling polygons out of edges. It requires that no two edges cross. + * It can handle both directed and undirected edges, and, optionally, it can remove duplicate-edge + * pairs (consisting of two identical edges or an edge and its reverse edge). This is useful for + * computing seamless unions of polygons that have been cut into pieces. + * + *

        Some of the situations this class was designed to handle: + * + *

          + *
        1. Computing the union of disjoint polygons that may share part of their boundaries. For + * example, reassembling a lake that has been split into two loops by a state boundary. + *
        2. Constructing polygons from input data that do not follow S2 conventions, i.e., where loops + * may have repeated vertices, or distinct loops may share edges, or shells and holes having + * opposite or unspecified orientations. + *
        3. Computing the symmetric difference of a set of polygons whose edges intersect only at + * vertices. This can be used to implement a limited form of polygon intersection or + * subtraction as well as unions. + *
        4. As a tool for implementing other polygon operations by generating a collection of directed + * edges and then assembling them into loops. + *
        + * + * Caveat: Because S2PolygonBuilder only works with polygon boundaries, it cannot distinguish + * between the empty and full polygon. If your application can generate both the empty and full + * polygons, you must implement logic outside of this class (see {@link #assemblePolygon()}). + * + */ +@GwtCompatible(serializable = false) +public final strictfp class S2PolygonBuilder { + private final Options options; + + /** + * The current set of edges, grouped by origin. The set of destination vertices is a multiset so + * that the same edge can be present more than once. + */ + private final Map> edges = Maps.newHashMap(); + + /** + * Unique collection of the starting (first) vertex of all edges, in the order they are added to + * {@code edges}. + */ + private final List startingVertices = Lists.newArrayList(); + + /** Default constructor for well-behaved polygons. Uses the DIRECTED_XOR options. */ + public S2PolygonBuilder() { + this(Options.DIRECTED_XOR); + } + + public S2PolygonBuilder(Options options) { + this.options = options; + } + + /** + * Options for initializing a {@link S2PolygonBuilder}. Choose one of the predefined options, or + * use a {@link Builder} to construct a new one. + * + *

        Examples: + * + *

        {@code
        +   * S2PolygonBuilder polygonBuilder = new S2PolygonBuilder(
        +   *     S2PolygonBuilder.Options.UNDIRECTED_XOR);
        +   *
        +   * S2PolygonBuilder.Options options =
        +   *     S2PolygonBuilder.Options.DIRECTED_XOR.toBuilder()
        +   *         .setMergeDistance(vertexMergeRadius)
        +   *         .build();
        +   * S2PolygonBuilder polygonBuilder = new S2PolygonBuilder(options);
        +   *
        +   * S2PolygonBuilder.Options options =
        +   *     S2PolygonBuilder.Options.builder()
        +   *         .setUndirectedEdges(false)
        +   *         .setXorEdges(true)
        +   *         .setMergeDistance(vertexMergeRadius)
        +   *         .build();
        +   * S2PolygonBuilder polygonBuilder = new S2PolygonBuilder(options);
        +   * }
        + */ + public static final class Options { + /** + * These are the options that should be used for assembling well-behaved input data into + * polygons. All edges should be directed such that "shells" and "holes" have opposite + * orientations (typically CCW shells and clockwise holes), unless it is known that shells and + * holes do not share any edges. + */ + public static final Options DIRECTED_XOR = + builder().setUndirectedEdges(false).setXorEdges(true).build(); + + /** + * These are the options that should be used for assembling polygons that do not follow the + * conventions above, e.g., where edge directions may vary within a single loop, or shells and + * holes are not oppositely oriented. + */ + public static final Options UNDIRECTED_XOR = + builder().setUndirectedEdges(true).setXorEdges(true).build(); + + /** + * These are the options that should be used for assembling edges where the desired output is a + * collection of loops rather than a polygon, and edges may occur more than once. Edges are + * treated as undirected and are not XORed together, in particular, adding edge A->B also adds + * B->A. + */ + public static final Options UNDIRECTED_UNION = + builder().setUndirectedEdges(true).setXorEdges(false).build(); + + /** + * Finally, select this option when the desired output is a collection of loops rather than a + * polygon, but your input edges are directed and you do not want reverse edges to be added + * implicitly as above. + */ + public static final Options DIRECTED_UNION = + builder().setUndirectedEdges(false).setXorEdges(false).build(); + + // See get methods below for documentation of these fields + private final boolean undirectedEdges; + private final boolean xorEdges; + private final boolean validate; + private final S1Angle mergeDistance; + private final boolean snapToCellCenters; + private final double edgeSpliceFraction; + + /** Private constructor called by the {@link Builder}. */ + private Options(Builder builder) { + this.undirectedEdges = builder.undirectedEdges; + this.xorEdges = builder.xorEdges; + this.validate = builder.validate; + this.mergeDistance = builder.mergeDistance; + this.snapToCellCenters = builder.snapToCellCenters; + this.edgeSpliceFraction = builder.edgeSpliceFraction; + } + + /** + * Static factory method for returning a new options {@link Builder} with default settings, + * which is equivalent to {@link #DIRECTED_XOR}. + */ + public static Builder builder() { + return new Builder(); + } + + /** + * Returns a new {@link Builder} with the same settings as the current options. Use this to + * create a new {@link Options} based on the current options. + */ + public Builder toBuilder() { + return new Builder() + .setUndirectedEdges(undirectedEdges) + .setXorEdges(xorEdges) + .setValidate(validate) + .setMergeDistance(mergeDistance) + .setSnapToCellCenters(snapToCellCenters) + .setEdgeSpliceFraction(edgeSpliceFraction); + } + + /** + * If this returns false, the input is assumed to consist of edges that can be assembled into + * oriented loops without reversing any of the edges. Otherwise, use {@link + * Builder#setUndirectedEdges} to set this attribute to true when building the options. + */ + public boolean getUndirectedEdges() { + return undirectedEdges; + } + + /** + * If {@code xorEdges} is true, any duplicate edge pairs are removed. This is useful for + * computing the union of a collection of polygons whose interiors are disjoint but whose + * boundaries may share some common edges (e.g., computing the union of South Africa, Lesotho, + * and Swaziland). + * + *

        Note that for directed edges, a "duplicate edge pair" consists of an edge and its + * corresponding reverse edge. This means that either (a) "shells" and "holes" must have + * opposite orientations, or (b) shells and holes do not share edges. + * + *

        There are only two reasons to turn off {@code xorEdges} (via {@link Builder#setXorEdges}): + * + *

          + *
        1. {@link #assemblePolygon} will be called, and you want to assert that there are no + * duplicate edge pairs in the input. + *
        2. {@link #assembleLoops} will be called, and you want to keep abutting loops separate in + * the output, rather than merging their regions together (e.g., assembling loops for + * Kansas City, KS and Kansas City, MO simultaneously). + *
        + */ + public boolean getXorEdges() { + return xorEdges; + } + + /** + * If true, {@link S2Loop#isValid} is called on all loops and polygons before constructing them. + * If any loop is invalid (e.g., self-intersecting), it is rejected and returned as a set of + * "unused edges". Any remaining valid loops are kept. If the entire polygon is invalid (e.g., + * two loops intersect), then all edges are rejected and returned as unused edges. See {@link + * Builder#setValidate}. + * + *

        Default value: false + */ + public boolean getValidate() { + return validate; + } + + /** + * If set to a positive value, all vertex pairs that are separated by less than this distance + * will be merged together. Note that vertices can move arbitrarily far if there is a long chain + * of vertices separated by less than this distance. + * + *

        Setting this to a positive value is useful for assembling polygons out of input data where + * vertices and/or edges may not be perfectly aligned. See {@link Builder#setMergeDistance}. + * + *

        Default value: 0 + */ + public S1Angle getMergeDistance() { + return mergeDistance; + } + + /** + * If true, the built polygon will have its vertices snapped to the centers of s2 cells at the + * smallest level number such that no vertex will move by more than the robustness radius. If + * the robustness radius is smaller than half the leaf cell diameter, no snapping will occur. + * This is useful because snapped polygons can be {@code Encode()}d using less space. See {@code + * S2EncodePointsCompressed} in C++. + * + *

        Default value: false + */ + public boolean getSnapToCellCenters() { + return snapToCellCenters; + } + + /** + * Returns the edge splice fraction, which defaults to 0.866 (approximately {@code sqrt(3)/2}). + * + *

        The edge splice radius is automatically set to this fraction of the vertex merge radius. + * If the edge splice radius is positive, then all vertices that are closer than this distance + * to an edge are spliced into that edge. Note that edges can move arbitrarily far if there is a + * long chain of vertices in just the right places. + * + *

        You can turn off edge splicing by setting this value to zero; see {@link + * Builder#setEdgeSpliceFraction}. This will save some time if you don't need this feature, or + * you don't want vertices to be spliced into nearby edges for some reason. + * + *

        Note that the edge splice fraction must be less than {@code sqrt(3)/2} in order to avoid + * infinite loops in the merge algorithm. The default value is very close to this maximum and + * therefore results in the maximum amount of edge splicing for a given vertex merge radius. + * + *

        The only reason to reduce the edge splice fraction is if you want to limit changes in edge + * direction due to splicing. The direction of an edge can change by up to {@code + * asin(edgeSpliceFraction)} due to each splice. Thus, by default, edges are allowed to change + * direction by up to 60 degrees per splice. However, note that most direction changes are much + * smaller than this: the worst case occurs only if the vertex being spliced is just outside the + * vertex merge radius from one of the edge endpoints. + */ + public double getEdgeSpliceFraction() { + return edgeSpliceFraction; + } + + /** + * Returns robustness radius computed from {@code mergeDistance} and {@code edgeSpliceFraction}. + * + *

        The lossless way to serialize a polygon takes 24 bytes per vertex (3 doubles). If you want + * a representation with fewer bits, you need to snap your vertices to a grid. If a vertex is + * very close to an edge, the snapping operation can lead to self-intersection. The {@code + * edgeSpliceFraction} cannot be zero to have a robustness guarantee. See {@link + * Builder#setRobustnessRadius}. + */ + public S1Angle getRobustnessRadius() { + return S1Angle.radians(mergeDistance.radians() * edgeSpliceFraction / 2.0); + } + + /** + * If {@code snapToCellCenters} is true, returns the minimum level at which snapping a point to + * the center of a cell at that level will move the point by no more than the robustness radius. + * Returns -1 if there is no such level, or if {@code snapToCellCenters} is false. + */ + public int getSnapLevel() { + if (!getSnapToCellCenters()) { + return -1; + } + + final S1Angle tolerance = getRobustnessRadius(); + // The distance from a point in the cell to the center is at most {@code maxDiag / 2}. See + // the comment for {@link S2Projections.maxDiag}. + // + // TODO(user): Verify and understand this. [todo copied from C++] + final int level = PROJ.maxDiag.getMinLevel(tolerance.radians() * 2.0); + + // getMinLevel will return MAX_LEVEL even if the max level does not satisfy the condition. + if (level == S2CellId.MAX_LEVEL + && tolerance.radians() < (PROJ.maxDiag.getValue(level) / 2.0)) { + return -1; + } + + return level; + } + + /** Builder class for {@link Options}. */ + public static class Builder { + private boolean undirectedEdges = false; + private boolean xorEdges = true; + private boolean validate = false; + private S1Angle mergeDistance = S1Angle.radians(0); + private boolean snapToCellCenters = false; + private double edgeSpliceFraction = 0.866; + + /** + * Constructs a new builder with default values, which is equivalent to {@link + * Options#DIRECTED_XOR}. + */ + public Builder() {} + + /** Builds and returns a new (immutable) instance of {@link Options}. */ + public Options build() { + return new Options(this); + } + + /** + * Sets whether edges are undirected. See {@link Options#getUndirectedEdges}. + * + *

        Default: false + */ + public Builder setUndirectedEdges(boolean undirectedEdges) { + this.undirectedEdges = undirectedEdges; + return this; + } + + /** + * Sets whether duplicated edges will be collapsed. See {@link Options#getXorEdges}. + * + *

        Default: true + */ + public Builder setXorEdges(boolean xorEdges) { + this.xorEdges = xorEdges; + return this; + } + + /** + * Sets whether {@link S2Loop#isValid} is called for all loops. See {@link + * Options#getValidate}. + * + *

        Default: false + */ + public Builder setValidate(boolean validate) { + this.validate = validate; + return this; + } + + /** + * Sets the threshold angle at which to merge vertex pairs. See {@link + * Options#getMergeDistance}. + * + *

        Default value: 0. + */ + public Builder setMergeDistance(S1Angle mergeDistance) { + this.mergeDistance = mergeDistance; + return this; + } + + /** + * Sets whether a polygon will snap its vertices to the centers of s2 cells at the smallest + * level number such that no vertex will move by more than the robustness radius. If the + * robustness radius is smaller than half the leaf cell diameter, no snapping will occur. See + * {@link Options#getSnapToCellCenters()}. + * + *

        Default value: false + */ + public Builder setSnapToCellCenters(boolean snapToCellCenters) { + this.snapToCellCenters = snapToCellCenters; + return this; + } + + /** + * Sets the threshold radius at which vertex are spliced into an edge. See {@link + * Options#getEdgeSpliceFraction}. Must be at least {@code sqrt(3) / 2}. + * + *

        Default value: 0.866 + */ + public Builder setEdgeSpliceFraction(double edgeSpliceFraction) { + Preconditions.checkState( + edgeSpliceFraction < Math.sqrt(3) / 2, + "Splice fraction must be at least sqrt(3)/2 to ensure termination " + + "of edge splicing algorithm."); + this.edgeSpliceFraction = edgeSpliceFraction; + return this; + } + + /** + * Sets {@code mergeDistance} computed from robustness radius and edge splice fraction. The + * {@code edgeSpliceFraction} cannot be zero to have a robustness guarantee. + * + *

        See {@link Options#getRobustnessRadius}. To guarantee that a polygon remains valid when + * its vertices are moved by an angle of up to epsilon, you need {@code mergeDistance * + * edgeSpliceFraction >= 2 * epsilon}, as the edge and the vertex can each move by epsilon + * upon snapping. + * + *

        If your grid has maximum diameter {@code d}, call {@code setRobustnessRadius(d/2)}. + */ + public Builder setRobustnessRadius(S1Angle robustnessRadius) { + this.mergeDistance = S1Angle.radians(2.0 * robustnessRadius.radians() / edgeSpliceFraction); + return this; + } + } + } + + public Options options() { + return options; + } + + /** + * Adds the given edge to the polygon builder and returns true if the edge was actually added to + * the edge graph. + * + *

        This method should be used for input data that may not follow S2 polygon conventions. Note + * that edges are not allowed to cross each other. Also note that as a convenience, edges where v0 + * == v1 are ignored. + */ + public boolean addEdge(S2Point v0, S2Point v1) { + if (v0.equalsPoint(v1)) { + return false; + } + + // If xorEdges is true, we look for an existing edge in the opposite + // direction. We either delete that edge or insert a new one. + if (options.getXorEdges() && hasEdge(v1, v0)) { + eraseEdge(v1, v0); + return false; + } + + if (edges.get(v0) == null) { + edges.put(v0, HashMultiset.create()); + startingVertices.add(v0); + } + + edges.get(v0).add(v1); + if (options.getUndirectedEdges()) { + if (edges.get(v1) == null) { + edges.put(v1, HashMultiset.create()); + startingVertices.add(v1); + } + edges.get(v1).add(v0); + } + + return true; + } + + /** + * Adds all edges in the given loop. If the {@code sign()} of the loop is negative (i.e., this + * loop represents a hole), the reverse edges are added instead. This implies that "shells" are + * CCW and "holes" are CW, as required for the directed edges convention described above. + * + *

        This method does not take ownership of the loop. + */ + public void addLoop(S2Loop loop) { + // Only add loops that have edges to add. + if (!loop.isEmptyOrFull()) { + int sign = loop.sign(); + for (int i = loop.numVertices(); i > 0; --i) { + // Vertex indices need to be in the range [0, 2*num_vertices()-1]. + addEdge(loop.vertex(i), loop.vertex(i + sign)); + } + } + } + + /** + * Add all loops in the given polygon. Shells and holes are added with opposite orientations as + * described for {@link #addLoop(S2Loop)}. Note that this method does not distinguish between the + * empty and full polygons, i.e. adding a full polygon has the same effect as adding an empty one. + */ + public void addPolygon(S2Polygon polygon) { + for (int i = 0; i < polygon.numLoops(); ++i) { + addLoop(polygon.loop(i)); + } + } + + /** + * Returns a new point, snapped to the center of the cell containing the given point at the + * specified level. + */ + private S2Point snapPointToLevel(final S2Point p, int level) { + return S2CellId.fromPoint(p).parent(level).toPoint(); + } + + /** + * Returns a new loop where the vertices of the given loop have been snapped to the centers of + * cells at the specified level. + */ + private S2Loop snapLoopToLevel(final S2Loop loop, int level) { + List snappedVertices = Lists.newArrayListWithCapacity(loop.numVertices()); + for (int i = 0; i < loop.numVertices(); i++) { + snappedVertices.add(snapPointToLevel(loop.vertex(i), level)); + } + return new S2Loop(snappedVertices); + } + + /** + * Assembles the given edges into as many non-crossing loops as possible. When there is a choice + * about how to assemble the loops, then CCW loops are preferred. Returns true if all edges were + * assembled. If {@code unusedEdges} is not null, it is initialized to the set of edges that could + * not be assembled into loops. + * + *

        Note that if {@link Options#getXorEdges} returns false and duplicate edge pairs may be + * present, then use {@link Options.Builder#setUndirectedEdges} to set it to true, unless all + * loops can be assembled in a counter-clockwise direction. Otherwise this method may not be able + * to assemble all loops due to its preference for CCW loops. + * + *

        This method resets the {@link S2PolygonBuilder} state so that it can be reused. + */ + public boolean assembleLoops(List loops, @Nullable List unusedEdges) { + if (options.getMergeDistance().radians() > 0) { + S1Angle mergeDistance = options.getMergeDistance(); + Map mergeMap = buildMergeMap(mergeDistance); + moveVertices(mergeMap); + double spliceFraction = options.getEdgeSpliceFraction(); + if (spliceFraction > 0) { + spliceEdges(S1Angle.radians(mergeDistance.radians() * spliceFraction)); + } + } + + final int snapLevel = options.getSnapLevel(); + + // We repeatedly choose an edge and attempt to assemble a loop + // starting from that edge. (This is always possible unless the + // input includes extra edges that are not part of any loop.) To + // maintain a consistent scanning order over edges between + // different machine architectures (e.g. 'clovertown' vs. 'opteron'), + // we follow the order they were added to the builder. + if (unusedEdges != null) { + unusedEdges.clear(); + } + for (int i = 0; i < startingVertices.size(); ) { + S2Point v0 = startingVertices.get(i); + Multiset candidates = edges.get(v0); + if (candidates == null) { + i++; + continue; + } + S2Point v1 = candidates.iterator().next(); + S2Loop loop = assembleLoop(v0, v1, unusedEdges); + if (loop != null) { + eraseLoop(loop, loop.numVertices()); + + if (snapLevel >= 0) { + // TODO(user): Change AssembleLoop to return a List, then optionally snap + // that before constructing the loop. This would prevent us from constructing two + // loops. [todo copied from C++] + loop = snapLoopToLevel(loop, snapLevel); + } + + loops.add(loop); + } + } + startingVertices.clear(); + return unusedEdges == null || unusedEdges.isEmpty(); + } + + /** + * Like AssembleLoops, but then assembles the loops into a polygon. If the edges were directed, + * then it is expected that holes and shells will have opposite orientations, and the polygon + * interior is to the left of all edges. If the edges were undirected, then all loops are first + * normalized so that they enclose at most half of the sphere, and the polygon interior consists + * of points enclosed by an odd number of loops. + * + *

        For this method to succeed, there should be no duplicate edges in the input. If this is not + * known to be true, then the "xor_edges" option should be set (which is true by default). + * + *

        Note that because the polygon is constructed from its boundary, this method cannot + * distinguish between the empty and full polygons. An empty boundary always yields an empty + * polygon. If the result should sometimes be the full polygon, such logic must be implemented + * outside of this class (and will need to consider factors other than the polygon's boundary). + * For example, it is often possible to estimate the polygon area. + */ + public boolean assemblePolygon(S2Polygon polygon, @Nullable List unusedEdges) { + List loops = Lists.newArrayList(); + boolean success = assembleLoops(loops, unusedEdges); + if (options.getValidate() && !S2Polygon.isValid(loops) && unusedEdges != null) { + for (S2Loop loop : loops) { + rejectLoop(loop, loop.numVertices(), unusedEdges); + } + return false; + } + if (options.getUndirectedEdges()) { + // If edges are undirected, then all loops are already normalized (i.e., + // contain at most half the sphere). This implies that no loop contains + // the complement of any other loop, and therefore we can call the normal + // Init() method. + polygon.init(loops); + } else { + // If edges are directed, then shells and holes have opposite orientations + // such that the polygon interior is always on their left-hand side. + polygon.initOriented(loops); + } + return success; + } + + /** Convenience method for when you don't care about unused edges. */ + public S2Polygon assemblePolygon() { + S2Polygon polygon = new S2Polygon(); + assemblePolygon(polygon, null); + return polygon; + } + + private void eraseEdge(S2Point v0, S2Point v1) { + // Note that there may be more than one copy of an edge if we are not XORing + // them, so a VertexSet is a multiset. + + Multiset vset = edges.get(v0); + // assert (vset.count(v1) > 0); + vset.remove(v1); + if (vset.isEmpty()) { + edges.remove(v0); + } + + if (options.getUndirectedEdges()) { + vset = edges.get(v1); + // assert (vset.count(v0) > 0); + vset.remove(v0); + if (vset.isEmpty()) { + edges.remove(v1); + } + } + } + + private void eraseLoop(List v, int n) { + for (int i = n - 1, j = 0; j < n; i = j++) { + eraseEdge(v.get(i), v.get(j)); + } + } + + private void eraseLoop(S2Loop v, int n) { + for (int i = n - 1, j = 0; j < n; i = j++) { + eraseEdge(v.vertex(i), v.vertex(j)); + } + } + + /** + * We start at the given edge and assemble a loop taking left turns whenever possible. We stop the + * loop as soon as we encounter any vertex that we have seen before *except* for the first vertex + * (v0). This ensures that only CCW loops are constructed when possible. + */ + private S2Loop assembleLoop(S2Point v0, S2Point v1, @Nullable List unusedEdges) { + // The path so far. + List path = Lists.newArrayList(); + + // Maps a vertex to its index in "path". + Map index = Maps.newHashMap(); + path.add(v0); + path.add(v1); + + index.put(v1, 1); + + while (path.size() >= 2) { + // Note that "v0" and "v1" become invalid if "path" is modified. + v0 = path.get(path.size() - 2); + v1 = path.get(path.size() - 1); + + S2Point v2 = null; + boolean v2Found = false; + Multiset vset = edges.get(v1); + if (vset != null) { + for (S2Point v : vset) { + // We prefer the leftmost outgoing edge, ignoring any reverse edges. + if (v.equalsPoint(v0)) { + continue; + } + if (!v2Found || S2Predicates.orderedCCW(v0, v2, v, v1)) { + v2 = v; + } + v2Found = true; + } + } + if (!v2Found) { + // We've hit a dead end. Remove this edge and backtrack. + if (unusedEdges != null) { + unusedEdges.add(new S2Edge(v0, v1)); + } + eraseEdge(v0, v1); + index.remove(v1); + path.remove(path.size() - 1); + } else if (index.get(v2) == null) { + // This is the first time we've visited this vertex. + index.put(v2, path.size()); + path.add(v2); + } else { + // We've completed a loop. In a simple case last edge is the same as the + // first one (since we did not add the very first vertex to the index we + // would not know that the loop is completed till the second vertex is + // examined). In this case we just remove the last edge to preserve the + // original vertex order. In a more complicated case the edge that + // closed the loop is different and we should remove initial vertices + // that are not part of the loop. + if (index.get(v2) == 1 && path.get(0).equalsPoint(path.get(path.size() - 1))) { + path.remove(path.size() - 1); + } else { + // We've completed a loop. Throw away any initial vertices that + // are not part of the loop. + path = path.subList(index.get(v2), path.size()); + } + + S2Loop loop = new S2Loop(path); + if (options.getValidate() && !loop.isValid()) { + // We've constructed a loop that crosses itself, which can only happen + // if there is bad input data. Throw away the whole loop. + if (unusedEdges != null) { + rejectLoop(path, path.size(), unusedEdges); + } + eraseLoop(path, path.size()); + return null; + } + + // In the case of undirected edges, we may have assembled a clockwise + // loop while trying to assemble a CCW loop. To fix this, we assemble + // a new loop starting with an arbitrary edge in the reverse direction. + // This is guaranteed to assemble a loop that is interior to the previous + // one and will therefore eventually terminate. + if (options.getUndirectedEdges() && !loop.isNormalized()) { + return assembleLoop(path.get(1), path.get(0), unusedEdges); + } + + return loop; + } + } + return null; + } + + /** Erases all edges of the given loop and marks them as unused. */ + private void rejectLoop(S2Loop v, int n, List unusedEdges) { + for (int i = n - 1, j = 0; j < n; i = j++) { + unusedEdges.add(new S2Edge(v.vertex(i), v.vertex(j))); + } + } + + /** Marks all edges of the given loop as unused. */ + private void rejectLoop(List v, int n, List unusedEdges) { + for (int i = n - 1, j = 0; j < n; i = j++) { + unusedEdges.add(new S2Edge(v.get(i), v.get(j))); + } + } + + /** Moves a set of vertices from old to new positions. */ + private void moveVertices(Map mergeMap) { + if (mergeMap.isEmpty()) { + return; + } + + // We need to copy the set of edges affected by the move, since + // this.edges could be reallocated when we start modifying it. + List edgesCopy = Lists.newArrayList(); + for (Map.Entry> edge : this.edges.entrySet()) { + S2Point v0 = edge.getKey(); + Multiset vset = edge.getValue(); + for (S2Point v1 : vset) { + if (mergeMap.get(v0) != null || mergeMap.get(v1) != null) { + + // We only need to modify one copy of each undirected edge. + if (!options.getUndirectedEdges() || v0.lessThan(v1)) { + edgesCopy.add(new S2Edge(v0, v1)); + } + } + } + } + + // Now erase all the old edges, and add all the new edges. This will + // automatically take care of any XORing that needs to be done, because + // EraseEdge also erases the sibiling of undirected edges. + for (int i = 0; i < edgesCopy.size(); ++i) { + S2Point v0 = edgesCopy.get(i).getStart(); + S2Point v1 = edgesCopy.get(i).getEnd(); + eraseEdge(v0, v1); + S2Point new0 = mergeMap.get(v0); + if (new0 != null) { + v0 = new0; + } + S2Point new1 = mergeMap.get(v1); + if (new1 != null) { + v1 = new1; + } + addEdge(v0, v1); + } + } + + /** Returns an index of all the points. */ + private S2PointIndex index() { + S2PointIndex index = new S2PointIndex<>(); + for (Map.Entry> edge : edges.entrySet()) { + index.add(edge.getKey(), null); + for (S2Point v : edge.getValue().elementSet()) { + index.add(v, null); + } + } + return index; + } + + /** + * Clusters vertices that are separated by at most {@link Options#getMergeDistance} and returns a + * map of each one to a single representative vertex for all the vertices in the cluster. + */ + private Map buildMergeMap(S1Angle snapDistance) { + // The overall strategy is to start from each vertex and grow a maximal + // cluster of mergeable vertices. In graph theoretic terms, we find the + // connected components of the undirected graph whose edges connect pairs of + // vertices that are separated by at most vertex_merge_radius(). + // + // We then choose a single representative vertex for each cluster, and + // update "merge_map" appropriately. We choose an arbitrary existing + // vertex rather than computing the centroid of all the vertices to avoid + // creating new vertex pairs that need to be merged. (We guarantee that all + // vertex pairs are separated by at least the merge radius in the output.) + + // Next, we loop through all the vertices and attempt to grow a maximal + // mergeable group starting from each vertex. + Map mergeMap = Maps.newHashMap(); + Stack frontier = new Stack(); + List> mergeable = Lists.newArrayList(); + S2PointIndex index = index(); + S2ClosestPointQuery query = new S2ClosestPointQuery<>(index); + query.setMaxDistance(snapDistance); + for (S2Iterator> it = index.iterator(); !it.done(); it.next()) { + // Skip any vertices that have already been merged with another vertex. + S2Point vstart = it.entry().point(); + if (mergeMap.containsKey(vstart)) { + continue; + } + + // Grow a maximal mergeable component starting from "vstart", the + // canonical representative of the mergeable group. + frontier.add(vstart); + while (!frontier.isEmpty()) { + // Pop the top frontier point and get all points nearby + mergeable.clear(); + query.findClosestPoints(mergeable, frontier.pop()); + for (Result result : mergeable) { + S2Point vnear = result.entry().point(); + if (!mergeMap.containsKey(vnear) && !vstart.equalsPoint(vnear)) { + frontier.push(vnear); + mergeMap.put(vnear, vstart); + } + } + } + } + + return mergeMap; + } + + /** Returns true if the given directed edge [v0 -> v1] is present in the directed edge graph. */ + public boolean hasEdge(S2Point v0, S2Point v1) { + Multiset vset = edges.get(v0); + return vset == null ? false : vset.count(v1) > 0; + } + + /** Splices vertices that are near an edge onto the edge. */ + private void spliceEdges(S1Angle spliceDistance) { + // We keep a stack of unprocessed edges. Initially all edges are + // pushed onto the stack. + List pendingEdges = Lists.newArrayList(); + for (Map.Entry> edge : edges.entrySet()) { + S2Point v0 = edge.getKey(); + for (S2Point v1 : edge.getValue().elementSet()) { + // We only need to modify one copy of each undirected edge. + if (!options.getUndirectedEdges() || v0.compareTo(v1) < 0) { + pendingEdges.add(new S2Edge(v0, v1)); + } + } + } + + // For each edge, we check whether there are any nearby vertices that should + // be spliced into it. If there are, we choose one such vertex, split + // the edge into two pieces, and iterate on each piece. + S2ClosestPointQuery query = new S2ClosestPointQuery<>(index()); + query.setMaxDistance(spliceDistance); + List> results = new ArrayList<>(); + while (!pendingEdges.isEmpty()) { + // Must remove last edge before pushing new edges. + S2Edge lastPair = pendingEdges.remove(pendingEdges.size() - 1); + S2Point v0 = lastPair.getStart(); + S2Point v1 = lastPair.getEnd(); + + // If we are XORing, edges may be erased before we get to them. + if (options.getXorEdges() && !hasEdge(v0, v1)) { + continue; + } + + // Find nearest point to [v0,v1], or null if no point is within range. + results.clear(); + query.findClosestPointsToEdge(results, v0, v1); + for (Result result : results) { + S2Point vmid = result.entry().point(); + if (!vmid.equalsPoint(v0) && !vmid.equalsPoint(v1)) { + // Replace [v0,v1] with [v0,vmid] and [vmid,v1], and then add the new + // edges to the set of edges to test for adjacent points. + eraseEdge(v0, v1); + if (addEdge(v0, vmid)) { + pendingEdges.add(new S2Edge(v0, vmid)); + } + if (addEdge(vmid, v1)) { + pendingEdges.add(new S2Edge(vmid, v1)); + } + break; + } + } + } + } +} diff --git a/core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/S2Polyline.java b/core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/S2Polyline.java new file mode 100644 index 0000000000..5827c5e770 --- /dev/null +++ b/core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/S2Polyline.java @@ -0,0 +1,706 @@ +/* + * Copyright 2006 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.mogo.eagle.core.utilcode.geometry; + +import com.google.common.annotations.GwtCompatible; +import com.google.common.base.Objects; +import com.google.common.base.Preconditions; +import com.google.common.collect.Lists; +import com.mogo.eagle.core.utilcode.geometry.S2EdgeUtil.EdgeCrosser; +import com.mogo.eagle.core.utilcode.geometry.S2Projections.FaceSiTi; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.logging.Logger; + +/** + * An S2Polyline 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. + * + *

        Note: Polylines do not have a Contains(S2Point) method, because "containment" is not + * numerically well-defined except at the polyline vertices. + * + */ +@GwtCompatible(serializable = true) +public final strictfp class S2Polyline implements S2Shape, S2Region, Serializable { + private static final Logger log = Platform.getLoggerForClass(S2Polyline.class); + + private static final S2Point[] ARR_TEMPLATE = {}; + private static final byte LOSSLESS_ENCODING_VERSION = 1; + private static final byte COMPRESSED_ENCODING_VERSION = 2; + + private final int numVertices; + private final S2Point[] vertices; + + /** + * Create a polyline that connects the given vertices. Empty polylines are allowed. Adjacent + * vertices should not be identical or antipodal. All vertices should be unit length. + */ + public S2Polyline(List vertices) { + this(vertices.toArray(ARR_TEMPLATE)); + } + + private S2Polyline(S2Point[] vertices) { + // assert isValid(vertices); + this.numVertices = vertices.length; + this.vertices = vertices; + } + + /** Returns an unmodifiable view of the vertices of this polyline. */ + public List vertices() { + return Collections.unmodifiableList(Arrays.asList(vertices)); + } + + /** + * Return true if the polyline is valid having all vertices be in unit length and having no + * identical or antipodal adjacent vertices. + */ + public boolean isValid() { + return isValid(vertices()); + } + + /** Return true if the given vertices form a valid polyline. */ + public boolean isValid(List vertices) { + // All vertices must be unit length. + int n = vertices.size(); + for (int i = 0; i < n; ++i) { + if (!S2.isUnitLength(vertices.get(i))) { + log.info("Vertex " + i + " is not unit length"); + return false; + } + } + + // Adjacent vertices must not be identical or antipodal. + for (int i = 1; i < n; ++i) { + if (vertices.get(i - 1).equalsPoint(vertices.get(i)) + || vertices.get(i - 1).equalsPoint(S2Point.neg(vertices.get(i)))) { + log.info("Vertices " + (i - 1) + " and " + i + " are identical or antipodal"); + return false; + } + } + + return true; + } + + public int numVertices() { + return numVertices; + } + + public S2Point vertex(int k) { + // assert (k >= 0 && k < numVertices); + return vertices[k]; + } + + /** Return the angle corresponding to the total arclength of the polyline on a unit sphere. */ + public S1Angle getArclengthAngle() { + double lengthSum = 0; + for (int i = 1; i < numVertices(); ++i) { + lengthSum += vertex(i - 1).angle(vertex(i)); + } + return S1Angle.radians(lengthSum); + } + + /** + * Return the point whose distance from vertex 0 along the polyline is the given fraction of the + * polyline's total length. Fractions less than zero or greater than one are clamped. The return + * value is unit length. This cost of this function is currently linear in the number of vertices. + */ + public S2Point interpolate(double fraction) { + // We intentionally let the (fraction >= 1) case fall through, since + // we need to handle it in the loop below in any case because of + // possible roundoff errors. + if (fraction <= 0) { + return vertex(0); + } + + double lengthSum = 0; + for (int i = 1; i < numVertices(); ++i) { + lengthSum += vertex(i - 1).angle(vertex(i)); + } + double target = fraction * lengthSum; + for (int i = 1; i < numVertices(); ++i) { + double length = vertex(i - 1).angle(vertex(i)); + if (target < length) { + // This code interpolates with respect to arc length rather than + // straight-line distance, and produces a unit-length result. + double f = Math.sin(target) / Math.sin(length); + return S2Point.add( + S2Point.mul(vertex(i - 1), (Math.cos(target) - f * Math.cos(length))), + S2Point.mul(vertex(i), f)); + } + target -= length; + } + return vertex(numVertices() - 1); + } + + /** + * Projects the query point to the nearest part of the polyline, and returns the fraction of the + * polyline's total length traveled along the polyline from vertex 0 to the projected point. + * + *

        For any query point, the returned fraction is at least 0 (when the query point projects to + * the first vertex of the line) and at most 1 (when the query point projects to the last vertex). + * + *

        This method is essentially the inverse of {@link #interpolate(double)}, except that this + * method accepts any normalized point, whereas interpolate() only produces points on the line. + * + *

        In the unusual case of multiple equidistant points on the polyline, one of the nearest + * points is selected in a deterministic but unpredictable manner, and the fraction is computed up + * to that position. For example, all points of the S2 edge from (1,0,0) to (0,1,0) are + * equidistant from (0,0,1), so any fraction from 0 to 1 is a correct answer! + */ + public double uninterpolate(S2Point queryPoint) { + int i = getNearestEdgeIndex(queryPoint); + + double arcLength = + S2EdgeUtil.getClosestPoint(queryPoint, vertex(i), vertex(i + 1)).angle(vertex(i)); + for (; i > 0; i--) { + arcLength += vertex(i - 1).angle(vertex(i)); + } + + return Math.min(arcLength / getArclengthAngle().radians(), 1); + } + + // S2Region interface (see S2Region.java for details): + + /** Return a bounding spherical cap. */ + @Override + public S2Cap getCapBound() { + return getRectBound().getCapBound(); + } + + /** Return a bounding latitude-longitude rectangle. */ + @Override + public S2LatLngRect getRectBound() { + S2EdgeUtil.RectBounder bounder = new S2EdgeUtil.RectBounder(); + for (int i = 0; i < numVertices(); ++i) { + bounder.addPoint(vertex(i)); + } + return bounder.getBound(); + } + + /** + * If this method returns true, the region completely contains the given cell. Otherwise, either + * the region does not contain the cell or the containment relationship could not be determined. + */ + @Override + public boolean contains(S2Cell cell) { + return false; + } + + @Override + public boolean contains(S2Point point) { + return false; + } + + /** + * If this method returns false, the region does not intersect the given cell. Otherwise, either + * region intersects the cell, or the intersection relationship could not be determined. + */ + @Override + public boolean mayIntersect(S2Cell cell) { + if (numVertices() == 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 (int i = 0; i < numVertices(); ++i) { + if (cell.contains(vertex(i))) { + return true; + } + } + S2Point[] cellVertices = new S2Point[4]; + for (int i = 0; i < 4; ++i) { + cellVertices[i] = cell.getVertex(i); + } + for (int j = 0; j < 4; ++j) { + S2EdgeUtil.EdgeCrosser crosser = + new S2EdgeUtil.EdgeCrosser(cellVertices[j], cellVertices[(j + 1) & 3], vertex(0)); + for (int i = 1; i < numVertices(); ++i) { + if (crosser.robustCrossing(vertex(i)) >= 0) { + // There is a proper crossing, or two vertices were the same. + return true; + } + } + } + return false; + } + + /** + * Returns a new polyline where the vertices of the given polyline have been snapped to the + * centers of cells at the specified level. + */ + public static S2Polyline fromSnapped(final S2Polyline a, int snapLevel) { + // TODO(eengle): Use S2Builder when available. + List snappedVertices = new ArrayList<>(a.numVertices()); + S2Point prev = snapPointToLevel(a.vertex(0), snapLevel); + snappedVertices.add(prev); + for (int i = 1; i < a.numVertices(); i++) { + S2Point curr = snapPointToLevel(a.vertex(i), snapLevel); + if (!curr.equalsPoint(prev)) { + prev = curr; + snappedVertices.add(curr); + } + } + return new S2Polyline(snappedVertices); + } + + /** + * Returns a new point, snapped to the center of the cell containing the given point at the + * specified level. + */ + private static S2Point snapPointToLevel(final S2Point p, int level) { + return S2CellId.fromPoint(p).parent(level).toPoint(); + } + + /** + * Return a subsequence of vertex indices such that the polyline connecting these vertices is + * never further than "tolerance" from the original polyline. Provided the first and last vertices + * are distinct, they are always preserved; if they are not, the subsequence may contain only a + * single index. + * + *

        Some useful properties of the algorithm: + * + *

          + *
        • It runs in linear time. + *
        • The output is always a valid polyline. In particular, adjacent output vertices are never + * identical or antipodal. + *
        • The method is not optimal, but it tends to produce 2-3% fewer vertices than the + * Douglas-Peucker algorithm with the same tolerance. + *
        • The output is *parametrically* equivalent to the original polyline to within the given + * tolerance. For example, if a polyline backtracks on itself and then proceeds onwards, the + * backtracking will be preserved (to within the given tolerance). This is different than + * the Douglas-Peucker algorithm, which only guarantees geometric equivalence. + *
        + */ + public S2Polyline subsampleVertices(S1Angle tolerance) { + if (vertices.length == 0) { + return this; + } + List results = Lists.newArrayList(); + results.add(vertex(0)); + S1Angle clampedTolerance = S1Angle.max(tolerance, S1Angle.ZERO); + for (int i = 0; i < vertices.length - 1; ) { + int nextIndex = findEndVertex(clampedTolerance, i); + // Don't create duplicate adjacent vertices. + if (!vertex(nextIndex).equalsPoint(vertex(i))) { + results.add(vertex(nextIndex)); + } + i = nextIndex; + } + return new S2Polyline(results); + } + + /** + * Given a polyline, a tolerance distance, and a start index, this function returns the maximal + * end index such that the line segment between these two vertices passes within "tolerance" of + * all interior vertices, in order. + */ + private int findEndVertex(S1Angle tolerance, int index) { + // assert tolerance.radians() >= 0; + // assert index + 1 < polyline.num_vertices(); + + // The basic idea is to keep track of the "pie wedge" of angles from the starting vertex such + // that a ray from the starting vertex at that angle will pass through the discs of radius + // "tolerance" centered around all vertices processed so far. + + // First we define a "coordinate frame" for the tangent and normal spaces at the starting + // vertex. Essentially this means picking three orthonormal vectors X,Y,Z such that X and Y + // span the tangent plane at the starting vertex, and Z is "up". We use the coordinate frame to + // define a mapping from 3D direction vectors to a one-dimensional "ray angle" in the range + // (-Pi, Pi]. The angle of a direction vector is computed by transforming it into the X,Y,Z + // basis, and then calculating atan2(y,x). This mapping allows us to represent a wedge of + // angles as a 1D interval. Since the interval wraps around, we represent it as an S1Interval, + // i.e. an interval on the unit circle. + S2Point origin = vertex(index); + Matrix3x3 frame = S2.getFrame(origin); + + // As we go along, we keep track of the current wedge of angles and the distance to the last + // vertex (which must be non-decreasing). + S1Interval currentWedge = S1Interval.full(); + double lastDistance = 0; + + for (++index; index < vertices.length; ++index) { + S2Point candidate = vertex(index); + double distance = origin.angle(candidate); + + // We don't allow simplification to create edges longer than 90 degrees, to avoid numeric + // instability as lengths approach 180 degrees. (We do need to allow for original edges + // longer than 90 degrees, though.) + if (distance > S2.M_PI / 2 && lastDistance > 0) { + break; + } + + // Vertices must be in increasing order along the ray, except for the initial disc around the + // origin. + if (distance < lastDistance && lastDistance > tolerance.radians()) { + break; + } + lastDistance = distance; + + // Points that are within the tolerance distance of the origin do not constrain the ray + // direction, so we can ignore them. + if (distance <= tolerance.radians()) { + continue; + } + + // If the current wedge of angles does not contain the angle to this vertex, then stop right + // now. Note that the wedge of possible ray angles is not necessarily empty yet, but we can't + // continue unless we are willing to backtrack to the last vertex that was contained within + // the wedge (since we don't create new vertices). This would be more complicated and also + // make the worst-case running time more than linear. + S2Point direction = S2.toFrame(frame, candidate); + double center = Math.atan2(direction.y, direction.x); + if (!currentWedge.contains(center)) { + break; + } + + // To determine how this vertex constrains the possible ray angles, consider the triangle ABC + // where A is the origin, B is the candidate vertex, and C is one of the two tangent points + // between A and the spherical cap of radius "tolerance" centered at B. Then from the + // spherical law of sines, sin(a)/sin(A) = sin(c)/sin(C), where "a" and "c" are the lengths of + // the edges opposite A and C. In our case C is a 90 degree angle, therefore + // A = asin(sin(a) / sin(c)). Angle A is the half-angle of the allowable wedge. + double halfAngle = Math.asin(Math.sin(tolerance.radians()) / Math.sin(distance)); + S1Interval target = S1Interval.fromPoint(center).expanded(halfAngle); + currentWedge = currentWedge.intersection(target); + // assert !currentWedge.isEmpty(); + } + + // We break out of the loop when we reach a vertex index that can't be + // included in the line segment, so back up by one vertex. + return index - 1; + } + + /** + * Given a point, returns the index of the start point of the (first) edge on the polyline that is + * closest to the given point. The polyline must have at least one vertex. Throws + * IllegalStateException if this is not the case. + */ + public int getNearestEdgeIndex(S2Point point) { + Preconditions.checkState(numVertices() > 0, "Empty polyline"); + + if (numVertices() == 1) { + // If there is only one vertex, the "edge" is trivial, and it's the only one + return 0; + } + + // Initial value larger than any possible distance on the unit sphere. + S1Angle minDistance = S1Angle.radians(10); + int minIndex = -1; + + // Find the line segment in the polyline that is closest to the point given. + for (int i = 0; i < numVertices() - 1; ++i) { + S1Angle distanceToSegment = S2EdgeUtil.getDistance(point, vertex(i), vertex(i + 1)); + if (distanceToSegment.lessThan(minDistance)) { + minDistance = distanceToSegment; + minIndex = i; + } + } + return minIndex; + } + + /** + * Given a point p and the index of the start point of an edge of this polyline, returns the point + * on that edge that is closest to p. + */ + public S2Point projectToEdge(S2Point point, int index) { + Preconditions.checkState(numVertices() > 0, "Empty polyline"); + Preconditions.checkState(numVertices() == 1 || index < numVertices() - 1, "Invalid edge index"); + if (numVertices() == 1) { + // If there is only one vertex, it is always closest to any given point. + return vertex(0); + } + return S2EdgeUtil.getClosestPoint(point, vertex(index), vertex(index + 1)); + } + + /** + * Returns the point on the polyline closest to {@code queryPoint}. + * + *

        In the unusual case of a query point that is equidistant from multiple points on the line, + * one is returned in a deterministic but otherwise unpredictable way. + */ + public S2Point project(S2Point queryPoint) { + Preconditions.checkState(numVertices() > 0, "Empty polyline"); + if (numVertices() == 1) { + // If there is only one vertex, it is always closest to any given point. + return vertex(0); + } + int i = getNearestEdgeIndex(queryPoint); + return S2EdgeUtil.getClosestPoint(queryPoint, vertex(i), vertex(i + 1)); + } + + @Override + public boolean equals(Object that) { + if (!(that instanceof S2Polyline)) { + return false; + } + + S2Polyline thatPolyline = (S2Polyline) that; + if (numVertices != thatPolyline.numVertices) { + return false; + } + + for (int i = 0; i < vertices.length; i++) { + if (!vertices[i].equalsPoint(thatPolyline.vertices[i])) { + return false; + } + } + return true; + } + + /** + * Return true if this polyline intersects the given polyline. If the polylines share a vertex + * they are considered to be intersecting. When a polyline endpoint is the only intersection with + * the other polyline, the function may return true or false arbitrarily. + * + *

        The running time is quadratic in the number of vertices. + */ + public boolean intersects(S2Polyline line) { + if (numVertices() <= 0 || line.numVertices() <= 0) { + return false; + } + + if (!getRectBound().intersects(line.getRectBound())) { + return false; + } + + // TODO(user): Use S2ShapeIndex here. + for (int i = 1; i < numVertices(); ++i) { + EdgeCrosser crosser = new EdgeCrosser(vertex(i - 1), vertex(i), line.vertex(0)); + for (int j = 1; j < line.numVertices(); ++j) { + if (crosser.robustCrossing(line.vertex(j)) >= 0) { + return true; + } + } + } + return false; + } + + @Override + public int hashCode() { + return Objects.hashCode(numVertices, Arrays.deepHashCode(vertices)); + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder("S2Polyline, "); + + builder.append(vertices.length).append(" points. ["); + + for (S2Point v : vertices) { + builder.append(v.toDegreesString()).append(" "); + } + builder.append("]"); + + return builder.toString(); + } + + // S2Shape interface (see S2Shape.java for details): + + @Override + public int numEdges() { + return Math.max(0, numVertices - 1); + } + + @Override + public void getEdge(int index, MutableEdge result) { + result.set(vertices[index], vertices[index + 1]); + } + + @Override + public boolean hasInterior() { + return false; + } + + @Override + public boolean containsOrigin() { + throw new IllegalStateException( + "An S2Polyline has no interior, so " + "containsOrigin() should never be called on one."); + } + + @Override + public int numChains() { + return (numVertices > 1) ? 1 : 0; + } + + @Override + public int getChainStart(int chainId) { + Preconditions.checkElementIndex(chainId, numChains()); + return 0; + } + + @Override + public int getChainLength(int chainId) { + Preconditions.checkElementIndex(chainId, numChains()); + return numEdges(); + } + + @Override + public void getChainEdge(int chainId, int offset, MutableEdge result) { + Preconditions.checkElementIndex(chainId, numChains()); + getEdge(offset, result); + } + + @Override + public S2Point getChainVertex(int chainId, int edgeOffset) { + Preconditions.checkElementIndex(chainId, numChains()); + return vertex(edgeOffset); + } + + @Override + public int dimension() { + return 1; + } + + /** Encodes this polyline into the given output stream. */ + public void encode(OutputStream os) throws IOException { + encodeUncompressed(new LittleEndianOutput(os)); + } + + /** + * Encodes the polyline into an efficient, lossless binary representation, which can be decoded by + * calling {@link S2Polyline#decode}. The encoding is byte-compatible with the C++ version of the + * S2 library. + * + * @param output The output stream into which the encoding should be written. + * @throws IOException if there was a problem writing into the output stream. + */ + public void encodeCompact(OutputStream output) throws IOException { + int level = numVertices == 0 ? S2CellId.MAX_LEVEL : getBestSnapLevel(); + LittleEndianOutput encoder = new LittleEndianOutput(output); + if (level == -1) { + encodeUncompressed(encoder); + } else { + encodeCompressed(level, encoder); + } + } + + /** Encodes this polyline into the given little endian output stream. */ + void encodeUncompressed(LittleEndianOutput os) throws IOException { + os.writeByte(LOSSLESS_ENCODING_VERSION); + os.writeInt(numVertices); + for (S2Point p : vertices) { + p.encode(os); + } + } + + /** Encodes a compressed polyline at requested snap level. */ + void encodeCompressed(int snapLevel, LittleEndianOutput encoder) throws IOException { + encoder.writeByte(COMPRESSED_ENCODING_VERSION); + encoder.writeByte((byte) snapLevel); + encoder.writeVarint32(numVertices); + S2PointCompression.encodePointsCompressed(vertices(), snapLevel, encoder); + } + + public static S2Polyline decode(InputStream is) throws IOException { + return decode(new LittleEndianInput(is)); + } + + static S2Polyline decode(LittleEndianInput decoder) throws IOException { + byte version = decoder.readByte(); + switch (version) { + case LOSSLESS_ENCODING_VERSION: + return decodeLossless(decoder); + case COMPRESSED_ENCODING_VERSION: + return decodeCompressed(decoder); + default: + throw new IOException("Unsupported S2Polyline encoding version " + version); + } + } + + private static S2Polyline decodeLossless(LittleEndianInput is) throws IOException { + S2Point[] vertices = new S2Point[is.readInt()]; + for (int i = 0; i < vertices.length; i++) { + vertices[i] = S2Point.decode(is); + } + return new S2Polyline(vertices); + } + + private static S2Polyline decodeCompressed(LittleEndianInput decoder) throws IOException { + int level = decoder.readByte(); + if (level > S2CellId.MAX_LEVEL) { + throw new IOException("Invalid level " + level); + } + int numVertices = decoder.readVarint32(); + return new S2Polyline(S2PointCompression.decodePointsCompressed(numVertices, level, decoder)); + } + + /** + * If all of the polyline's vertices happen to be the centers of S2Cells at some level, then + * returns that level, otherwise returns -1. See also {@link #fromSnapped(S2Polyline, int)}. + * Returns -1 if the polyline has no vertices. + */ + public int getSnapLevel() { + int snapLevel = -1; + for (S2Point p : vertices) { + FaceSiTi faceSiTi = S2Projections.PROJ.xyzToFaceSiTi(p); + int level = S2Projections.PROJ.levelIfCenter(faceSiTi, p); + if (level < 0) { + // Vertex is not a cell center. + return level; + } + if (level != snapLevel) { + if (snapLevel < 0) { + // First vertex. + snapLevel = level; + } else { + // Vertices at more than one cell level. + return -1; + } + } + } + + return snapLevel; + } + + /** + * Computes the level at which most of the vertices are snapped. If multiple levels have the same + * maximum number of vertices snapped to it, the first one (lowest level number / largest area / + * smallest encoding length) will be chosen, so this is desired. Returns -1 for unsnapped + * polylines. + */ + int getBestSnapLevel() { + int[] histogram = new int[S2CellId.MAX_LEVEL + 1]; + for (S2Point p : vertices) { + FaceSiTi faceSiTi = S2Projections.PROJ.xyzToFaceSiTi(p); + int level = S2Projections.PROJ.levelIfCenter(faceSiTi, p); + // Level is -1 for unsnapped points. + if (level >= 0) { + histogram[level]++; + } + } + int snapLevel = 0; + for (int i = 1; i < histogram.length; i++) { + if (histogram[i] > histogram[snapLevel]) { + snapLevel = i; + } + } + if (histogram[snapLevel] == 0 && numVertices > 0) { + // This is an unsnapped polyline. + return -1; + } + return snapLevel; + } +} diff --git a/core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/S2Predicates.java b/core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/S2Predicates.java new file mode 100644 index 0000000000..507d55cc0a --- /dev/null +++ b/core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/S2Predicates.java @@ -0,0 +1,1733 @@ +/* + * Copyright 2018 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.mogo.eagle.core.utilcode.geometry; + +import com.google.common.annotations.GwtCompatible; +import java.math.BigDecimal; + +/** + * A collection of geometric predicates core to the robustness of the S2 library. In particular, + * + *

          + *
        • {@link #sign sign(A, B, C)}: Compute the orientation of a triple of points as clockwise + * (-1), colinear (0), or counter-clockwise (1). + *
        + */ +@GwtCompatible +public final strictfp class S2Predicates { + /** Maximum rounding error of a 64 bit double. */ + private static final double DBL_ERR = S2.DBL_EPSILON / 2; + + /** + * Rounding error of numeric type used for computation. This is a distinct value from {@link + * #DBL_ERR} to avoid recomputing the error bounds when we may later use higher precision (but + * inexact) types for computations. + */ + // TODO(eengle): Triage with Real when double math isn't precise enough, before using BigDecimal. + private static final double T_ERR = DBL_ERR; + + /** A predefined S1ChordAngle representing (approximately) 45 degrees. */ + private static final S1ChordAngle DEG_45 = S1ChordAngle.fromLength2(2 - S2.M_SQRT2); + + // Preallocated constants. + + private static final BigDecimal QUARTER = new BigDecimal("0.25"); + private static final BigDecimal HALF = new BigDecimal("0.5"); + private static final BigDecimal TWO = new BigDecimal("2"); + private static final BigDecimal FOUR = new BigDecimal("4"); + + /** + * Returns +1 if the points A, B, C are counterclockwise, -1 if the points are clockwise, and 0 if + * any two points are the same. This function is essentially like taking the sign of the + * determinant of ABC, except that it 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. + * + *

        Sign satisfies the following conditions: + * + *

          + *
        1. {@code sign(a,b,c) == 0} iff {@code a.equals(b) || b.equals(c) || c.equals(a)} + *
        2. {@code sign(b,c,a) == sign(a,b,c)}, for all a,b,c + *
        3. {@code sign(c,b,a) == -sign(a,b,c)}, for all a,b,c + *
        + * + *

        In other words: + * + *

          + *
        1. The result is zero 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 {@code sign(-a,b,c) == + * -sign(a,b,c)}, or any similar identities involving antipodal points. + */ + public static int sign(S2Point a, S2Point b, S2Point c) { + return Sign.sign(a, b, c, true); + } + + /** Tests of whether three points represent a left turn (+1), right turn (-1), or neither (0). */ + public static class Sign { + /** No instantiation. */ + private Sign() {} + + /** + * This version of Sign returns +1 if the points are definitely CCW, -1 if they are definitely + * CW, and 0 if two points are identical or the result is uncertain. Uncertain cases can be + * resolved, if desired, by {@link #expensive}. + * + *

        The purpose of this method is to allow additional cheap tests to be done, where possible, + * in order to avoid calling more expensive operations unnecessarily. + */ + public static int triage(S2Point a, S2Point b, S2Point c) { + // There are 14 multiplications and additions to compute the determinant + // below. Since all three points are normalized, it is possible to show + // that the average rounding error per operation does not exceed 2**-54, + // the maximum rounding error for an operation whose result magnitude is in + // the range [0.5,1). Therefore, if the absolute value of the determinant + // is greater than 2*14*(2**-54), the determinant will have the same sign + // even if the arguments are rotated (which produces a mathematically + // equivalent result but with potentially different rounding errors). + final double kMaxDetError = 1.6e-15; // 2 * 14 * 2**-54 + // assert S2.isUnitLength(a); + // assert S2.isUnitLength(b); + // assert S2.isUnitLength(c); + + double det = S2Point.scalarTripleProduct(c, a, b); + + // Double-check borderline cases in debug mode. + // assert ((Math.abs(det) <= kMinAbsValue) || (Math.abs(det) >= 100 * kMinAbsValue) + // || (det * expensiveCCW(a, b, c) > 0)); + + if (det >= kMaxDetError) { + return 1; + } + if (det <= -kMaxDetError) { + return -1; + } + return 0; + } + + /** + * Returns the sign of the turn ABC. Exactly straight points will only result in 0 if 'perturb' + * is false, otherwise the points are perturbed according to the rules in Simulation of + * Simplicity, to provide a logically consistent non-zero result for all inputs. + */ + public static int sign(S2Point a, S2Point b, S2Point c, boolean perturb) { + // assert (isUnitLength(a) && isUnitLength(b) && isUnitLength(c)); + int ccw = Sign.triage(a, b, c); + if (ccw == 0) { + ccw = Sign.expensive(a, b, c, perturb); + } + return ccw; + } + + /** + * Returns the sign of the determinant using more expensive techniques. To be used when the + * magnitude of the determinant is close enough to zero that its value is uncertain using faster + * but less robust techniques. + */ + public static int expensive(S2Point a, S2Point b, S2Point c, boolean perturb) { + // Return zero if and only if two points are the same, the first property of sign(). + if (a.equalsPoint(b) || b.equalsPoint(c) || c.equalsPoint(a)) { + return 0; + } + + int sign = stable(a, b, c); + if (sign != 0) { + return sign; + } + + return exact(a, b, c, perturb); + } + + /** + * Compute the determinant in a numerically stable way. Unlike {@link #triage}, 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. + */ + public static int stable(S2Point a, S2Point b, S2Point c) { + S2Point ab = S2Point.sub(b, a); + S2Point bc = S2Point.sub(c, b); + S2Point ca = S2Point.sub(a, c); + double ab2 = ab.norm2(); + double bc2 = bc.norm2(); + double 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. + // Using a similar technique to the one used for kMaxDetError, the error is at most + // + // |d| <= (3 + 6/sqrt(3)) * |A-C| * |B-C| * e + // + // where e = 0.5 * DBL_EPSILON. If the determinant magnitude is larger than this value then + // we know its sign with certainty. + final double detErrorMultiplier = 3.2321 * S2.DBL_EPSILON; // see above + double det; + double maxError; + if (ab2 >= bc2 && ab2 >= ca2) { + // AB is the longest edge, so compute (A-C)x(B-C).C. + det = -S2Point.scalarTripleProduct(c, ca, bc); + maxError = detErrorMultiplier * Math.sqrt(ca2 * bc2); + } else if (bc2 >= ca2) { + // BC is the longest edge, so compute (B-A)x(C-A).A. + det = -S2Point.scalarTripleProduct(a, ab, ca); + maxError = detErrorMultiplier * Math.sqrt(ab2 * ca2); + } else { + // CA is the longest edge, so compute (C-B)x(A-B).B. + det = -S2Point.scalarTripleProduct(b, bc, ab); + maxError = detErrorMultiplier * Math.sqrt(bc2 * ab2); + } + return signum(det, maxError); + } + + /** + * Computes the determinant using exact arithmetic and/or symbolic permutations. Requires that + * the three points are distinct. + */ + public static int exact(S2Point a, S2Point b, S2Point c, boolean perturb) { + // assert !a.equalsPoint(b) && !b.equalsPoint(c) && !c.equalsPoint(a); + + // Check the determinant using any available Platform optimizations, which may return zero + // but are faster than BigDecimal techniques. + int sign = Platform.sign(a, b, c); + if (sign != 0) { + return sign; + } + + // Sort the 3 points in lexicographic order, keeping track of the sign of the permutation. + // (Each exchange inverts the sign of the determinant.) + int permSign = 1; + if (a.compareTo(b) > 0) { + S2Point t = a; + a = b; + b = t; + permSign = -permSign; + } + if (b.compareTo(c) > 0) { + S2Point t = b; + b = c; + c = t; + permSign = -permSign; + } + if (a.compareTo(b) > 0) { + S2Point t = a; + a = b; + b = t; + permSign = -permSign; + } + // assert a.compareTo(b) < 0 && b.compareTo(c) < 0; + + // The BigDecimal approach used below can't handle NaNs. Prior to use of this type, NaN always + // resulted in -1, regardless of other coordinate values, so maintain that behavior here. + if (!a.equalsPoint(a) || !b.equalsPoint(b) || !c.equalsPoint(c)) { + return -permSign; + } + + // Check the determinant using BigDecimal, which is exact but very slow. + // Save the cross product for use in symbolic perturbation if necessary. + BigPoint xa = big(a); + BigPoint xb = big(b); + BigPoint xc = big(c); + BigPoint xbc = xb.crossProd(xc); + sign = xbc.dotProd(xa).signum(); + if (sign != 0) { + return permSign * sign; + } + + // If !perturb, then the caller has requested no SoS tie breaking, so simply return 0 now. + if (!perturb) { + return 0; + } + + // Resort to symbolic perturbations to resolve a stable non-zero result. + sign = sos(xa, xb, xc, xbc); + // assert 0 != sign; + return permSign * sign; + } + + /** + * Returns the sign of the determinant of three column vectors A, B, C under a model where every + * possible S2Point is slightly perturbed by a unique infinitesimal amount such that no three + * perturbed points are collinear and no four points are coplanar. The perturbations are so + * small that they do not change the sign of any determinant that was non-zero before the + * perturbations, and therefore can be safely ignored unless the determinant of three points is + * exactly zero (using multiple-precision arithmetic). + * + *

        Since the symbolic perturbation of a given point is fixed (i.e., the perturbation is the + * same for all calls to this method and does not depend on the other two arguments), the + * results of this method are always self-consistent. It will never return results that would + * correspond to an "impossible" configuration of non-degenerate points. + * + *

        Requirements: + * + *

          + *
        • The 3x3 determinant of A, B, C must be exactly zero. + *
        • The points must be distinct, with A < B < C in lexicographic order. + *
        + * + *

        Returns +1 or -1 according to the sign of the determinant after the symbolic perturbations + * are taken into account. This method is suitable as a tie breaker when all other tests fail to + * choose a distinct turning direction. + * + *

        Reference: + * + *

        +     * "Simulation of Simplicity"
        +     * Edelsbrunner and Muecke, ACM Transactions on Graphics, 1990
        +     * 
        + */ + public static int sos(BigPoint a, BigPoint b, BigPoint c, BigPoint bc) { + // This method requires that the points are sorted in lexicographically + // increasing order. This is because every possible S2Point has its own + // symbolic perturbation such that if A < B then the symbolic perturbation + // for A is much larger than the perturbation for B. + // + // Alternatively, we could sort the points in this method and keep track of + // the sign of the permutation, but it is more efficient to do this before + // converting the inputs to the multi-precision representation, and this + // also lets us re-use the result of the cross product B x C. + // assert a.compareTo(b) < 0 && b.compareTo(c) < 0; + + // Every input coordinate x[i] is assigned a symbolic perturbation dx[i]. + // We then compute the sign of the determinant of the perturbed points, + // i.e. + // | a[0]+da[0] a[1]+da[1] a[2]+da[2] | + // | b[0]+db[0] b[1]+db[1] b[2]+db[2] | + // | c[0]+dc[0] c[1]+dc[1] c[2]+dc[2] | + // + // The perturbations are chosen such that + // + // da[2] > da[1] > da[0] > db[2] > db[1] > db[0] > dc[2] > dc[1] > dc[0] + // + // where each perturbation is so much smaller than the previous one that we + // don't even need to consider it unless the coefficients of all previous + // perturbations are zero. In fact, it is so small that we don't need to + // consider it unless the coefficient of all products of the previous + // perturbations are zero. For example, we don't need to consider the + // coefficient of db[1] unless the coefficient of db[2]*da[0] is zero. + // + // The following code simply enumerates the coefficients of the perturbations + // (and products of perturbations) that appear in the determinant above, in + // order of decreasing perturbation magnitude. The first non-zero + // coefficient determines the sign of the result. The easiest way to + // enumerate the coefficients in the correct order is to pretend that each + // perturbation is some tiny value "eps" raised to a power of two: + // + // eps** 1 2 4 8 16 32 64 128 256 + // da[2] da[1] da[0] db[2] db[1] db[0] dc[2] dc[1] dc[0] + // + // Essentially we can then just count in binary and test the corresponding + // subset of perturbations at each step. So for example, we must test the + // coefficient of db[2]*da[0] before db[1] because eps**12 > eps**16. + // + // Of course, not all products of these perturbations appear in the + // determinant above, since the determinant only contains the products of + // elements in distinct rows and columns. Thus we don't need to consider + // da[2]*da[1], db[1]*da[1], etc. Furthermore, sometimes different pairs of + // perturbations have the same coefficient in the determinant; for example, + // da[1]*db[0] and db[1]*da[0] have the same coefficient (c[2]). Therefore + // we only need to test this coefficient the first time we encounter it in + // the binary order above (which will be db[1]*da[0]). + // + // The sequence of tests below also appears in Table 4-ii of the paper + // referenced above, if you just want to look it up, with the following + // translations: [a,b,c] -> [i,j,k] and [0,1,2] -> [1,2,3]. Also note that + // some of the signs are different because the opposite cross product is + // used (e.g., B x C rather than C x B). + + int sign = bc.z.signum(); // da[2] + if (sign != 0) { + return sign; + } + sign = bc.y.signum(); // da[1] + if (sign != 0) { + return sign; + } + sign = bc.x.signum(); // da[0] + if (sign != 0) { + return sign; + } + + sign = c.x.multiply(a.y).subtract(c.y.multiply(a.x)).signum(); // db[2] + if (sign != 0) { + return sign; + } + sign = c.x.signum(); // db[2] * da[1] + if (sign != 0) { + return sign; + } + sign = -c.y.signum(); // db[2] * da[0] + if (sign != 0) { + return sign; + } + sign = c.z.multiply(a.x).subtract(c.x.multiply(a.z)).signum(); // db[1] + if (sign != 0) { + return sign; + } + sign = c.z.signum(); // db[1] * da[0] + if (sign != 0) { + return sign; + } + + // The following test is listed in the paper, but it is redundant because + // the previous tests guarantee that C == (0, 0, 0). + // assert 0 == cy.multiply(az).subtract(cz.multiply(ay)).signum(); // db[0] + + sign = a.x.multiply(b.y).subtract(a.y.multiply(b.x)).signum(); // dc[2] + if (sign != 0) { + return sign; + } + sign = -b.x.signum(); // dc[2] * da[1] + if (sign != 0) { + return sign; + } + sign = b.y.signum(); // dc[2] * da[0] + if (sign != 0) { + return sign; + } + sign = a.x.signum(); // dc[2] * db[1] + if (sign != 0) { + return sign; + } + return 1; // dc[2] * db[1] * da[0] + } + } + + /** + * Return 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 a continuous + * CCW ordering around O. + * + *

        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 + *
        + */ + public static boolean orderedCCW(S2Point a, S2Point b, S2Point c, S2Point o) { + // The last inequality below is ">" rather than ">=" so that we return true + // if A == B or B == C, and otherwise false if A == C. Recall that + // sign(x,y,z) == -sign(z,y,x) for all x,y,z. + + int sum = 0; + if (sign(b, o, a) >= 0) { + ++sum; + } + if (sign(c, o, b) >= 0) { + ++sum; + } + if (sign(a, o, c) > 0) { + ++sum; + } + return sum >= 2; + } + + /** + * Returns -1, 0, or +1 according to whether AX < BX, A == B, or AX > BX respectively. Distances + * are measured with respect to the positions of X, A, and B as though they were reprojected to + * lie exactly on the surface of the unit sphere. Furthermore, this method uses symbolic + * perturbations to ensure that the result is non-zero whenever A != B, even when AX == BX + * exactly, or even when A and B project to the same point on the sphere. Such results are + * guaranteed to be self-consistent, i.e. if AB < BC and BC < AC, then AB < AC. + */ + public static int compareDistances(S2Point x, S2Point a, S2Point b) { + // We start by comparing distances using dot products (i.e., cosine of the + // angle), because (1) this is the cheapest technique, and (2) it is valid + // over the entire range of possible angles. (We can only use the sin^2 + // technique if both angles are less than 90 degrees or both angles are + // greater than 90 degrees.) + int sign = CompareDistances.triageCos(x, a, b); + if (sign != 0) { + return sign; + } + + // Optimization for (a == b) to avoid falling back to exact arithmetic. + if (a.equalsPoint(b)) { + return 0; + } + + // It is much better numerically to compare distances using cos(angle) if + // the distances are near 90 degrees and sin^2(angle) if the distances are + // near 0 or 180 degrees. We only need to check one of the two angles when + // making this decision because the fact that the test above failed means + // that angles "a" and "b" are very close together. + double cosAX = a.dotProd(x); + if (cosAX > S2.M_SQRT1_2) { + // Angles < 45 degrees. + sign = CompareDistances.triageSin2(x, a, b); + } else if (cosAX < -S2.M_SQRT1_2) { + // Angles > 135 degrees. sin^2(angle) is decreasing in this range. + sign = -CompareDistances.triageSin2(x, a, b); + } + if (sign != 0) { + return sign; + } + sign = CompareDistances.exact(x, a, b); + if (sign != 0) { + return sign; + } + return CompareDistances.sos(a, b); + } + + /** + * A set of tests to determine which of two points is closer to a reference point. Generally much + * faster then computing even one exact distance, since most points are "obviously" ordered w.r.t. + * the reference point. Returns -1, 0, or +1 according to whether AX < BX, A == B, or AX > BX + * respectively, and may return 0 if the result is indeterminate. + */ + static class CompareDistances { + /** No instantiation. */ + private CompareDistances() {} + + /** + * Returns a cosine-based test result. It handles all angles, is the fastest implementation, but + * has a wide margin of uncertainty. + */ + public static int triageCos(S2Point x, S2Point a, S2Point b) { + double cosAX = cosDistance(a, x); + double cosBX = cosDistance(b, x); + return compare(cosBX, cosDistanceError(cosBX), cosAX, cosDistanceError(cosAX)); + } + + /** + * Returns the test result using a more accurate sine strategy, which only allows angles either + * both below -90 or both above +90 degrees. + */ + public static int triageSin2(S2Point x, S2Point a, S2Point b) { + double sin2AX = sin2Distance(a, x); + double sin2BX = sin2Distance(b, x); + return compare(sin2AX, sin2DistanceError(sin2AX), sin2BX, sin2DistanceError(sin2BX)); + } + + /** Returns a BigDecimal-based test result, which is slow but handle all input. */ + public static int exact(S2Point x, S2Point a, S2Point b) { + return exact(big(x), big(a), big(b)); + } + + /** Returns a BigDecimal-based test result, which is slow but handle all input. */ + public static int exact(BigPoint x, BigPoint a, BigPoint b) { + // This code produces the same result as though all points were reprojected to lie exactly on + // the surface of the unit sphere. It is based on testing whether x.dotProd(a.normalize()) < + // x.dotProd(b.normalize()), reformulated so that it can be evaluated using exact arithmetic. + BigDecimal cosAX = x.dotProd(a); + BigDecimal cosBX = x.dotProd(b); + // If the values have different signs, handle that case now before squaring them below. + int aSign = cosAX.signum(); + int bSign = cosBX.signum(); + if (aSign != bSign) { + // If cos(AX) > cos(BX), then AX < BX. + return Integer.compare(bSign, aSign); + } + int cmpSign = + cosBX + .multiply(cosBX) + .multiply(a.norm2()) + .compareTo(cosAX.multiply(cosAX).multiply(b.norm2())); + return aSign * cmpSign; + } + + /** + * Given that the exact test returned 0, returns a Simulation of Simplicity symbolic + * perturbation-based test result to select a consistent non-zero result. + */ + public static int sos(S2Point a, S2Point b) { + // Our symbolic perturbation strategy is based on the following model. Similar to "simulation + // of simplicity", we assign a perturbation to every point such that if A < B, then the + // symbolic perturbation for A is much, much larger than the symbolic perturbation for B. We + // imagine that rather than projecting every point to lie exactly on the unit sphere, instead + // each point is positioned on its own tiny pedestal that raises it just off the surface of + // the unit sphere. This means that the distance AX is actually the true distance AX plus the + // (symbolic) heights of the pedestals for A and X. The pedestals are infinitesmally thin, so + // they do not affect distance measurements except at the two endpoints. If several points + // project to exactly the same point on the unit sphere, we imagine that they are placed on + // separate pedestals placed close together, where the distance between pedestals is much, + // much less than the height of any pedestal; as there are a finite number of S2Points, and + // therefore a finite number of pedestals, this is possible. + // + // If A < B, then A is on a higher pedestal than B, and therefore AX > BX. + return b.compareTo(a); + } + } + + /** + * Returns -1, 0, or +1 according to whether the distance XY is less than, equal to, or greater + * than the squared chord distance "r2" respectively. Distances are measured with respect the + * positions of all points as though they are projected to lie exactly on the surface of the unit + * sphere. + */ + // TODO(eengle): Make this public once the library has moved onto it. + static int compareDistance(S2Point x, S2Point y, double r2) { + int sign = CompareDistance.triage(x, y, r2); + if (sign != 0) { + return sign; + } + return CompareDistance.exact(x, y, r2); + } + + /** + * A set of tests to compare the distance XY and a previously computed distance. When doing many + * distance tests, this saves a lot of work over computing exact distances only to throw away most + * of them. Each test returns -1 if the distance XY is less than 'r2', +1 if the distance XY is + * greater than 'r2', and 0 if the distances are exactly equal or the relation is indeterminate. + */ + static class CompareDistance { + /** No instantiation. */ + private CompareDistance() {} + + /** + * Returns a cosine-based test result. This is the fastest test, it handle all angles, but has a + * wide margin of uncertainty. + */ + public static int triageCos(S2Point x, S2Point y, double r2) { + double cosXY = cosDistance(x, y); + double cosR = 1 - 0.5 * r2; + return compare(cosR, 2 * T_ERR * cosR, cosXY, cosDistanceError(cosXY)); + } + + /** + * Returns a sine-based test result, which has very good accuracy for small angles, although it + * only handles angles below 90 degrees. + */ + public static int triageSin2(S2Point x, S2Point y, double r2) { + // assert r2 < 2.0; // Only valid for distance limits < 90 degrees. + double xySin2 = sin2Distance(x, y); + double rSin2 = r2 * (1 - 0.25 * r2); + return compare(xySin2, sin2DistanceError(xySin2), rSin2, 3 * T_ERR * rSin2); + } + + /** + * Returns a test result based on first {@link #triageCos} and then {@link #triageSin2}, so it + * is fast for any input where exact arithmetic isn't needed. + */ + public static int triage(S2Point x, S2Point y, double r2) { + // The Sin2 method is much more accurate for small distances, but it is only valid when the + // actual distance and the distance limit are both less than 90 degrees. So we always start + // with the Cos method. + int sign = triageCos(x, y, r2); + if (sign == 0 && r2 < DEG_45.getLength2()) { + sign = triageSin2(x, y, r2); + } + return sign; + } + + /** Calls {@link #exact(BigPoint, BigPoint, BigDecimal)} with its more precise types. */ + public static int exact(S2Point x, S2Point y, double r2) { + return exact(big(x), big(y), big(r2)); + } + + /** + * Returns a BigDecimal-based test result, which is exact for all inputs but very slow. In + * particular, this code produces a result as though all points were reprojected to lie exactly + * on the surface of the unit sphere. It is based on comparing the cosine of the angle XY (when + * both points are projected to lie exactly on the sphere) to the given threshold. + */ + public static int exact(BigPoint x, BigPoint y, BigDecimal r2) { + BigDecimal cosXY = x.dotProd(y); + BigDecimal cosR = BigDecimal.ONE.subtract(HALF.multiply(r2)); + // If the values have different signs, handle that case now before squaring them below. + int xySign = cosXY.signum(); + int rSign = cosR.signum(); + if (xySign != rSign) { + // If cos(XY) > cos(r), then XY < r. + return Integer.compare(rSign, xySign); + } + int cmpSign = square(cosR).multiply(x.norm2().multiply(y.norm2())).compareTo(square(cosXY)); + return xySign * cmpSign; + } + } + + /** + * Returns -1, 0, or +1 according to whether the distance from the point X to the edge AB is less + * than, equal to, or greater than the squared chord distance "r2" respectively. + * + *

        Distances are measured with respect to the positions of all points as though they were + * projected to lie exactly on the surface of the unit sphere. + * + *

        Requires that A and B do not project to antipodal points (e.g., A != -B). This can occur if + * for example A == S * B, for some constant S < 0. + * + *

        Note all of the predicates defined here could be extended to handle edges consisting of + * antipodal points by implementing additional symbolic perturbation logic (similar to {@link + * #sign}) in order to rigorously define the direction of such edges. + */ + public static int compareEdgeDistance(S2Point x, S2Point a, S2Point b, double r2) { + // Check that the edge does not consist of antipodal points. + // This catches the most common case. The full test is in EXACT. + // assert !a.equals(b.neg()); + + int sign = CompareEdgeDistance.triage(x, a, b, r2); + if (sign != 0) { + return sign; + } + + // Optimization for the case where the edge is degenerate. + if (a.equalsPoint(b)) { + return compareDistance(x, a, r2); + } + + return CompareEdgeDistance.exact(x, a, b, r2); + } + + /** + * A test to compare the distance from point X to edge A with a previously computed distance. When + * doing many edge distance tests, this saves a lot of work over computing exact distances only to + * throw them away most of them. + * + *

        Does not offer generally correct results for all inputs, so that multiple strategies may be + * implemented for different classes of input. + */ + static class CompareEdgeDistance { + /** No instantiation. */ + private CompareEdgeDistance() {} + + /** + * Returns -1, 0, or +1 according to whether the distance from the point X to the edge AB is + * less than, equal to, or greater than "r2" respectively, and may return 0 if the relation is + * indeterminate. + * + *

        This test uses double arithmetic, which is reasonably precise but allocates a lot. + */ + // TODO(eengle): x.angle(a)+a.angle(b) < r is usually false, so test that first? + public static int triage(S2Point x, S2Point a, S2Point b, double r2) { + // First we need to decide whether the closest point is an edge endpoint or somewhere in the + // interior. To determine this we compute a plane perpendicular to (a, b) that passes through + // X. Letting M be the normal to this plane, the closest point is in the edge interior if and + // only if a.M < 0 and b.M > 0. Note that we can use "<" rather than "<=" because if a.M or + // b.M is zero exactly then it doesn't matter which code path we follow (since the distance to + // an endpoint and the distance to the edge interior are exactly the same in this case). + S2Point n = ndCross(a, b); + S2Point m = n.crossProd(x); + // For better accuracy when the edge (a,b) is very short, we subtract "x" before computing the + // dot products with M. + S2Point aDir = a.sub(x); + S2Point bDir = b.sub(x); + double aSign = aDir.dotProd(m); + double bSign = bDir.dotProd(m); + double n2 = n.norm2(); + double n1 = Math.sqrt(n2); + double n1Error = ((3.5 + 8 / Math.sqrt(3)) * n1 + 32 * Math.sqrt(3) * DBL_ERR) * T_ERR; + double aSignError = n1Error * aDir.norm(); + double bSignError = n1Error * bDir.norm(); + if (Math.abs(aSign) < aSignError || Math.abs(bSign) < bSignError) { + // It is uncertain whether minimum distance is to an edge vertex or to the edge interior. + // So compute both distances and check whether they yield the same result. + int vertexSign = triageLineEndpoints(x, a, b, r2); + int lineSign = triageLineInterior(x, a, b, r2, n, n1, n2); + return (vertexSign == lineSign) ? lineSign : 0; + } + if (aSign >= 0 || bSign <= 0) { + // The minimum distance is to an edge endpoint. + return triageLineEndpoints(x, a, b, r2); + } else { + // The minimum distance is to the edge interior. + return triageLineInterior(x, a, b, r2, n, n1, n2); + } + } + + /** Returns the min test result from XA and XB, assuming the projection is A or B. */ + static int triageLineEndpoints(S2Point x, S2Point a, S2Point b, double r2) { + return Math.min(CompareDistance.triage(x, a, r2), CompareDistance.triage(x, b, r2)); + } + + /** Returns the min test result from XA and XB, assuming the projection is between A and B. */ + static int triageLineInterior( + S2Point x, S2Point a, S2Point b, double r2, S2Point n, double n1, double n2) { + if (r2 < DEG_45.getLength2()) { + return triageLineSin2(x, a, b, r2, n, n1, n2); + } else { + return triageLineCos2(x, r2, n, n1, n2); + } + } + + /** + * Returns -1, 0, or +1 according to whether the distance from "x" to the great circle through + * (a, b) is less than, equal to, or greater than the given squared chord length "r2". This + * method computes the squared sines of the distances involved, which is more accurate when the + * distances are small (less than 45 degrees). + * + *

        The remaining parameters are functions of (a, b) and are passed in because they have + * already been computed: n = (a - b) x (a + b), n1 = n.norm(), and n2 = n.norm2(). + */ + static int triageLineSin2( + S2Point x, S2Point a, S2Point b, double r2, S2Point n, double n1, double n2) { + // The minimum distance is to a point on the edge interior. Since the true distance to the + // edge is always less than 90 degrees, we can return immediately if the limit is 90 degrees + // or larger. + if (r2 >= 2.0) { + // distance < limit + return -1; + } + + // Otherwise we compute sin^2(distance to edge) to get the best accuracy when the distance + // limit is small (e.g., S2.kIntersectionError). + double n2Sin2R = n2 * r2 * (1 - 0.25 * r2); + double n2Sin2RError = 6 * T_ERR * n2Sin2R; + double[] ax2 = {0}; + double xDn = x.sub(closestVertex(x, a, b, ax2)).dotProd(n); + double xDn2 = xDn * xDn; + double c1 = + ((3.5 + 2 * Math.sqrt(3)) * n1 + 32 * Math.sqrt(3) * DBL_ERR) * T_ERR * Math.sqrt(ax2[0]); + double xDn2Error = 4 * T_ERR * xDn2 + (2 * Math.abs(xDn) + c1) * c1; + + // If we are using extended precision, then it is worthwhile to recompute the length of X more + // accurately. Otherwise we use the fact that X is guaranteed to be unit length to with a + // tolerance of 4 * DBL_ERR. + if (T_ERR < DBL_ERR) { + n2Sin2R *= x.norm2(); + n2Sin2RError += 4 * T_ERR * n2Sin2R; + } else { + n2Sin2RError += 8 * DBL_ERR * n2Sin2R; + } + return compare(xDn2, xDn2Error, n2Sin2R, n2Sin2RError); + } + + /** + * Like triageLineSin2, but this method computes the squared cosines of the distances involved. + * It is more accurate when the distances are large (greater than 45 degrees). + */ + static int triageLineCos2(S2Point x, double r2, S2Point n, double n1, double n2) { + // The minimum distance is to a point on the edge interior. Since the true distance to the + // edge is always less than 90 degrees, return if the limit is 90 degrees or larger. + if (r2 >= 2.0) { + // distance < limit + return -1; + } + + // Otherwise we compute cos^2(distance to edge). + double cosR = 1 - 0.5 * r2; + double n2Cos2R = n2 * cosR * cosR; + double n2Cos2RError = 7 * DBL_ERR * n2Cos2R; + + // The length of M = X.crossProd(N) is the cosine of the distance. + double m2 = x.crossProd(n).norm2(); + double m1 = Math.sqrt(m2); + double m1Error = ((1 + 8 / Math.sqrt(3)) * n1 + 32 * Math.sqrt(3) * DBL_ERR) * DBL_ERR; + double m2Error = 3 * DBL_ERR * m2 + (2 * m1 + m1Error) * m1Error; + + // If we are using extended precision, then it is worthwhile to recompute the length of X more + // accurately. Otherwise we use the fact that X is guaranteed to be unit length to within a + // tolerance of 4 * DBL_ERR. + if (T_ERR < DBL_ERR) { + // Note this is effectively dead code in Java, pending a long double data type. + n2Cos2R *= x.norm2(); + n2Cos2RError += 4 * T_ERR * n2Cos2R; + } else { + n2Cos2RError += 8 * DBL_ERR * n2Cos2R; + } + return compare(n2Cos2R, n2Cos2RError, m2, m2Error); + } + + /** Returns a BigDecimal-based test result, which is exact but very slow. */ + public static int exact(S2Point x, S2Point a, S2Point b, double r2) { + // Even if previous calculations were uncertain, we might not need to do *all* the + // calculations in exact arithmetic here. For example it may be easy to determine whether "x" + // is closer to an endpoint or the edge interior. The only calculation where we always use + // exact arithmetic is when measuring the distance to the extended line (great circle) through + // "a" and "b", since it is virtually certain that the previous floating point calculations + // failed in that case. + + // CompareEdgeDirections also checks that no edge has antipodal endpoints. + if (compareEdgeDirections(a, b, a, x) > 0 && compareEdgeDirections(a, b, x, b) > 0) { + return exactLineInterior(big(x), big(a), big(b), big(r2)); + } else { + return exactLineEndpoints(x, a, b, r2); + } + } + + /** Returns a BigDecimal-based test result assuming the projection of X is onto A or B. */ + static int exactLineEndpoints(S2Point x, S2Point a, S2Point b, double r2) { + return Math.min(compareDistance(x, a, r2), compareDistance(x, b, r2)); + } + + /** Returns a BigDecimal-based test assuming the projection of "x" is between A and B. */ + static int exactLineInterior(BigPoint x, BigPoint a, BigPoint b, BigDecimal r2) { + // Since we are given that the closest point is in the edge interior, the true distance is + // always less than 90 degrees (which corresponds to a squared chord length of 2.0). + if (r2.compareTo(TWO) >= 0) { + // distance < limit + return -1; + } + + // Otherwise compute the edge normal + BigPoint n = a.crossProd(b); + BigDecimal sinD = x.dotProd(n); + BigDecimal sin2R = r2.multiply(BigDecimal.ONE.subtract(QUARTER.multiply(r2))); + return square(sinD).compareTo(sin2R.multiply(x.norm2()).multiply(n.norm2())); + } + } + + /** + * Returns -1, 0, or +1 according to whether the normal of edge AB has negative, zero, or positive + * dot product with the normal of edge CD. This essentially measures whether the edges AB and CD + * are closer to proceeding in the same direction or in opposite directions around the sphere. + * + *

        This method returns an exact result, i.e. the result is zero if and only if the two edges + * are exactly perpendicular or at least one edge is degenerate. (i.e., both edge endpoints + * project to the same point on the sphere). + * + *

        However, this method does not use symbolic perturbations. Therefore it can return zero even + * when A != B and C != D, e.g. if A == S * B exactly for some constant S > 0 (which is possible + * even when both points are considered "normalized"). + * + *

        Edges may not consist of antipodal points (e.g., A != -B). See {@link #compareEdgeDistance}. + */ + // TODO(eengle): Make this public once the library has moved onto it. + static int compareEdgeDirections(S2Point a, S2Point b, S2Point c, S2Point d) { + // Check that no edge consists of antipodal points. This catches the most common case; a full + // test is in CompareEdgeDirections.exact.) + // assert !a.equals(b.neg()); + // assert !c.equals(d.neg()); + + int sign = CompareEdgeDirections.triage(a, b, c, d); + if (sign != 0) { + return sign; + } + + // Optimization for the case where either edge is degenerate. + if (a.equalsPoint(b) || c.equalsPoint(d)) { + return 0; + } + + return CompareEdgeDirections.exact(a, b, c, d); + } + + /** + * A test to compare whether two edges are closer to proceeding in the same direction or in + * opposite directions around the sphere, essentially signum((AxB)x(CxD)). Returns -1, 0, or +1 + * according to whether the normal of edge AB has negative, zero, or positive dot product with the + * normal of edge CD, and may return 0 if the relation is indeterminate. + */ + static class CompareEdgeDirections { + /** No instantiation. */ + private CompareEdgeDirections() {} + + /** Returns a cosine-based test result. Fast but has a wide margin of uncertainty. */ + public static int triage(S2Point a, S2Point b, S2Point c, S2Point d) { + S2Point n1 = ndCross(a, b); + S2Point n2 = ndCross(c, d); + double len1 = n1.norm(); + double len2 = n2.norm(); + double cos = n1.dotProd(n2); + double cosError = + ((5 + 4 * Math.sqrt(3)) * len1 * len2 + 32 * Math.sqrt(3) * DBL_ERR * (len1 + len2)) + * T_ERR; + return signum(cos, cosError); + } + + /** Returns a BigDecimal-based test result. Exact but very slow. */ + public static int exact(S2Point a, S2Point b, S2Point c, S2Point d) { + return exact(big(a), big(b), big(c), big(d)); + } + + /** Returns a BigDecimal-based test result. Exact but very slow. */ + public static int exact(BigPoint a, BigPoint b, BigPoint c, BigPoint d) { + // assert !a.isAntipodal(b); + // assert !c.isAntipodal(d); + return a.crossProd(b).dotProd(c.crossProd(d)).signum(); + } + } + + /** + * Returns sign(P, Q, Z) where Z is the circumcenter of triangle ABC. The return value is -1 if Z + * is to the left of edge PQ, and +1 if Z is to the right of edge PQ. The return value is zero if + * A == B, B == C, or C == A (exactly), and also if P and Q project to identical points on the + * sphere (e.g., P == Q). + * + *

        The result is determined with respect to the positions of all points as though they were + * projected to lie exactly on the surface of the unit sphere. Furthermore this method uses + * symbolic perturbations to compute a consistent non-zero result even when Z lies exactly on edge + * PQ. + * + *

        Requires that P and Q do not project to antipodal points (e.g., P == -Q) (see comments in + * compareEdgeDistance). + */ + public static int edgeCircumcenterSign(S2Point p, S2Point q, S2Point a, S2Point b, S2Point c) { + // Check that the edge does not consist of antipodal points. This catches the most common case, + // see EdgeCircumcenterSign.exact for a full test. + // assert !p.equals(q.neg()); + + int abc = sign(a, b, c); + int sign = EdgeCircumcenterSign.triage(p, q, a, b, c, abc); + if (sign != 0) { + return sign; + } + + // Optimization for the cases that are going to return zero anyway, in order to avoid falling + // back to exact arithmetic. + if (p.equalsPoint(q) || a.equalsPoint(b) || b.equalsPoint(c) || c.equalsPoint(a)) { + return 0; + } + + sign = EdgeCircumcenterSign.exact(p, q, a, b, c, abc); + if (sign != 0) { + return sign; + } + + return EdgeCircumcenterSign.sos(p, q, a, b, c); + } + + /** + * A predicate for whether an edge PQ passes to the left, to the right, or through the center of + * the circumcircle of triangle ABC. Useful to determine the orientation of an edge with respect + * to the centers of a Voronoi diagram. Returns sign(P, Q, Z) where Z is the circumcenter of + * triangle ABC. The return value is -1 if Z is to the left of edge PQ, and +1 if Z is to the + * right of edge PQ. The return value is zero if the triangle has two or more exactly duplicate + * vertices, or if the result is indeterminate. + */ + static class EdgeCircumcenterSign { + /** No instantiation. */ + private EdgeCircumcenterSign() {} + + /** Returns a double-based test result. Faster but has a larger margin of uncertainty. */ + public static int triage(S2Point p, S2Point q, S2Point a, S2Point b, S2Point c, int abc) { + // Compute the circumcenter Z of triangle ABC, and then test which side of edge PQ it lies on. + double[] zError = {0}; + S2Point z = circumcenter(a, b, c, zError); + S2Point nx = ndCross(p, q); + + // When triangle ABC sign is negative, we have computed -Z and the result should be negated. + double result = abc * nx.dotProd(z); + double zLen = z.norm(); + double nxLen = nx.norm(); + double nxError = ((1 + 2 * Math.sqrt(3)) * nxLen + 32 * Math.sqrt(3) * DBL_ERR) * T_ERR; + double resultError = ((3 * T_ERR * nxLen + nxError) * zLen + zError[0] * nxLen); + return signum(result, resultError); + } + + /** Returns a BigDecimal-based test result. Exact but very slow. */ + public static int exact(S2Point p, S2Point q, S2Point a, S2Point b, S2Point c, int abc) { + return exact(big(p), big(q), big(a), big(b), big(c), abc); + } + + /** Returns a BigDecimal-based test result. Exact but very slow. */ + public static int exact(BigPoint p, BigPoint q, BigPoint a, BigPoint b, BigPoint c, int abc) { + // Return zero if the edge PQ is degenerate. (Also see the comments in sosTest.) + if (p.isLinearlyDependent(q)) { + // assert p.dotProd(q).signum() > 0; // Antipodal edges not allowed. + return 0; + } + + // The simplest predicate for testing whether the sign is positive is + // + // (1) (P x Q) . (|C|(A x B) + |A|(B x C) + |B|(C x A)) > 0 + // + // where |A| denotes A.norm() and the expression after the "." represents the circumcenter of + // triangle ABC. (This predicate is terrible from a numerical accuracy point of view, but + // that doesn't matter since we are going to use exact arithmetic.) This predicate also + // assumes that triangle ABC is CCW (positive sign); we correct for that below. + // + // The only problem with evaluating this inequality is that computing |A|, |B| and |C| + // requires square roots. To avoid this problem we use the standard technique of rearranging + // the inequality to isolate at least one square root and then squaring both sides. We need + // to repeat this process twice in order to eliminate all the square roots, which leads to a + // polynomial predicate of degree 20 in the input arguments. + // + // Rearranging (1) we get + // + // (P x Q) . (|C|(A x B) + |A|(B x C)) > |B|(P x Q) . (A x C) + // + // Before squaring we need to check the sign of each side. If the signs are different then we + // know the result without squaring, and if the signs are both negative then after squaring + // both sides we need to invert the result. Define + // + // dAB = (P x Q) . (A x B) + // dBC = (P x Q) . (B x C) + // dCA = (P x Q) . (C x A) + // + // Then we can now write the inequality above as + // + // (2) |C| dAB + |A| dBC > -|B| dCA + // + // The RHS of (2) is positive if dCA < 0, and the LHS of (2) is positive if + // (|C| dAB + |A| dBC) > 0. Since the LHS has square roots, we need to eliminate them using + // the same process. Rewriting the LHS as + // + // (3) |C| dAB > -|A| dBC + // + // we again the signs of both sides. Let's start with that. We also precompute the following + // values because they are used repeatedly when squaring various expressions below: + // + // abc2 = |A|^2 dBC^2 + // bca2 = |B|^2 dCA^2 + // cab2 = |C|^2 dAB^2 + BigPoint nx = p.crossProd(q); + BigDecimal dab = nx.dotProd(a.crossProd(b)); + BigDecimal dbc = nx.dotProd(b.crossProd(c)); + BigDecimal dca = nx.dotProd(c.crossProd(a)); + BigDecimal abc2 = a.norm2().multiply(square(dbc)); + BigDecimal bca2 = b.norm2().multiply(square(dca)); + BigDecimal cab2 = c.norm2().multiply(square(dab)); + + // If the two sides of (3) have different signs (including the case where one side is zero) + // then we know the result. Also, if both sides are zero then we know the result. The + // following logic encodes this. + int lhsSign3 = dab.signum(); + int rhs3Sign = -dbc.signum(); + int lhsSign2 = Math.max(-1, Math.min(1, lhsSign3 - rhs3Sign)); + if (lhsSign2 == 0 && lhsSign3 != 0) { + // Both sides of (3) have the same non-zero sign, so square both sides. If both sides were + // negative then invert the result. + lhsSign2 = cab2.compareTo(abc2) * lhsSign3; + // cab2=4, abc2=2, cab2-abc2=2, compareTo(4,2)=1 + } + // Now if the two sides of (2) have different signs then the result of this function is known. + int rhsSign2 = -dca.signum(); + int result = Math.max(-1, Math.min(1, lhsSign2 - rhsSign2)); + if (result == 0 && lhsSign2 != 0) { + // Both sides of (2) have the same non-zero sign, so square both sides. If both sides were + // negative then we invert the result below. This gives + // + // |C|^2 dAB^2 + |A|^2 dBC^2 + 2 |A| |C| dAB dBC > |B|^2 dCA^2 + // + // This expression still has square roots (|A| and |C|), so we rewrite as + // + // (4) 2 |A| |C| dAB dBC > |B|^2 dCA^2 - |C|^2 dAB^2 - |A|^2 dBC^2 . + // + // Again, if the two sides have different signs then we know the result. + int lhsSign4 = dab.signum() * dbc.signum(); + BigDecimal rhs4 = bca2.subtract(cab2).subtract(abc2); + result = Math.max(-1, Math.min(1, lhsSign4 - rhs4.signum())); + if (result == 0 && lhsSign4 != 0) { + // Both sides of (4) have the same non-zero sign, so square both sides. If both sides were + // negative then invert the result. + result = FOUR.multiply(abc2).multiply(cab2).compareTo(square(rhs4)) * lhsSign4; + } + // Correct the sign if both sides of (2) were negative. + result *= lhsSign2; + } + // If the sign of triangle ABC is negative, we have computed -Z so negate the result. + return abc * result; + } + + /** + * Given the exact test resulted in 0, returns a Simulation of Simplicity-based test result, + * that can only result in zero if P == Q, A == B, B == C, or C == A (the result will be nonzero + * if these pairs are exactly proportional to each other but not equal.) + */ + public static int sos(S2Point p, S2Point q, S2Point a, S2Point b, S2Point c) { + // We use the same perturbation strategy as SymbolicCompareDistances. Note that pedestal + // perturbations of P and Q do not affect the result, because Sign(P, Q, Z) does not change + // when its arguments are scaled by a positive factor. Therefore we only need to consider + // A, B, C. Suppose that A is the smallest lexicographically and therefore has the largest + // perturbation. This has the effect of perturbing the circumcenter of ABC slightly towards + // A, and since the circumcenter Z was previously exactly collinear with edge PQ, this implies + // that after the perturbation sign(P, Q, Z) == unperturbedSign(P, Q, A). We want the result + // to be zero if P, Q, and A are linearly dependent, rather than using symbolic perturbations, + // because these perturbations are defined to be much, much smaller than the pedestal + // perturbation of B and C that are considered below.) + // + // If A is also exactly collinear with edge PQ, then we move on to the next smallest point + // lexicographically out of {B, C}. It is easy to see that as long as A, B, C are all + // distinct, one of these three Sign calls will be nonzero, because if A, B, C are all + // distinct and collinear with edge PQ then their circumcenter Z coincides with the normal of + // PQ, and therefore Sign(P, Q, Z) is nonzero. + // + // This function could be extended to handle the case where P and Q are linearly dependent as + // follows. First, suppose that every point has both a pedestal peturbation as described + // above, and also the three axis-aligned perturbations described in the "Simulation of + // Simplicity" paper, where all pedestal perturbations are defined to be much, much larger + // than any axis-aligned perturbation. Note that since pedestal perturbations have no effect + // on 'sign', we can use this model for *all* the S2 predicates, which ensures that all the + // various predicates are fully consistent with each other. + // + // With this model, the strategy described above yields the correct result unless P and Q are + // exactly linearly dependent. When that happens, then no perturbation (pedestal or + // axis-aligned) of A,B,C affects the result, and no pedestal perturbation of P or Q affects + // the result, therefore we need to consider the smallest axis-aligned perturbation of P or Q. + // The first perturbation that makes P and Q linearly independent yields the result. + // Supposing that P < Q, this is the perturbation of P[2] unless both points are multiples of + // [0, 0, 1], in which case it is the perturbation of P[1]. The sign test can be implemented + // by computing the perturbed cross product of P and Q and taking the dot product with the + // exact value of Z. For example if P[2] is perturbed, the perturbed cross product is + // proportional to (0, 0, 1) x Q = (-Q[1], Q[0], 0). Note that if the dot product with Z is + // exactly zero, then it is still necessary to fall back to pedestal perturbations of A, B, C, + // but one of these perturbations is now guaranteed to succeed. + + // If any two triangle vertices are equal, the result is zero. + if (a.equalsPoint(b) || b.equalsPoint(c) || c.equalsPoint(a)) { + return 0; + } + + // Sort A, B, C in lexicographic order. + if (b.compareTo(a) < 0) { + S2Point temp = a; + a = b; + b = temp; + } + if (c.compareTo(b) < 0) { + S2Point temp = b; + b = c; + c = temp; + } + if (b.compareTo(a) < 0) { + S2Point temp = a; + a = b; + b = temp; + } + + // Now consider the perturbations in decreasing order of size. + int sign = Sign.sign(p, q, a, false); + if (sign != 0) { + return sign; + } + sign = Sign.sign(p, q, b, false); + if (sign != 0) { + return sign; + } + return Sign.sign(p, q, c, false); + } + } + + /** + * Given two sites A and B that form the center of caps of radius 'r', this indicates which sites + * are irrelevant to the Voronoi diagram relative to an edge PQ. + */ + public enum Excluded { + /** The first site is excluded, i.e. always further from the edge than the second site. */ + FIRST, + /** The second site is excluded, i.e. always further from the edge than the first site. */ + SECOND, + /** Neither site is excluded, i.e. both sites are closer to part of the edge than the other. */ + NEITHER, + /** The algorithm could not robustly determine the exclusion, or A is exactly equal to B. */ + UNCERTAIN + } + + /** + * This is a specialized method that is used to compute the intersection of an edge PQ with the + * Voronoi diagram of a set of points, where each Voronoi region is intersected with a disc of + * fixed radius "r". + * + *

        Given two sites A and B and an edge (P, Q) such that {@code d(A,P) < d(B,P)}, and both sites + * are within the given distance "r" of edge PQ, this method intersects the Voronoi region of each + * site with a disc of radius r and determines whether either region has an empty intersection + * with edge PQ. It returns FIRST if site A has an empty intersection, SECOND if site B has an + * empty intersection, NEITHER if neither site has an empty intersection, or UNCERTAIN if A == B + * exactly. Note that it is not possible for both intersections to be empty because of the + * requirement that both sites are within distance r of edge PQ. (For example, the only reason + * that Voronoi region A can have an empty intersection with PQ is that site B is closer to all + * points on PQ that are within radius r of site A.) + * + *

        The result is determined with respect to the positions of all points as though they were + * projected to lie exactly on the surface of the unit sphere. Furthermore this method uses + * symbolic perturbations to compute a consistent non-zero result even when A and B lie on + * opposite sides of PQ such that the Voronoi edge between them exactly coincides with edge PQ, or + * when A and B are distinct but project to the same point on the sphere (i.e., they are linearly + * dependent). + * + *

        Requires that + * + *

          + *
        • {@code r < S1ChordAngle.RIGHT} (90 degrees) + *
        • {@code compareDistances(p, a, b) < 0} + *
        • {@code compareEdgeDistance(a, p, q, r) <= 0} + *
        • {@code compareEdgeDistance(b, p, q, r) <= 0} + *
        • P and Q do not project to antipodal points (e.g., P != -Q), see {@link + * CompareEdgeDistance} for details. + *
        + */ + public static Excluded getVoronoiSiteExclusion( + S2Point a, S2Point b, S2Point p, S2Point q, double r2) { + // assert r2 < 2.0; // Less than a right angle. + // assert compareDistances(p, a, b) < 0; // (implies a != b) + // assert compareEdgeDistance(a, p, q, r2) <= 0; + // assert compareEdgeDistance(b, p, q, r2) <= 0; + + // Check that the edge does not consist of antipodal points. This catches the most common case, + // see VoronoiSiteExclusion.exact for the full test. + // assert !p.equals(q.neg()); + + // If one site is closer than the other to both endpoints of PQ, then it is closer to every + // point on PQ. Note that this also handles the case where A and B are equidistant from every + // point on PQ (i.e., PQ is the perpendicular bisector of AB), because CompareDistances uses + // symbolic perturbations to ensure that A or B is considered closer, in a consistent way. This + // also ensures that the choice of A or B does not depend on the direction of PQ. + if (compareDistances(q, a, b) < 0) { + // Site A is closer to every point on PQ. + return Excluded.SECOND; + } else { + Excluded result = VoronoiSiteExclusion.triage(a, b, p, q, r2); + return result != Excluded.UNCERTAIN ? result : VoronoiSiteExclusion.exact(a, b, p, q, r2); + } + } + + /** + * A test for which (if any) of two Voronoi sites within R of an edge PQ are covered by the other. + * + *

        Does not offer generally correct results for all inputs, so that multiple strategies may be + * implemented for different classes of input. + */ + static class VoronoiSiteExclusion { + /** No instantiation. */ + private VoronoiSiteExclusion() {} + + /** An exact representation of a right angle. */ + static final BigDecimal R90 = big(S1ChordAngle.RIGHT.getLength2()); + + /** A site exclusion test using double arithmetic. */ + public static Excluded triage(S2Point a, S2Point b, S2Point p, S2Point q, double r2) { + // Define the "coverage disc" of a site S to be the disc centered at S with radius r (i.e. + // squared chord angle length r2). Similarly, define the "coverage interval" of S along an + // edge PQ to be the intersection of PQ with the coverage disc of S. The coverage interval + // can be represented as the point at the center of the interval and an angle that measures + // the semi-width or "radius" of the interval. + // + // To test whether site A excludes site B along the input edge PQ, we test whether the + // coverage interval of A contains the coverage interval of B. Let "ra" and "rb" be the radii + // (semi-widths) of the two intervals, and let "d" be the angle between their center points. + // Then "a" properly contains "b" if (ra - rb > d), and "b" contains "a" if (rb - ra > d). + // Note that only one of these conditions can be true. Therefore we can determine whether one + // site excludes the other by checking whether + // + // (1) |rb - ra| > d + // + // and use the sign of (rb - ra) to determine which site is excluded. + // + // The actual code is based on the following. Let A1 and B1 be the unit vectors A and B + // scaled by cos(r) (these points are inside the sphere). The planes perpendicular to OA1 and + // OA2 cut off two discs of radius r around A and B. Now consider the two lines (inside the + // sphere) where these planes intersect the plane containing the input edge PQ, and let A2 and + // B2 be the points on these lines that are closest to A and B. The coverage intervals of A + // and B can be represented as an interval along each of these lines, centered at A2 and B2. + // Let P1 and P2 be the endpoints of the coverage interval for A, and let Q1 and Q2 be the + // endpoints of the coverage interval for B. We can view each coverage interval as either a + // chord through the sphere's interior, or as a segment of the original edge PQ (by projecting + // the chord onto the sphere's surface). + // + // To check whether B's interval is contained by A's interval, we test whether both endpoints + // of B's interval (Q1 and Q2) are contained by A's interval. E.g., we could test whether + // Qi.dotProd(A2) > A2.norm2(). + // + // However rather than constructing the actual points A1, A2, and so on, it turns out to be + // more efficient to compute the sines and cosines ("components") of the various angles and + // then use trigonometric identities. Predicate (1) can be expressed as + // + // |sin(rb - ra)| > sin(d) + // + // provided that |d| <= Pi/2 (which must be checked), and then expanded to + // + // (2) |sin(rb) cos(ra) - sin(ra) cos(rb)| > sin(d) . + // + // The components of the various angles can be expressed using dot and cross products based on + // the construction above: + // + // sin(ra) = sqrt(sin^2(r) |a|^2 |n|^2 - |a.n|^2) / |aXn| + // cos(ra) = cos(r) |a| |n| / |aXn| + // sin(rb) = sqrt(sin^2(r) |b|^2 |n|^2 - |b.n|^2) / |bXn| + // cos(rb) = cos(r) |b| |n| / |bXn| + // sin(d) = (aXb).n |n| / (|aXn| |bXn|) + // cos(d) = (aXn).(bXn) / (|aXn| |bXn|) + // + // Also, the squared chord length r2 is equal to 4 * sin^2(r / 2), which yields the following + // relationships: + // + // sin(r) = sqrt(r2 (1 - r2 / 4)) + // cos(r) = 1 - r2 / 2 + // + // We then scale both sides of (2) by |aXn| |bXn| / |n| (in order to minimize the number of + // calculations and to avoid divisions), which gives: + // + // cos(r) ||a| sqrt(sin^2(r) |b|^2 |n|^2 - |b.n|^2) - + // |b| sqrt(sin^2(r) |a|^2 |n|^2 - |a.n|^2)| > (aXb).n + // + // Furthermore we can substitute |a| = |b| = 1 (as long as this is taken into account in the + // error bounds), yielding + // + // (3) cos(r) |sqrt(sin^2(r) |n|^2 - |b.n|^2) - + // sqrt(sin^2(r) |n|^2 - |a.n|^2)| > (aXb).n + // + // The code below is more complicated than this because many expressions have been modified + // for better numerical stability. For example, dot products between unit vectors are + // computed using (x - y).dotProd(x + y), and the dot product between a point P and the normal + // N of an edge X is measured using (P - Xi).dotProd(N) where Xi is the endpoint of X that is + // closer to P. + + S2Point n = ndCross(p, q); // 2 * p.crossProd(q) + double n2 = n.norm2(); + double n1 = Math.sqrt(n2); + // This factor is used in the error terms of dot products with "n" below. + double dnError = ((3.5 + 2 * Math.sqrt(3)) * n1 + 32 * Math.sqrt(3) * DBL_ERR) * T_ERR; + + double cosR = 1 - 0.5 * r2; + double sin2r = r2 * (1 - 0.25 * r2); + double n2sin2r = n2 * sin2r; + + // "ra" and "rb" denote sin(ra) and sin(rb) after the scaling above. + double[] d = {0}; + double aDn = a.sub(closestVertex(a, p, q, d)).dotProd(n); + double aDn2 = aDn * aDn; + double aDnError = dnError * Math.sqrt(d[0]); + double ra2 = n2sin2r - aDn2; + double ra2Error = + (8 * DBL_ERR + 4 * T_ERR) * aDn2 + + (2 * Math.abs(aDn) + aDnError) * aDnError + + 6 * T_ERR * n2sin2r; + // This is the minimum possible value of ra2, which is used to bound the derivative of + // sqrt(ra2) in computing raError below. + double minRa2 = ra2 - ra2Error; + if (minRa2 < 0) { + return Excluded.UNCERTAIN; + } + double ra = Math.sqrt(ra2); + // Includes the ra2 subtraction error above. + double raError = 1.5 * T_ERR * ra + 0.5 * ra2Error / Math.sqrt(minRa2); + + double[] bx2 = {0}; + double bDn = b.sub(closestVertex(b, p, q, bx2)).dotProd(n); + double bDn2 = bDn * bDn; + double bDnError = dnError * Math.sqrt(bx2[0]); + double rb2 = n2sin2r - bDn2; + double rb2Error = + (8 * DBL_ERR + 4 * T_ERR) * bDn2 + + (2 * Math.abs(bDn) + bDnError) * bDnError + + 6 * T_ERR * n2sin2r; + double minRb2 = rb2 - rb2Error; + if (minRb2 < 0) { + return Excluded.UNCERTAIN; + } + double rb = Math.sqrt(rb2); + // Includes the rb2 subtraction error above. + double rbError = 1.5 * T_ERR * rb + 0.5 * rb2Error / Math.sqrt(minRb2); + + // The sign of LHS(3) determines which site may be excluded by the other. + double lhs3 = cosR * (rb - ra); + double absLhs3 = Math.abs(lhs3); + double lhs3Error = cosR * (raError + rbError) + 3 * T_ERR * absLhs3; + + // Now we evaluate the RHS of (3), which is proportional to sin(d). + S2Point aXb = ndCross(a, b); // 2 * a.crossProd(b) + double aXb1 = aXb.norm(); + double sinD = 0.5 * aXb.dotProd(n); + double sinDError = + (4 * DBL_ERR + (2.5 + 2 * Math.sqrt(3)) * T_ERR) * aXb1 * n1 + + 16 * Math.sqrt(3) * DBL_ERR * T_ERR * (aXb1 + n1); + + // If LHS(3) is definitely less than RHS(3), neither site excludes the other. + double result = absLhs3 - sinD; + double resultError = lhs3Error + sinDError; + if (result < -resultError) { + return Excluded.NEITHER; + } + + // Otherwise, before proceeding further we need to check that |d| <= Pi/2. In fact, |d| < Pi/2 + // is enough because of the requirement that r < Pi/2. The following expression represents + // cos(d) after scaling; it is equivalent to (aXn).(bXn) but has about 30% less error. + double cosD = a.dotProd(b) * n2 - aDn * bDn; + double cosDError = + ((8 * DBL_ERR + 5 * T_ERR) * Math.abs(aDn) + aDnError) * Math.abs(bDn) + + (Math.abs(aDn) + aDnError) * bDnError + + (8 * DBL_ERR + 8 * T_ERR) * n2; + if (cosD <= -cosDError) { + return Excluded.NEITHER; + } + + // Potential optimization: if the sign of cos(d) is uncertain, then instead we could check + // whether cos(d) >= cos(r). Unfortunately this is fairly expensive since it requires + // computing denominator |aXn||bXn| of cos(d) and the associated error bounds. In any case + // this case is relatively rare so it seems better to punt. + if (cosD < cosDError) { + return Excluded.UNCERTAIN; + } + + // Normally we have d > 0 because the sites are sorted so that A is closer to P and B is + // closer to Q. However if the edge PQ is longer than Pi/2, and the sites A and B are beyond + // its endpoints, then AB can wrap around the sphere in the opposite direction from PQ. In + // this situation d < 0 but each site is closest to one endpoint of PQ, so neither excludes + // the other. + // + // It turns out that this can happen only when the site that is further away from edge PQ is + // less than 90 degrees away from whichever endpoint of PQ it is closer to. It is provable + // that if this distance is less than 90 degrees, then it is also less than r2, and therefore + // the Voronoi regions of both sites intersect the edge. + if (sinD < -sinDError) { + double r90 = S1ChordAngle.RIGHT.getLength2(); + // "ca" is negative if Voronoi region A definitely intersects edge PQ. + int ca = (lhs3 < -lhs3Error) ? -1 : CompareDistance.triageCos(a, p, r90); + int cb = (lhs3 > lhs3Error) ? -1 : CompareDistance.triageCos(b, q, r90); + if (ca < 0 && cb < 0) { + return Excluded.NEITHER; + } + if (ca <= 0 && cb <= 0) { + return Excluded.UNCERTAIN; + } + if (absLhs3 <= lhs3Error) { + return Excluded.UNCERTAIN; + } + } else if (sinD <= sinDError) { + return Excluded.UNCERTAIN; + } + + // Now we can finish checking the results of predicate (3). + if (result <= resultError) { + return Excluded.UNCERTAIN; + } + // assert absLhs3 > lhs3Error; + return (lhs3 > 0) ? Excluded.FIRST : Excluded.SECOND; + } + + public static Excluded exact(S2Point a, S2Point b, S2Point p, S2Point q, double r2) { + return exact(big(a), big(b), big(p), big(q), big(r2)); + } + + /** A site exclusion test using BigDecimal arithmetic. */ + public static Excluded exact(BigPoint a, BigPoint b, BigPoint p, BigPoint q, BigDecimal r2) { + // assert !p.isAntipodal(q); + + // Recall that one site excludes the other if + // + // (1) |sin(rb - ra)| > sin(d) + // + // and that the sign of (rb - ra) determines which site is excluded; see the comments in + // triage(). To evaluate this using exact arithmetic, we expand this to the same predicate as + // before: + // + // (2) cos(r) ||a| sqrt(sin^2(r) |b|^2 |n|^2 - |b.n|^2) - + // |b| sqrt(sin^2(r) |a|^2 |n|^2 - |a.n|^2)| > (aXb).n + // + // We also need to verify that d <= Pi/2, which is implemented by checking that sin(d) >= 0 + // and cos(d) >= 0. + // + // To eliminate the square roots we use the standard technique of rearranging the inequality + // to isolate at least one square root and then squaring both sides. We need to repeat this + // process twice in order to eliminate all the square roots, which leads to a polynomial + // predicate of degree 20 in the input arguments (i.e. degree 4 in each of "a", "b", "p", "q", + // and "r2"). + // + // Before squaring we need to check the sign of each side. We also check the condition that + // cos(d) >= 0. Given what else we need to compute, it is cheaper use the identity + // + // (aXn).(bXn) = (a.b) |n|^2 - (a.n)(b.n) + BigPoint n = p.crossProd(q); + BigDecimal n2 = n.norm2(); + BigDecimal aDn = a.dotProd(n); + BigDecimal bDn = b.dotProd(n); + int cosDSign = a.dotProd(b).multiply(n2).compareTo(aDn.multiply(bDn)); + if (cosDSign < 0) { + return Excluded.NEITHER; + } + + // Otherwise we continue evaluating the LHS of (2), defining + // sa = |b| sqrt(sin^2(r) |a|^2 |n|^2 - |a.n|^2) + // sb = |a| sqrt(sin^2(r) |b|^2 |n|^2 - |b.n|^2) . + // The sign of the LHS of (2) (before taking the absolute value) determines which coverage + // interval is larger and therefore which site is potentially being excluded. + BigDecimal a2 = a.norm2(); + BigDecimal b2 = b.norm2(); + BigDecimal n2Sin2R = r2.multiply(BigDecimal.ONE.subtract(QUARTER.multiply(r2))).multiply(n2); + BigDecimal sa2 = b2.multiply(n2Sin2R.multiply(a2).subtract(square(aDn))); + BigDecimal sb2 = a2.multiply(n2Sin2R.multiply(b2).subtract(square(bDn))); + int lhsSign2 = sb2.compareTo(sa2); + + // If the RHS of (2) is negative (corresponding to sin(d) < 0), then we need to consider the + // possibility that the edge AB wraps around the sphere in the opposite direction from edge + // PQ, with the result that neither site excludes the other; see triage(). + BigDecimal rhs2 = a.crossProd(b).dotProd(n); + int rhsSign2 = rhs2.signum(); + if (rhsSign2 < 0) { + int ca = (lhsSign2 < 0) ? -1 : CompareDistance.exact(a, p, R90); + int cb = (lhsSign2 > 0) ? -1 : CompareDistance.exact(b, q, R90); + if (ca <= 0 && cb <= 0) { + return Excluded.NEITHER; + } + // assert ca != 1 || cb != 1; + return ca == 1 ? Excluded.FIRST : Excluded.SECOND; + } + if (lhsSign2 == 0) { + // If the RHS of (2) is zero as well (i.e., d == 0) then both sites are equidistant from + // every point on edge PQ. This case requires symbolic perturbations, but it should already + // have been handled in getVoronoiSiteExclusion(); see the call to CompareDistances. + // assert rhsSign2 > 0; + return Excluded.NEITHER; + } + + // Next we square both sides of (2), yielding + // + // cos^2(r) (sb^2 + sa^2 - 2 sa sb) > (aXb.n)^2 + // + // which can be rearranged to give + // + // (3) cos^2(r) (sb^2 + sa^2) - (aXb.n)^2 > 2 cos^2(r) sa sb . + // + // The RHS of (3) is always non-negative, but we still need to check the sign of the LHS. + BigDecimal cosR = BigDecimal.ONE.subtract(HALF.multiply(r2)); + BigDecimal cos2R = square(cosR); + BigDecimal lhs3 = cos2R.multiply(sa2.add(sb2)).subtract(square(rhs2)); + if (lhs3.signum() < 0) { + return Excluded.NEITHER; + } + + // Otherwise we square both sides of (3) to obtain: + // + // (4) LHS(3)^2 > 4 cos^4(r) sa^2 sb^2 + BigDecimal lhs4 = square(lhs3); + BigDecimal rhs4 = FOUR.multiply(square(cos2R)).multiply(sa2).multiply(sb2); + int result = lhs4.compareTo(rhs4); + if (result < 0) { + return Excluded.NEITHER; + } + if (result == 0) { + // We have |rb - ra| = d and d > 0. This implies that one coverage interval contains the + // other, but not properly: the two intervals share a common endpoint. The distance from + // each site to that point is exactly "r", therefore we need to use symbolic perturbations. + // Recall that site A is considered closer to an equidistant point if and only if A > B. + // Therefore if (rb > ra && A > B) or (ra > rb && B > A) then each site is closer to at + // least one point and neither site is excluded. + // + // Ideally this logic would be in a separate SymbolicVoronoiSiteExclusion method for better + // testing, but this is not convenient because it needs lhsSign (which requires exact + // arithmetic to compute). + if ((lhsSign2 > 0) == (a.compareTo(b) > 0)) { + return Excluded.NEITHER; + } + } + + // At this point we know that one of the two sites is excluded. The sign of the LHS of (2) + // (before the absolute value) determines which one. + return lhsSign2 > 0 ? Excluded.FIRST : Excluded.SECOND; + } + } + + /** + * Returns (a-b).crossProd(a+b), which eliminates almost all of the error due to "x" and "y" being + * not quite unit length. This method is extremely accurate for small distances; the *relative* + * error in the result is O(DBL_ERR) for distances as small as DBL_ERR. + */ + // TODO(eengle): Update callers to instead use S2.robustCrossProd(b,a).neg()? + private static S2Point ndCross(S2Point a, S2Point b) { + return a.sub(b).crossProd(a.add(b)); + } + + /** Returns cos(XY). Requires that "x" and "y" are {@link S2#isUnitLength}. */ + private static double cosDistance(S2Point x, S2Point y) { + return x.dotProd(y); + } + + /** Returns the error in a value returned from {@link #cosDistance}. */ + private static double cosDistanceError(double x) { + return 9.5 * DBL_ERR * Math.abs(x) + 1.5 * DBL_ERR; + } + + /** Returns sin^2(XY), where XY=x.angle(y). Requires "x" and "y" to be {@link S2#isUnitLength}. */ + private static double sin2Distance(S2Point x, S2Point y) { + return 0.25 * ndCross(x, y).norm2(); + } + + /** Returns the error in a value returned from {@link #sin2Distance}. */ + private static double sin2DistanceError(double x) { + return (21 + 4 * Math.sqrt(3)) * DBL_ERR * x + + 32 * Math.sqrt(3) * DBL_ERR * DBL_ERR * Math.sqrt(x) + + 768 * DBL_ERR * DBL_ERR * DBL_ERR * DBL_ERR; + } + + /** Returns the same result as {@link Math#signum}, or 0 if 'value' is within 'error' of 0. */ + private static int signum(double value, double error) { + return value > error ? 1 : value < -error ? -1 : 0; + } + + /** + * Returns the same result as {@link Double#compare}, or 0 if 'a' and 'b' are within their + * measurement errors of each other. + * + * @param a the first value + * @param aError the true value of 'a' must be at least a-aError and at most a+aError + * @param b the second value + * @param bError the true value of 'b' must be at least b-bError and at most b+bError + */ + private static int compare(double a, double aError, double b, double bError) { + double diff = a - b; + double error = aError + bError; + return (diff > error) ? 1 : (diff < -error) ? -1 : 0; + } + + /** + * Returns "a" or "b", whichever is closer to "x". Also returns the squared distance from the + * returned point to "x" in "d". + */ + private static S2Point closestVertex(S2Point x, S2Point a, S2Point b, double[] dx2) { + double ax2 = a.getDistance2(x); + double bx2 = b.getDistance2(x); + if (ax2 < bx2 || (ax2 == bx2 && a.lessThan(b))) { + dx2[0] = ax2; + return a; + } else { + dx2[0] = bx2; + return b; + } + } + + /** + * If triangle ABC has positive sign, returns its circumcenter. If ABC has negative sign, returns + * the negated circumcenter. + */ + private static S2Point circumcenter(S2Point a, S2Point b, S2Point c, double[] error) { + // We compute the circumcenter using the intersection of the perpendicular bisectors of AB and + // BC. The formula is essentially + // + // Z = ((A x B) x (A + B)) x ((B x C) x (B + C)), + // + // except that we compute the cross product (A x B) as (A - B) x (A + B) (and similarly for + // B x C) since this is much more stable when the inputs are unit vectors. + S2Point abDiff = a.sub(b); + S2Point abSum = a.add(b); + S2Point bcDiff = b.sub(c); + S2Point bcSum = b.add(c); + S2Point nab = abDiff.crossProd(abSum); + double nabLen = nab.norm(); + double abLen = abDiff.norm(); + S2Point nbc = bcDiff.crossProd(bcSum); + double nbcLen = nbc.norm(); + double bcLen = bcDiff.norm(); + S2Point mab = nab.crossProd(abSum); + S2Point mbc = nbc.crossProd(bcSum); + error[0] = + ((16 + 24 * Math.sqrt(3)) * T_ERR + 8 * DBL_ERR * (abLen + bcLen)) * nabLen * nbcLen + + 128 * Math.sqrt(3) * DBL_ERR * T_ERR * (nabLen + nbcLen) + + 3 * 4096 * DBL_ERR * DBL_ERR * T_ERR * T_ERR; + return mab.crossProd(mbc); + } + + /** Returns a BigDecimal-based representation of 'p'. */ + private static BigPoint big(S2Point p) { + return new BigPoint(p); + } + + /** Returns a BigDecimal-based representation of 'v'. */ + private static BigDecimal big(double v) { + return Platform.newBigDecimal(v); + } + + /** Returns v*v. */ + private static BigDecimal square(BigDecimal v) { + return v.multiply(v); + } + + // Don't instantiate + private S2Predicates() {} +} diff --git a/core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/S2Projections.java b/core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/S2Projections.java new file mode 100644 index 0000000000..8adc31e5ab --- /dev/null +++ b/core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/S2Projections.java @@ -0,0 +1,930 @@ +/* + * Copyright 2005 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.mogo.eagle.core.utilcode.geometry; + +/** + * This class specifies the coordinate systems and transforms used to project points from the sphere + * to the unit cube to an {@link S2CellId}. + * + *

        In the process of converting a latitude-longitude pair to a 64-bit cell id, the following + * coordinate systems are used: + * + *

          + *
        • (id): An S2CellId is a 64-bit encoding of a face and a Hilbert curve position on that face. + * The Hilbert curve position implicitly encodes both the position of a cell and its + * subdivision level (see s2cellid.h). + *
        • (face, i, j): Leaf-cell coordinates. "i" and "j" are integers in the range [0,(2**30)-1] + * that identify a particular leaf cell on the given face. The (i, j) coordinate system is + * right-handed on each face, and the faces are oriented such that Hilbert curves connect + * continuously from one face to the next. + *
        • (face, s, t): Cell-space coordinates. "s" and "t" are real numbers in the range [0,1] that + * identify a point on the given face. For example, the point (s, t) = (0.5, 0.5) corresponds + * to the center of the top-level face cell. This point is also a vertex of exactly four cells + * at each subdivision level greater than zero. + *
        • (face, si, ti): Discrete cell-space coordinates. These are obtained by multiplying "s" and + * "t" by 2**31 and rounding to the nearest unsigned integer. Discrete coordinates lie in the + * range [0,2**31]. This coordinate system can represent the edge and center positions of all + * cells with no loss of precision (including non-leaf cells). In binary, each coordinate of a + * level-k cell center ends with a 1 followed by (30 - k) 0s. The coordinates of its edges end + * with (at least) (31 - k) 0s. + *
        • (face, u, v): Cube-space coordinates. To make the cells at each level more uniform in size + * after they are projected onto the sphere, we apply a nonlinear transformation of the form + * u=f(s), v=f(t). The (u, v) coordinates after this transformation give the actual + * coordinates on the cube face (modulo some 90 degree rotations) before it is projected onto + * the unit sphere. + *
        • (face, u, v, w): Per-face coordinate frame. This is an extension of the (face, u, v) + * cube-space coordinates that adds a third axis "w" in the direction of the face normal. It + * is always a right-handed 3D coordinate system. Cube-space coordinates can be converted to + * this frame by setting w=1, while (u,v,w) coordinates can be projected onto the cube face by + * dividing by w, i.e. (face, u/w, v/w). + *
        • (x, y, z): Direction vector (S2Point). Direction vectors are not necessarily unit length, + * and are often chosen to be points on the biunit cube [-1,+1]x[-1,+1]x[-1,+1]. They can be + * normalized to obtain the corresponding point on the unit sphere. + *
        • (lat, lng): Latitude and longitude (S2LatLng). Latitudes must be between -90 and 90 degrees + * inclusive, and longitudes must be between -180 and 180 degrees inclusive. + *
        + * + *

        Note that the (i, j), (s, t), (si, ti), and (u, v) coordinate systems are right-handed on all + * six faces. + * + *

        We have implemented three different projections from cell-space (s,t) to cube-space (u,v): + * {@link S2Projections#S2_LINEAR_PROJECTION}, {@link S2Projections#S2_TAN_PROJECTION}, and {@link + * S2Projections#S2_QUADRATIC_PROJECTION}. The default is in {@link S2Projections#PROJ}, and uses + * the quadratic projection since it has the best overall behavior. + * + *

        Here is a table comparing the cell uniformity using each projection. "Area Ratio" is the + * maximum ratio over all subdivision levels of the largest cell area to the smallest cell area at + * that level, "Edge Ratio" is the maximum ratio of the longest edge of any cell to the shortest + * edge of any cell at the same level, and "Diag Ratio" is the ratio of the longest diagonal of any + * cell to the shortest diagonal of any cell at the same level. "ToPoint" and "FromPoint" are the + * times in microseconds required to convert cell IDs to and from points (unit vectors) + * respectively. "ToPointRaw" is the time to convert to a non-unit-length vector, which is all that + * is needed for some purposes. + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
        ProjectionArea RatioEdge RatioDiag RatioToPointRaw (microseconds)ToPoint (microseconds)FromPoint (microseconds)
        Linear5.2002.1172.9590.0200.0870.085
        Tangent1.4141.4141.7040.2370.2990.258
        Quadratic2.0821.8021.9320.0330.0960.108
        + * + *

        The worst-case cell aspect ratios are about the same with all three projections. The maximum + * ratio of the longest edge to the shortest edge within the same cell is about 1.4 and the maximum + * ratio of the diagonals within the same cell is about 1.7. + * + *

        This data was produced using {@code S2CellTest} and {@code S2CellIdTest}. + * + * @author eengle@google.com (Eric Engle) ported from util/geometry + */ +public strictfp enum S2Projections { + // All of the values below were obtained by a combination of hand analysis and + // Mathematica. In general, S2_TAN_PROJECTION produces the most uniform + // shapes and sizes of cells, S2_LINEAR_PROJECTION is considerably worse, and + // S2_QUADRATIC_PROJECTION is somewhere in between (but generally closer to + // the tangent projection than the linear one). + + /** + * This is the fastest transformation, but also produces the least uniform cell sizes. Cell areas + * vary by a factor of about 5.2, with the largest cells at the center of each face and the + * smallest cells in the corners. + */ + S2_LINEAR_PROJECTION( + 4 / (3 * Math.sqrt(3)), // minArea 0.770 + 4, // maxArea 4.000 + 1.0, // minAngleSpan 1.000 + 2, // maxAngleSpan 2.000 + Math.sqrt(2. / 3), // minWidth 0.816 + 1.411459345844456965, // avgWidth 1.411 + 2 * Math.sqrt(2) / 3, // minEdge 0.943 + 1.440034192955603643, // avgEdge 1.440 + 2 * Math.sqrt(2) / 3, // minDiag 0.943 + 2 * Math.sqrt(2), // maxDiag 2.828 + 2.031817866418812674, // avgDiag 2.032 + Math.sqrt(2)) { // maxEdgeAspect 1.414 + @Override + public double stToUV(double s) { + return 2 * s - 1; + } + + @Override + public double uvToST(double u) { + return 0.5 * (u + 1); + } + }, + /** + * Transforming the coordinates via atan() makes the cell sizes more uniform. The areas vary by a + * maximum ratio of 1.4 as opposed to a maximum ratio of 5.2. However, each call to atan() is + * about as expensive as all of the other calculations combined when converting from points to + * cell IDs, i.e. it reduces performance by a factor of 3. + */ + S2_TAN_PROJECTION( + (S2.M_PI * S2.M_PI) / (4 * Math.sqrt(2)), // minArea 1.745 + S2.M_PI * S2.M_PI / 4, // maxArea 2.467 + S2.M_PI / 2, // minAngleSpan 1.571 + S2.M_PI / 2, // maxAngleSpan 1.571 + S2.M_PI / (2 * Math.sqrt(2)), // minWidth 1.111 + 1.437318638925160885, // avgWidth 1.437 + S2.M_PI / (2 * Math.sqrt(2)), // minEdge 1.111 + 1.461667032546739266, // avgEdge 1.462 + S2.M_PI * Math.sqrt(2) / 3, // minDiag 1.481 + S2.M_PI * Math.sqrt(2. / 3), // maxDiag 2.565 + 2.063623197195635753, // avgDiag 2.064 + Math.sqrt(2)) { // maxEdgeAspect 1.414 + @Override + public double stToUV(double s) { + // Unfortunately, tan(M_PI_4) is slightly less than 1.0. This isn't due to a flaw in the + // implementation of tan(), it's because the derivative of tan(x) at x=pi/4 is 2, and it + // happens that the two adjacent floating point numbers on either side of the infinite- + // precision value of pi/4 have tangents that are slightly below and slightly above 1.0 when + // rounded to the nearest double-precision result. + s = Math.tan(S2.M_PI_2 * s - S2.M_PI_4); + return s + (1.0 / (1L << 53)) * s; + } + + @Override + public double uvToST(double u) { + return (2 * S2.M_1_PI) * (Math.atan(u) + S2.M_PI_4); + } + }, + /** + * This is an approximation of the tangent projection that is much faster and produces cells that + * are almost as uniform in size. It is about 3 times faster than the tangent projection for + * converting cell IDs to points or vice versa. Cell areas vary by a maximum ratio of about 2.1. + */ + S2_QUADRATIC_PROJECTION( + 8 * Math.sqrt(2) / 9, // minArea 1.257 + 2.635799256963161491, // maxArea 2.636 + 4. / 3, // minAngleSpan 1.333 + 1.704897179199218452, // maxAngleSpan 1.705 + 2 * Math.sqrt(2) / 3, // minWidth 0.943 + 1.434523672886099389, // avgWidth 1.435 + 2 * Math.sqrt(2) / 3, // minEdge 0.943 + 1.459213746386106062, // avgEdge 1.459 + 8 * Math.sqrt(2) / 9, // minDiag 1.257 + 2.438654594434021032, // maxDiag 2.439 + 2.060422738998471683, // avgDiag 2.060 + 1.442615274452682920) { // maxEdgeAspect 1.443 + @Override + public double stToUV(double s) { + if (s >= 0.5) { + return (1 / 3.) * (4 * s * s - 1); + } else { + return (1 / 3.) * (1 - 4 * (1 - s) * (1 - s)); + } + } + + @Override + public double uvToST(double u) { + if (u >= 0) { + return 0.5 * Math.sqrt(1 + 3 * u); + } else { + return 1 - 0.5 * Math.sqrt(1 - 3 * u); + } + } + }; + + /** + * The maximum value of an si- or ti-coordinate. The range of valid (si,ti) values is + * [0..MAX_SiTi]. + */ + public static final long MAX_SITI = 1L << (S2CellId.MAX_LEVEL + 1); + + /** The U,V,W axes for each face. */ + private static final S2Point[][] FACE_UVW_AXES = { + {S2Point.Y_POS, S2Point.Z_POS, S2Point.X_POS}, + {S2Point.X_NEG, S2Point.Z_POS, S2Point.Y_POS}, + {S2Point.X_NEG, S2Point.Y_NEG, S2Point.Z_POS}, + {S2Point.Z_NEG, S2Point.Y_NEG, S2Point.X_NEG}, + {S2Point.Z_NEG, S2Point.X_POS, S2Point.Y_NEG}, + {S2Point.Y_POS, S2Point.X_POS, S2Point.Z_NEG} + }; + + /** The precomputed neighbors of each face. See {@link #getUVWFace}. */ + private static final int[][][] FACE_UVW_FACES = { + {{4, 1}, {5, 2}, {3, 0}}, + {{0, 3}, {5, 2}, {4, 1}}, + {{0, 3}, {1, 4}, {5, 2}}, + {{2, 5}, {1, 4}, {0, 3}}, + {{2, 5}, {3, 0}, {1, 4}}, + {{4, 1}, {3, 0}, {2, 5}} + }; + + /** + * A transform from 3D cartesian coordinates to the 2D coordinates of a face. For (x, y, z) + * coordinates within the face, the resulting UV coordinates should each lie in the inclusive + * range [-1,1], with the center of the face along that axis at 0. + */ + public abstract static class UvTransform { + /** Internal implementations only. */ + private UvTransform() {} + + /** + * Returns the 'u' coordinate of the [u, v] point projected onto a cube face from the given [x, + * y, z] position. + */ + public abstract double xyzToU(double x, double y, double z); + + /** + * Returns the 'u' coordinate of the [u, v] point projected onto a cube face from the given [x, + * y, z] position. + */ + public final double xyzToU(S2Point p) { + return xyzToU(p.x, p.y, p.z); + } + + /** + * Returns the 'v' coordinate of the [u, v] point projected onto a cube face from the given [x, + * y, z] position. + */ + public abstract double xyzToV(double x, double y, double z); + + /** + * Returns the 'v' coordinate of the [u, v] point projected onto a cube face from the given [x, + * y, z] position. + */ + public final double xyzToV(S2Point p) { + return xyzToV(p.x, p.y, p.z); + } + } + + /** + * The transforms to convert (x, y, z) coordinates to u and v coordinates on a specific face, + * indexed by face. + */ + private static final UvTransform[] UV_TRANSFORMS = { + new UvTransform() { + @Override + public double xyzToU(double x, double y, double z) { + return y / x; + } + + @Override + public double xyzToV(double x, double y, double z) { + return z / x; + } + }, + new UvTransform() { + @Override + public double xyzToU(double x, double y, double z) { + return -x / y; + } + + @Override + public double xyzToV(double x, double y, double z) { + return z / y; + } + }, + new UvTransform() { + @Override + public double xyzToU(double x, double y, double z) { + return -x / z; + } + + @Override + public double xyzToV(double x, double y, double z) { + return -y / z; + } + }, + new UvTransform() { + @Override + public double xyzToU(double x, double y, double z) { + return z / x; + } + + @Override + public double xyzToV(double x, double y, double z) { + return y / x; + } + }, + new UvTransform() { + @Override + public double xyzToU(double x, double y, double z) { + return z / y; + } + + @Override + public double xyzToV(double x, double y, double z) { + return -x / y; + } + }, + new UvTransform() { + @Override + public double xyzToU(double x, double y, double z) { + return -y / z; + } + + @Override + public double xyzToV(double x, double y, double z) { + return -x / z; + } + } + }; + + /** + * A transform from 2D cartesian coordinates of a face to 3D directional vectors. The resulting + * vectors are not necessarily of unit length. + */ + interface XyzTransform { + /** + * Returns the 'x' coordinate for the [x, y, z] point on the unit sphere that projects to the + * given [u, v] point on a cube face. + */ + public double uvToX(double u, double v); + + /** + * Returns the 'y' coordinate for the [x, y, z] point on the unit sphere that projects to the + * given [u, v] point on a cube face. + */ + public double uvToY(double u, double v); + + /** + * Returns the 'z' coordinate for the [x, y, z] point on the unit sphere that projects to the + * given [u, v] point on a cube face. + */ + public double uvToZ(double u, double v); + } + + /** + * The transforms to convert (u, v) coordinates on a specific face to x-, y-, and z- coordinates, + * indexed by face. + */ + private static final XyzTransform[] XYZ_TRANSFORMS = { + new XyzTransform() { + @Override + public double uvToX(double u, double v) { + return 1; + } + + @Override + public double uvToY(double u, double v) { + return u; + } + + @Override + public double uvToZ(double u, double v) { + return v; + } + }, + new XyzTransform() { + @Override + public double uvToX(double u, double v) { + return -u; + } + + @Override + public double uvToY(double u, double v) { + return 1; + } + + @Override + public double uvToZ(double u, double v) { + return v; + } + }, + new XyzTransform() { + @Override + public double uvToX(double u, double v) { + return -u; + } + + @Override + public double uvToY(double u, double v) { + return -v; + } + + @Override + public double uvToZ(double u, double v) { + return 1; + } + }, + new XyzTransform() { + @Override + public double uvToX(double u, double v) { + return -1; + } + + @Override + public double uvToY(double u, double v) { + return -v; + } + + @Override + public double uvToZ(double u, double v) { + return -u; + } + }, + new XyzTransform() { + @Override + public double uvToX(double u, double v) { + return v; + } + + @Override + public double uvToY(double u, double v) { + return -1; + } + + @Override + public double uvToZ(double u, double v) { + return -u; + } + }, + new XyzTransform() { + @Override + public double uvToX(double u, double v) { + return v; + } + + @Override + public double uvToY(double u, double v) { + return u; + } + + @Override + public double uvToZ(double u, double v) { + return -1; + } + } + }; + + /** Minimum area of a cell at level k. */ + public final S2.Metric minArea; + + /** Maximum area of a cell at level k. */ + public final S2.Metric maxArea; + + /** Average area of a cell at level k. */ + public final S2.Metric avgArea; + + /** + * Minimum angular separation between opposite edges of a cell at level k. Each cell is bounded by + * four planes passing through its four edges and the center of the sphere. The angle span metrics + * relate to the angle between each pair of opposite bounding planes, or equivalently, between the + * planes corresponding to two different s-values or two different t-values. + */ + public final S2.Metric minAngleSpan; + + /** Maximum angular separation between opposite edges of a cell at level k. */ + public final S2.Metric maxAngleSpan; + + /** Average angular separation between opposite edges of a cell at level k. */ + public final S2.Metric avgAngleSpan; + + /** + * Minimum perpendicular angular separation between opposite edges of a cell at level k. + * + *

        The width of a geometric figure is defined as the distance between two parallel bounding + * lines in a given direction. For cells, the minimum width is always attained between two + * opposite edges, and the maximum width is attained between two opposite vertices. However, for + * our purposes we redefine the width of a cell as the perpendicular distance between a pair of + * opposite edges. A cell therefore has two widths, one in each direction. The minimum width + * according to this definition agrees with the classic geometric one, but the maximum width is + * different. (The maximum geometric width corresponds to {@link #maxDiag}.) + * + *

        This is useful for bounding the minimum or maximum distance from a point on one edge of a + * cell to the closest point on the opposite edge. For example, this is useful when "growing" + * regions by a fixed distance. + */ + public final S2.Metric minWidth; + + /** Maximum perpendicular angular separation between opposite edges of a cell at level k. */ + public final S2.Metric maxWidth; + + /** Average perpendicular angular separation between opposite edges of a cell at level k. */ + public final S2.Metric avgWidth; + + /** + * Minimum angular length of any cell edge at level k. The edge length metrics can also be used to + * bound the minimum, maximum, or average distance from the center of one cell to the center of + * one of its edge neighbors. In particular, it can be used to bound the distance between adjacent + * cell centers along the space-filling Hilbert curve for cells at any given level. + */ + public final S2.Metric minEdge; + + /** Maximum angular length of any cell edge at level k. */ + public final S2.Metric maxEdge; + + /** Average angular length of any cell edge at level k. */ + public final S2.Metric avgEdge; + + /** Minimum diagonal size of cells at level k. */ + public final S2.Metric minDiag; + + /** + * Maximum diagonal size of cells at level k. The maximum diagonal also happens to be the maximum + * diameter of any cell, and also the maximum geometric width. So for example, the distance from + * an arbitrary point to the closest cell center at a given level is at most half the maximum + * diagonal length. + */ + public final S2.Metric maxDiag; + + /** Average diagonal size of cells at level k. */ + public final S2.Metric avgDiag; + + /** + * Maximum edge aspect ratio over all cells at any level, where the edge aspect ratio of a cell is + * defined as the ratio of its longest edge length to its shortest edge length. + */ + public final double maxEdgeAspect; + + /** + * This is the maximum diagonal aspect ratio over all cells at any level, where the diagonal + * aspect ratio of a cell is defined as the ratio of its longest diagonal length to its shortest + * diagonal length. + */ + public final double maxDiagAspect = Math.sqrt(3); // 1.732 + + S2Projections( + double minAreaDeriv, + double maxAreaDeriv, + double minAngleSpanDeriv, + double maxAngleSpanDeriv, + double minWidthDeriv, + double avgWidthDeriv, + double minEdgeDeriv, + double avgEdgeDeriv, + double minDiagDeriv, + double maxDiagDeriv, + double avgDiagDeriv, + double maxEdgeAspect) { + this.minArea = new S2.Metric(2, minAreaDeriv); + this.maxArea = new S2.Metric(2, maxAreaDeriv); + this.avgArea = new S2.Metric(2, 4 * S2.M_PI / 6); // ~2.094 + this.minAngleSpan = new S2.Metric(1, minAngleSpanDeriv); + this.maxAngleSpan = new S2.Metric(1, maxAngleSpanDeriv); + this.avgAngleSpan = new S2.Metric(1, S2.M_PI / 2); // ~1.571 + this.minWidth = new S2.Metric(1, minWidthDeriv); + this.maxWidth = new S2.Metric(1, maxAngleSpanDeriv); + this.avgWidth = new S2.Metric(1, avgWidthDeriv); + this.minEdge = new S2.Metric(1, minEdgeDeriv); + this.maxEdge = new S2.Metric(1, maxAngleSpanDeriv); + this.avgEdge = new S2.Metric(1, avgEdgeDeriv); + this.minDiag = new S2.Metric(1, minDiagDeriv); + this.maxDiag = new S2.Metric(1, maxDiagDeriv); + this.avgDiag = new S2.Metric(1, avgDiagDeriv); + this.maxEdgeAspect = maxEdgeAspect; + } + + /** + * Convert 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. + */ + public abstract double stToUV(double s); + + /** + * Returns the i- or j-index of the leaf cell containing the given s- or t-value. If the argument + * is outside the range spanned by valid leaf cell indices, return the index of the closest valid + * leaf cell (i.e., return values are clamped to the range of valid leaf cell indices). + */ + public static int stToIj(double s) { + return Math.max( + 0, Math.min(S2CellId.MAX_SIZE - 1, (int) Math.round(S2CellId.MAX_SIZE * s - 0.5))); + } + + /** + * 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. + */ + public static double ijToStMin(int i) { + // assert (i >= 0 && i <= S2CellId.MAX_SIZE); + return (1.0 / S2CellId.MAX_SIZE) * i; + } + + /** + * Converts the specified i- or j-coordinate into its corresponding u- or v-coordinate, + * respectively, for the given cell size. + */ + public double ijToUV(int ij, int cellSize) { + return stToUV(ijToStMin(ij & -cellSize)); + } + + /** Returns the s- or t-value corresponding to the given si- or ti-value. */ + public static double siTiToSt(long si) { + // assert (si >= 0 && si <= MAX_SITI); + return (1.0 / MAX_SITI) * si; + } + + /** + * Returns the si- or ti-coordinate that is nearest to the given s- or t-value. The result may be + * outside the range of valid (si,ti)-values. + */ + public static long stToSiTi(double s) { + return Math.round(s * MAX_SITI); + } + + /** + * The inverse of {@link #stToUV(double)}. Note that it is not always true that {@code + * uvToST(stToUV(x)) == x} due to numerical errors. + */ + public abstract double uvToST(double u); + + /** + * Convert (face, u, v) coordinates to a direction vector (not necessarily unit length). + * + *

        Requires that the face is between 0 and 5, inclusive. + */ + public static S2Point faceUvToXyz(int face, double u, double v) { + XyzTransform t = faceToXyzTransform(face); + return new S2Point(t.uvToX(u, v), t.uvToY(u, v), t.uvToZ(u, v)); + } + + /** + * Convert (face, u, v) coordinates to a direction vector (not necessarily unit length). + * + *

        Requires that the face is between 0 and 5, inclusive. + */ + public static S2Point faceUvToXyz(int face, R2Vector uv) { + return faceUvToXyz(face, uv.x(), uv.y()); + } + + /** Returns the {@link XyzTransform} for the specified face. */ + static XyzTransform faceToXyzTransform(int face) { + // We map illegal face indices to the largest face index to preserve legacy behavior, i.e., we + // do not (yet) want to throw an index out of bounds exception. Note that S2CellId.face() is + // guaranteed to return a non-negative face index even for invalid S2 cells, so it is sufficient + // to just map all face indices greater than 5 to a face index of 5. + // + // TODO(bjj): Remove this legacy behavior. + return XYZ_TRANSFORMS[Math.min(5, face)]; + } + + /** + * If the dot product of p with the given face normal is positive, set the corresponding u and v + * values (which may lie outside the range [-1,1]) and return true. Otherwise return null. + */ + public static R2Vector faceXyzToUv(int face, S2Point p) { + if (face < 3) { + if (p.get(face) <= 0) { + return null; + } + } else { + if (p.get(face - 3) >= 0) { + return null; + } + } + return validFaceXyzToUv(face, p); + } + + /** + * Given a *valid* face for the given point p (meaning that dot product of p with the face normal + * is positive), return the corresponding u and v values (which may lie outside the range [-1,1]). + * + *

        Requires that the face is between 0 and 5, inclusive. + */ + public static R2Vector validFaceXyzToUv(int face, S2Point p) { + R2Vector result = new R2Vector(); + validFaceXyzToUv(face, p, result); + return result; + } + + /** + * As {@link #validFaceXyzToUv(int, S2Point)}, except {@code result} is updated, instead of a + * being returned in a new instance. Package-private because non-S2 classes should not be mutating + * R2Vectors. + */ + static void validFaceXyzToUv(int face, S2Point p, R2Vector result) { + UvTransform t = faceToUvTransform(face); + result.set(t.xyzToU(p.x, p.y, p.z), t.xyzToV(p.x, p.y, p.z)); + } + + /** Returns the {@link UvTransform} for the specified face. */ + public static UvTransform faceToUvTransform(int face) { + return UV_TRANSFORMS[face]; + } + + /** + * Returns the given point P transformed to the (u,v,w) coordinate frame of the given face (where + * the w-axis represents the face normal). + */ + public static S2Point faceXyzToUvw(int face, S2Point p) { + // The result coordinates are simply the dot products of P with the (u,v,w) axes for the given + // face (see FACE_UVW_AXES). + switch (face) { + case 0: + return new S2Point(p.y, p.z, p.x); + case 1: + return new S2Point(-p.x, p.z, p.y); + case 2: + return new S2Point(-p.x, -p.y, p.z); + case 3: + return new S2Point(-p.z, -p.y, -p.x); + case 4: + return new S2Point(-p.z, p.x, -p.y); + default: + return new S2Point(p.y, p.x, -p.z); + } + } + + /** Returns the level of the given si or ti coordinate. */ + private static final int siTiToLevel(long siTi) { + return S2CellId.MAX_LEVEL - Long.numberOfTrailingZeros(siTi | MAX_SITI); + } + + /** + * A [face, si, ti] position. This is package private for now, since we may want to rework the + * class to use 32-bit ints instead. + */ + static final class FaceSiTi { + /** The face on which the position exists. */ + public final int face; + /** The si coordinate. See {@link S2Projections} for details. */ + public final long si; + /** The ti coordinate. See {@link S2Projections} for details. */ + public final long ti; + + /** Package private constructor. Only S2 should create these for now. */ + FaceSiTi(int face, long si, long ti) { + this.face = face; + this.si = si; + this.ti = ti; + } + } + + /** Convert (face, si, ti) coordinates to a direction vector (not necessarily unit length.) */ + public S2Point faceSiTiToXyz(int face, long si, long ti) { + double u = stToUV(siTiToSt(si)); + double v = stToUV(siTiToSt(ti)); + return faceUvToXyz(face, u, v); + } + + /** Convert a direction vector (not necessarily unit length) to (face, si, ti) coordinates. */ + FaceSiTi xyzToFaceSiTi(S2Point p) { + int face = xyzToFace(p); + R2Vector uv = validFaceXyzToUv(face, p); + long si = stToSiTi(uvToST(uv.x())); + long ti = stToSiTi(uvToST(uv.y())); + return new FaceSiTi(face, si, ti); + } + + /** If p is exactly a cell center, returns the level of the cell, -1 otherwise. */ + int levelIfCenter(FaceSiTi fst, S2Point p) { + // If the levels corresponding to si,ti are not equal, then p is not a cell + // center. The si,ti values 0 and MAX_SITI need to be handled specially + // because they do not correspond to cell centers at any valid level; they + // are mapped to level -1 by the code below. + int level = siTiToLevel(fst.si); + if (level < 0 || level != siTiToLevel(fst.ti)) { + return -1; + } else { + // assert (level <= S2CellId.MAX_LEVEL); + // In infinite precision, this test could be changed to ST == SiTi. However, + // due to rounding errors, UVtoST(XYZtoFaceUV(FaceUVtoXYZ(STtoUV(...)))) is + // not idempotent. On the other hand, centerRaw is computed exactly the same + // way p was originally computed (if it is indeed the center of an S2Cell): + // the comparison can be exact. + S2Point center = S2Point.normalize(faceSiTiToXyz(fst.face, fst.si, fst.ti)); + if (p.equals(center)) { + return level; + } else { + return -1; + } + } + } + + /** + * Returns the face containing the given direction vector (for points on the boundary between + * faces, the result is arbitrary but repeatable.) + */ + public static int xyzToFace(S2Point p) { + return xyzToFace(p.x, p.y, p.z); + } + + /** + * As {@link #xyzToFace(S2Point)}, but accepts the coordinates as primitive doubles instead. + * Useful when the caller has coordinates and doesn't want to allocate an S2Point. + */ + static int xyzToFace(double x, double y, double z) { + switch (S2Point.largestAbsComponent(x, y, z)) { + case 0: + return (x < 0) ? 3 : 0; + case 1: + return (y < 0) ? 4 : 1; + default: + return (z < 0) ? 5 : 2; + } + } + + /** + * 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 vector is perpendicular to + * the plane through the sphere origin that contains the given edge.) + */ + public static S2Point getUNorm(int face, double u) { + switch (face) { + case 0: + return new S2Point(u, -1, 0); + case 1: + return new S2Point(1, u, 0); + case 2: + return new S2Point(1, 0, u); + case 3: + return new S2Point(-u, 0, 1); + case 4: + return new S2Point(0, -u, 1); + default: + return new S2Point(0, -1, -u); + } + } + + /** + * 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. + */ + public static S2Point getVNorm(int face, double v) { + switch (face) { + case 0: + return new S2Point(-v, 0, 1); + case 1: + return new S2Point(0, -v, 1); + case 2: + return new S2Point(0, -1, -v); + case 3: + return new S2Point(v, -1, 0); + case 4: + return new S2Point(1, v, 0); + default: + return new S2Point(1, 0, v); + } + } + + /** Returns the u-axis for the given face. */ + public static S2Point getUAxis(int face) { + return getUVWAxis(face, 0); + } + + /** Returns the v-axis for the given face. */ + public static S2Point getVAxis(int face) { + return getUVWAxis(face, 1); + } + + /** Returns the unit-length normal for the given face. */ + public static S2Point getNorm(int face) { + return getUVWAxis(face, 2); + } + + /** Returns the given axis of the given face (u=0, v=1, w=2). */ + static S2Point getUVWAxis(int face, int axis) { + return FACE_UVW_AXES[face][axis]; + } + + /** + * Returns the face that lies in the given direction (negative=0, positive=1) of the given axis + * (u=0, v=1, w=2) in the given face. For example, {@code getUVWFace(4, 0, 1)} returns the face + * that is adjacent to face 4 in the positive u-axis direction. + */ + static int getUVWFace(int face, int axis, int direction) { + // assert (face >= 0 && face <= 5); + // assert (axis >= 0 && axis <= 2); + // assert (direction >= 0 && direction <= 1); + return FACE_UVW_FACES[face][axis][direction]; + } + + /** The default transformation between ST and UV coordinates. */ + public static final S2Projections PROJ = S2Projections.S2_QUADRATIC_PROJECTION; +} diff --git a/core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/S2Region.java b/core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/S2Region.java new file mode 100644 index 0000000000..b1c6538870 --- /dev/null +++ b/core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/S2Region.java @@ -0,0 +1,55 @@ +/* + * Copyright 2005 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.mogo.eagle.core.utilcode.geometry; + +import com.google.common.annotations.GwtCompatible; + +/** + * An S2Region represents a two-dimensional region over the unit sphere. It is an abstract interface + * with various concrete subtypes. + * + *

        The main purpose of this interface is to allow complex regions to be approximated as simpler + * regions. So rather than having a wide variety of virtual methods that are implemented by all + * subtypes, the interface is restricted to methods that are useful for computing approximations. + * + */ +@GwtCompatible +public interface S2Region { + + /** Return a bounding spherical cap. */ + public abstract S2Cap getCapBound(); + + /** Return a bounding latitude-longitude rectangle. */ + public abstract S2LatLngRect getRectBound(); + + /** + * If this method returns true, the region completely contains the given cell. Otherwise, either + * the region does not contain the cell or the containment relationship could not be determined. + */ + public abstract boolean contains(S2Cell cell); + + /** + * Returns true if and only if the given point is contained by the region. {@code p} is generally + * required to be unit length, although some subtypes may relax this restriction. + */ + public abstract boolean contains(S2Point p); + + /** + * If this method returns false, the region does not intersect the given cell. Otherwise, either + * region intersects the cell, or the intersection relationship could not be determined. + */ + public abstract boolean mayIntersect(S2Cell cell); +} diff --git a/core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/S2RegionCoverer.java b/core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/S2RegionCoverer.java new file mode 100644 index 0000000000..018c7b09f4 --- /dev/null +++ b/core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/S2RegionCoverer.java @@ -0,0 +1,733 @@ +/* + * Copyright 2005 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.mogo.eagle.core.utilcode.geometry; + +import static com.mogo.eagle.core.utilcode.geometry.S2Projections.PROJ; + +import com.google.common.annotations.GwtCompatible; +import com.google.common.base.Objects; +import com.google.common.base.Preconditions; +import com.google.common.collect.ImmutableList; +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashSet; +import java.util.List; +import java.util.PriorityQueue; + +/** + * An S2RegionCoverer is a class that allows arbitrary regions to be approximated as unions of cells + * (S2CellUnion). This is useful for implementing various sorts of search and precomputation + * operations. + * + *

        Typical usage: {@code S2RegionCoverer coverer = + * S2RegionCoverer.builder().setMaxCells(5).build(); S2Cap cap = S2Cap.fromAxisAngle(...); + * S2CellUnion covering; coverer.getCovering(cap, covering);} + * + *

        This yields a cell union of at most 5 cells that is guaranteed to cover the given cap (a + * disc-shaped region on the sphere). + * + *

        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. + * + *

        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. + * + */ +@GwtCompatible(serializable = true) +public final strictfp class S2RegionCoverer implements Serializable { + + /** + * A S2RegionCoverer configured with the default options. The min level, max level, and level mod + * are unrestricted, and maxCells is {@link Builder#DEFAULT_MAX_CELLS}. See {@link Builder} for + * details. + */ + public static final S2RegionCoverer DEFAULT = builder().build(); + + private static final List FACE_CELLS; + + static { + ImmutableList.Builder builder = ImmutableList.builder(); + for (int face = 0; face < 6; ++face) { + builder.add(S2Cell.fromFace(face)); + } + FACE_CELLS = builder.build(); + } + + private final int minLevel; + private final int maxLevel; + private final int levelMod; + private final int maxCells; + + static class Candidate { + private S2Cell cell; + private boolean isTerminal; // Cell should not be expanded further. + private int numChildren; // Number of children that intersect the region. + private Candidate[] children; // Actual size may be 0, 4, 16, or 64 + // elements. + } + + static class QueueEntry { + private int id; + private Candidate candidate; + + public QueueEntry(int id, Candidate candidate) { + this.id = id; + this.candidate = candidate; + } + } + + /** + * We define our own comparison function on QueueEntries in order to make the results + * deterministic. + */ + static class QueueEntriesComparator implements Comparator { + @Override + public int compare(QueueEntry x, QueueEntry y) { + return x.id < y.id ? 1 : (x.id > y.id ? -1 : 0); + } + } + + /** + * Returns a new Builder with default values, which can be used to construct an S2RegionCoverer + * instance. + */ + public static Builder builder() { + return new Builder(); + } + + /** + * Construct from a {@link Builder}. Users should construct with + * S2RegionCoverer.builder().build(), or use the DEFAULT instance. + */ + private S2RegionCoverer(Builder builder) { + minLevel = builder.getMinLevel(); + maxLevel = builder.getMaxLevel(); + levelMod = builder.getLevelMod(); + maxCells = builder.getMaxCells(); + } + + /** A Build to construct a {@link S2RegionCoverer} with options. */ + public static final class Builder { + /** + * By default, the covering uses at most 8 cells at any level. This gives a reasonable tradeoff + * between the number of cells used and the accuracy of the approximation (see table below). + */ + private static final int DEFAULT_MAX_CELLS = 8; + + private int minLevel = 0; + private int maxLevel = S2CellId.MAX_LEVEL; + private int levelMod = 1; + private int maxCells = DEFAULT_MAX_CELLS; + + /** Users should create a Builder via the S2RegionCoverer.builder() method. */ + private Builder() {} + + // Set the minimum and maximum cell level to be used. The default is to use + // all cell levels. Requires: maxLevel() >= minLevel(). + // + // To find the cell level corresponding to a given physical distance, use + // the S2Cell metrics defined in s2.h. For example, to find the cell + // level that corresponds to an average edge length of 10km, use: + // + // int level = S2::kAvgEdge.GetClosestLevel( + // geostore::S2Earth::KmToRadians(length_km)); + // + // Note: 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. + + /** + * Sets the minimum level to be used. + * + *

        Default: 0 + */ + public Builder setMinLevel(int minLevel) { + // assert (minLevel >= 0 && minLevel <= S2CellId.MAX_LEVEL); + this.minLevel = Math.max(0, Math.min(S2CellId.MAX_LEVEL, minLevel)); + return this; + } + + /** Returns the minimum cell level to be used. */ + public int getMinLevel() { + return minLevel; + } + + /** + * Sets the maximum level to be used. + * + *

        Default: S2CellId.MAX_LEVEL + */ + public Builder setMaxLevel(int maxLevel) { + // assert (maxLevel >= 0 && maxLevel <= S2CellId.MAX_LEVEL); + this.maxLevel = Math.max(0, Math.min(S2CellId.MAX_LEVEL, maxLevel)); + return this; + } + + /** Returns the maximum cell level to be used. */ + public int getMaxLevel() { + return maxLevel; + } + + /** + * Only cells where (level - minLevel) is a multiple of "levelMod" will be used (default 1). + * This effectively allows the branching factor of the S2CellId hierarchy to be increased. + * Currently the only parameter values allowed are 1, 2, or 3, corresponding to branching + * factors of 4, 16, and 64 respectively. + * + *

        Default: 1 + */ + public Builder setLevelMod(int levelMod) { + // assert (levelMod >= 1 && levelMod <= 3); + this.levelMod = Math.max(1, Math.min(3, levelMod)); + return this; + } + + /** Returns the level mod. */ + public int getLevelMod() { + return levelMod; + } + + /** + * Sets the maximum desired number of cells in the approximation (defaults to + * DEFAULT_MAX_CELLS). Note the following: + * + *

          + *
        • 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. an S2Cap + * or S2LatLngRect). + *
        + * + *

        Accuracy is measured by dividing the area of the covering by the area of the original + * region. The following table shows the median and worst case values for this area ratio on a + * test case consisting of 100,000 spherical caps of random size (generated using + * s2regioncoverer_unittest): + * + *

        +     * max_cells: 3 4 5 6 8 12 20 100 1000
        +     * median ratio: 5.33 3.32 2.73 2.34 1.98 1.66 1.42 1.11 1.01
        +     * worst case: 215518 14.41 9.72 5.26 3.91 2.75 1.92 1.20 1.02
        +     * 
        + * + *

        Default: 8 + */ + public Builder setMaxCells(int maxCells) { + this.maxCells = maxCells; + return this; + } + + /** Returns the maximum desired number of cells to be used. */ + public int getMaxCells() { + return maxCells; + } + + /** Constructs a {@link S2RegionCoverer} with this Builders options. */ + public S2RegionCoverer build() { + return new S2RegionCoverer(this); + } + } + + @Override + public boolean equals(Object obj) { + if (obj instanceof S2RegionCoverer) { + S2RegionCoverer that = (S2RegionCoverer) obj; + return this.minLevel == that.minLevel + && this.maxLevel == that.maxLevel + && this.levelMod == that.levelMod + && this.maxCells == that.maxCells; + } + return false; + } + + @Override + public int hashCode() { + return Objects.hashCode(minLevel, maxLevel, levelMod, maxCells); + } + + public int minLevel() { + return minLevel; + } + + public int maxLevel() { + return maxLevel; + } + + public int maxCells() { + return maxCells; + } + + public int levelMod() { + return levelMod; + } + + /** + * Computes a list of cell ids that covers the given region and satisfies the various restrictions + * specified above. + * + * @param region The region to cover + * @param covering The list filled in by this method + */ + public void getCovering(S2Region region, ArrayList covering) { + // Rather than just returning the raw list of cell ids generated by + // GetCoveringInternal(), we construct a cell union and then denormalize it. + // This has the effect of replacing four child cells with their parent + // whenever this does not violate the covering parameters specified + // (minLevel, levelMod, etc). This strategy significantly reduces the + // number of cells returned in many cases, and it is cheap compared to + // computing the covering in the first place. + + S2CellUnion tmp = getCovering(region); + tmp.denormalize(minLevel(), levelMod(), covering); + } + + /** + * Computes a list of cell ids that is contained within the given region and satisfies the various + * restrictions specified above; note that if the max cell level is not specified very carefully + * this method can try to create an enormous number of cells, wasting a lot of time and memory, so + * care should be taken to set a max level suitable for the scale of the region being covered. + * + * @param region The region to fill + * @param interior The list filled in by this method + */ + public void getInteriorCovering(S2Region region, ArrayList interior) { + S2CellUnion tmp = getInteriorCovering(region); + tmp.denormalize(minLevel(), levelMod(), interior); + } + + /** + * Return a normalized cell union 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 cell union constructor + * does in fact satisfy all the given restrictions.) + */ + public S2CellUnion getCovering(S2Region region) { + S2CellUnion covering = new S2CellUnion(); + getCovering(region, covering); + return covering; + } + + public void getCovering(S2Region region, S2CellUnion covering) { + ActiveCovering state = new ActiveCovering(false, region); + state.getCoveringInternal(); + covering.initSwap(state.result); + } + + /** + * Return a normalized cell union that is contained within the given region and satisfies the + * restrictions *EXCEPT* for minLevel() and levelMod(). + */ + public S2CellUnion getInteriorCovering(S2Region region) { + S2CellUnion covering = new S2CellUnion(); + getInteriorCovering(region, covering); + return covering; + } + + public void getInteriorCovering(S2Region region, S2CellUnion covering) { + ActiveCovering state = new ActiveCovering(true, region); + state.getCoveringInternal(); + covering.initSwap(state.result); + } + + /** + * Given a connected region and a starting point, return a set of cells at the given level that + * cover the region. + */ + public static void getSimpleCovering( + S2Region region, S2Point start, int level, ArrayList output) { + floodFill(region, S2CellId.fromPoint(start).parent(level), output); + } + + /** + * Like GetCovering(), except that this method is much faster and the coverings are not as tight. + * + *

        All of the usual parameters are respected (max_cells, min_level, max_level, and level_mod), + * 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. + */ + public void getFastCovering(S2Cap cap, ArrayList results) { + getRawFastCovering(cap, maxCells(), results); + normalizeCovering(results); + } + + /** + * Compute 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. + * + *

        {@code max_cells_hint} can be used to request a more accurate covering (but is currently + * ignored). + */ + private static void getRawFastCovering( + S2Cap cap, @SuppressWarnings("unused") int maxCellsHint, List covering) { + // TODO(user): The covering could be made quite a bit tighter by mapping the cap to a rectangle + // in (i,j)-space and finding a covering for that. + covering.clear(); + + // Find the maximum level such that the cap contains at most one cell vertex and such that + // S2CellId.appendVertexNeighbors() can be called. + int level = S2Projections.PROJ.minWidth.getMaxLevel(2 * cap.angle().radians()); + level = Math.min(level, S2CellId.MAX_LEVEL - 1); + + if (level == 0) { + // Don't bother trying to optimize the level == 0 case, since more than four face cells may be + // required. + Collections.addAll(covering, S2CellId.FACE_CELLS); + } else { + // The covering consists of the 4 cells at the given level that share the cell vertex that is + // closest to the cap center. + S2CellId id = S2CellId.fromPoint(cap.axis()); + id.getVertexNeighbors(level, covering); + } + } + + /** + * Normalize "covering" so that it conforms to the current covering parameters (maxCells, + * minLevel, maxLevel, and levelMod). + */ + public void normalizeCovering(ArrayList covering) { + // This method makes no attempt to be optimal. In particular, if minMevel() > 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. + + // If any cells are too small, or don't satisfy levelMod(), then replace them with ancestors. + if (maxLevel() < S2CellId.MAX_LEVEL || levelMod() > 1) { + for (int i = 0; i < covering.size(); i++) { + S2CellId id = covering.get(i); + int level = id.level(); + int newLevel = adjustLevel(Math.min(level, maxLevel())); + if (newLevel != level) { + covering.set(i, id.parent(newLevel)); + } + } + } + + // Sort the cells and simplify them. + S2CellUnion.normalize(covering); + + // If there are still too many cells, then repeatedly replace two adjacent cells in S2CellId + // order by their lowest common ancestor. + while (covering.size() > maxCells()) { + int bestIndex = -1; + int bestLevel = -1; + for (int i = 0; i + 1 < covering.size(); i++) { + int level = covering.get(i).getCommonAncestorLevel(covering.get(i + 1)); + level = adjustLevel(level); + if (level > bestLevel) { + bestLevel = level; + bestIndex = i; + } + } + if (bestLevel < minLevel()) { + break; + } + covering.set(bestIndex, covering.get(bestIndex).parent(bestLevel)); + S2CellUnion.normalize(covering); + } + + // Make sure that the covering satisfies minLevel() and levelMod(), possibly at the expense of + // satisfying maxCells(). + if (minLevel() > 0 || levelMod() > 1) { + S2CellUnion result = new S2CellUnion(); + result.initRawSwap(covering); + result.denormalize(minLevel(), levelMod(), covering); + } + } + + /** + * If level > minLevel(), then reduce {@code level} if necessary so that it also satisfies + * levelMod(). Levels smaller than minLevel() are not affected (since cells at these levels are + * eventually expanded). + */ + private int adjustLevel(int level) { + if (levelMod() > 1 && level > minLevel()) { + level -= (level - minLevel()) % levelMod(); + } + return level; + } + + /** This class tracks the state of a covering while it is underway. */ + final class ActiveCovering { + /** True if we're covering the interior. */ + final boolean interiorCovering; + + /** The region being covered. */ + final S2Region region; + + /** Counter of number of candidates created, for performance evaluation. */ + int candidatesCreatedCounter = 0; + + /** Cell ids that have been added to the covering so far. */ + final ArrayList result = new ArrayList(); + + /** Prioritized candidates to explore next. */ + final PriorityQueue candidateQueue = + new PriorityQueue<>( + // TODO(kirilll?): 10 is a completely random number, work out a better + // estimate + 10, new QueueEntriesComparator()); + + ActiveCovering(boolean interior, S2Region region) { + this.interiorCovering = interior; + this.region = region; + } + + /** + * If the cell intersects the given region, return a new candidate with no children, otherwise + * return null. Also marks the candidate as "terminal" if it should not be expanded further. + */ + private Candidate newCandidate(S2Cell cell) { + if (!region.mayIntersect(cell)) { + return null; + } + + boolean isTerminal = false; + if (cell.level() >= minLevel) { + if (interiorCovering) { + if (region.contains(cell)) { + isTerminal = true; + } else if (cell.level() + levelMod > maxLevel) { + return null; + } + } else { + if (cell.level() + levelMod > maxLevel || region.contains(cell)) { + isTerminal = true; + } + } + } + Candidate candidate = new Candidate(); + candidate.cell = cell; + candidate.isTerminal = isTerminal; + if (!isTerminal) { + candidate.children = new Candidate[1 << maxChildrenShift()]; + } + candidatesCreatedCounter++; + return candidate; + } + + /** Return the log base 2 of the maximum number of children of a candidate. */ + private int maxChildrenShift() { + return 2 * levelMod; + } + + /** + * Process a candidate by either adding it to the result list or expanding its children and + * inserting it into the priority queue. Passing a null argument does nothing. + */ + private void addCandidate(Candidate candidate) { + if (candidate == null) { + return; + } + + if (candidate.isTerminal) { + result.add(candidate.cell.id()); + return; + } + // assert (candidate.numChildren == 0); + + // Expand one level at a time until we hit minLevel to ensure that + // we don't skip over it. + int numLevels = (candidate.cell.level() < minLevel) ? 1 : levelMod; + int numTerminals = expandChildren(candidate, candidate.cell, numLevels); + + if (candidate.numChildren == 0) { + // Do nothing + } else if (!interiorCovering + && numTerminals == 1 << maxChildrenShift() + && candidate.cell.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.isTerminal = true; + addCandidate(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 + // at the same level, we prefer the cells with the smallest number of + // intersecting children. Finally, we prefer cells that have the smallest + // number of children that cannot be refined any further. + int priority = + -((((candidate.cell.level() << maxChildrenShift()) + candidate.numChildren) + << maxChildrenShift()) + + numTerminals); + candidateQueue.add(new QueueEntry(priority, candidate)); + // logger.info("Push: " + candidate.cell.id() + " (" + priority + ") "); + } + } + + /** + * Populate the children of "candidate" by expanding the given number of levels from the given + * cell. Returns the number of children that were marked "terminal". + */ + private int expandChildren(Candidate candidate, S2Cell cell, int numLevels) { + numLevels--; + S2Cell[] childCells = new S2Cell[4]; + for (int i = 0; i < 4; ++i) { + childCells[i] = new S2Cell(); + } + cell.subdivide(childCells); + int numTerminals = 0; + for (int i = 0; i < 4; ++i) { + if (numLevels > 0) { + if (region.mayIntersect(childCells[i])) { + numTerminals += expandChildren(candidate, childCells[i], numLevels); + } + continue; + } + Candidate child = newCandidate(childCells[i]); + if (child != null) { + candidate.children[candidate.numChildren++] = child; + if (child.isTerminal) { + ++numTerminals; + } + } + } + return numTerminals; + } + + /** Computes a set of initial candidates that cover the given region. */ + private void getInitialCandidates() { + // Optimization: if at least 4 cells are desired (the normal case), + // start with a 4-cell covering of the region's bounding cap. This + // lets us skip quite a few levels of refinement when the region to + // be covered is relatively small. + if (maxCells >= 4) { + // Find the maximum level such that the bounding cap contains at most one + // cell vertex at that level. + S2Cap cap = region.getCapBound(); + int level = + Math.min( + PROJ.minWidth.getMaxLevel(2 * cap.angle().radians()), + Math.min(maxLevel(), S2CellId.MAX_LEVEL - 1)); + if (levelMod() > 1 && level > minLevel()) { + level -= (level - minLevel()) % levelMod(); + } + // We don't bother trying to optimize the level == 0 case, since more than + // four face cells may be required. + if (level > 0) { + // Find the leaf cell containing the cap axis, and determine which + // subcell of the parent cell contains it. + ArrayList base = new ArrayList(4); + S2CellId id = S2CellId.fromPoint(cap.axis()); + id.getVertexNeighbors(level, base); + for (int i = 0; i < base.size(); ++i) { + addCandidate(newCandidate(new S2Cell(base.get(i)))); + } + return; + } + } + // Default: start with all six cube faces. + for (int face = 0; face < 6; ++face) { + addCandidate(newCandidate(FACE_CELLS.get(face))); + } + } + + /** Generates a covering and stores it in result. */ + private void getCoveringInternal() { + // 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 the + // priority queue 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 candidateQueue 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). + + Preconditions.checkState(candidateQueue.isEmpty() && result.isEmpty()); + + getInitialCandidates(); + while (!candidateQueue.isEmpty() && (!interiorCovering || result.size() < maxCells)) { + Candidate candidate = candidateQueue.poll().candidate; + // 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 than maxCells in result (minLevel is too high). + if (interiorCovering + || candidate.cell.level() < minLevel + || candidate.numChildren == 1 + || result.size() + candidateQueue.size() + candidate.numChildren <= maxCells) { + // Expand this candidate into its children. + for (int i = 0; i < candidate.numChildren; ++i) { + if (!interiorCovering || result.size() < maxCells) { + addCandidate(candidate.children[i]); + } + } + } else { + candidate.isTerminal = true; + addCandidate(candidate); + } + } + } + } + + /** + * Given a region and a starting cell, return the set of all the edge-connected cells at the same + * level that intersect "region". The output cells are returned in arbitrary order. + */ + private static void floodFill(S2Region region, S2CellId start, ArrayList output) { + HashSet all = new HashSet(); + ArrayList frontier = new ArrayList(); + output.clear(); + all.add(start); + frontier.add(start); + while (!frontier.isEmpty()) { + S2CellId id = frontier.get(frontier.size() - 1); + frontier.remove(frontier.size() - 1); + if (!region.mayIntersect(new S2Cell(id))) { + continue; + } + output.add(id); + + S2CellId[] neighbors = new S2CellId[4]; + id.getEdgeNeighbors(neighbors); + for (int edge = 0; edge < 4; ++edge) { + S2CellId nbr = neighbors[edge]; + if (!all.contains(nbr)) { + frontier.add(nbr); + all.add(nbr); + } + } + } + } +} diff --git a/core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/S2RegionIntersection.java b/core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/S2RegionIntersection.java new file mode 100644 index 0000000000..c8eb09debd --- /dev/null +++ b/core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/S2RegionIntersection.java @@ -0,0 +1,114 @@ +/* + * Copyright 2014 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.mogo.eagle.core.utilcode.geometry; + +import com.google.common.annotations.GwtCompatible; +import java.io.Serializable; +import java.util.Arrays; +import java.util.Collection; + +/** + * An S2RegionIntersection represents an intersection of overlapping regions. It is convenient for + * computing a covering of the intersection of a set of regions. The regions are assumed to be + * immutable. Note: An intersection of no regions covers the entire sphere. + */ +@GwtCompatible(serializable = true) +public class S2RegionIntersection implements S2Region, Serializable { + // Regions is non-private so that it can be accessed from the custom field serializer. + final S2Region[] regions; + private transient S2LatLngRect cachedRectBound = null; + + /** Create an intersection from a copy of {@code regions}. */ + public S2RegionIntersection(Collection regions) { + this.regions = regions.toArray(new S2Region[regions.size()]); + } + + /** Returns true if all the regions fully contain the cell. */ + @Override + public boolean contains(S2Cell cell) { + for (S2Region region : regions) { + if (!region.contains(cell)) { + return false; + } + } + + return true; + } + + /** Returns true if all the regions fully contain the point. */ + @Override + public boolean contains(S2Point point) { + for (S2Region region : regions) { + if (!region.contains(point)) { + return false; + } + } + + return true; + } + + @Override + public S2Cap getCapBound() { + // This could be optimized to return a tighter bound, but doesn't seem worth it unless + // profiling shows otherwise. + return getRectBound().getCapBound(); + } + + @Override + public S2LatLngRect getRectBound() { + if (cachedRectBound != null) { + return cachedRectBound; + } + + S2LatLngRect.Builder builder = new S2LatLngRect.Builder(S2LatLngRect.full()); + for (S2Region region : regions) { + builder.intersection(region.getRectBound()); + } + cachedRectBound = builder.build(); + return cachedRectBound; + } + + /** Returns true if the cell may intersect all regions in this collection. */ + @Override + public boolean mayIntersect(S2Cell cell) { + for (S2Region region : regions) { + if (!region.mayIntersect(cell)) { + return false; + } + } + return true; + } + + /** + * Returns true if this S2RegionIntersection is equal to another S2RegionIntersection, where each + * region must be equal and in the same order. This method is intended only for testing purposes. + * NOTE: This should be rewritten to disregard order if such functionality is ever required. + */ + @Override + public boolean equals(Object thatObject) { + if (!(thatObject instanceof S2RegionIntersection)) { + return false; + } + S2RegionIntersection that = (S2RegionIntersection) thatObject; + return Arrays.deepEquals(regions, that.regions); + } + + @Override + public int hashCode() { + return Arrays.hashCode(regions); + } +} diff --git a/core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/S2RegionUnion.java b/core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/S2RegionUnion.java new file mode 100644 index 0000000000..e3975b5742 --- /dev/null +++ b/core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/S2RegionUnion.java @@ -0,0 +1,119 @@ +/* + * Copyright 2014 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.mogo.eagle.core.utilcode.geometry; + +import com.google.common.annotations.GwtCompatible; +import java.io.Serializable; +import java.util.Arrays; +import java.util.Collection; + +/** + * An S2RegionUnion represents a union of possibly overlapping regions. It is convenient for + * computing a covering of a set of regions. The regions are assumed to be immutable. + */ +@GwtCompatible(serializable = true) +public class S2RegionUnion implements S2Region, Serializable { + // Regions is non-private so that it can be accessed from the custom field + // serializer. + final S2Region[] regions; + private transient S2Cap cachedCapBound = null; + private transient S2LatLngRect cachedRectBound = null; + + public S2RegionUnion(Collection regions) { + this.regions = regions.toArray(new S2Region[regions.size()]); + } + + /** Only returns true if one of the regions fully contains the cell. */ + @Override + public boolean contains(S2Cell cell) { + for (S2Region region : regions) { + if (region.contains(cell)) { + return true; + } + } + + return false; + } + + /** Only returns true if one of the regions contains the point. */ + @Override + public boolean contains(S2Point point) { + for (S2Region region : regions) { + if (region.contains(point)) { + return true; + } + } + + return false; + } + + @Override + public S2Cap getCapBound() { + if (cachedCapBound != null) { + return cachedCapBound; + } + + cachedCapBound = S2Cap.empty(); + for (S2Region region : regions) { + cachedCapBound = cachedCapBound.addCap(region.getCapBound()); + } + return cachedCapBound; + } + + @Override + public S2LatLngRect getRectBound() { + if (cachedRectBound != null) { + return cachedRectBound; + } + + cachedRectBound = S2LatLngRect.empty(); + for (S2Region region : regions) { + cachedRectBound = cachedRectBound.union(region.getRectBound()); + } + return cachedRectBound; + } + + /** Returns true if the cell may intersect any region in this collection. */ + @Override + public boolean mayIntersect(S2Cell cell) { + for (S2Region region : regions) { + if (region.mayIntersect(cell)) { + return true; + } + } + return false; + } + + /** + * Returns true if this S2RegionUnion is equal to another S2RegionUnion, where each region must be + * equal and in the same order. This method is intended only for testing purposes. NOTE: This + * should be rewritten to disregard order if such functionality is ever required. + */ + @Override + public boolean equals(Object thatObject) { + if (!(thatObject instanceof S2RegionUnion)) { + return false; + } + S2RegionUnion that = (S2RegionUnion) thatObject; + return Arrays.deepEquals(regions, that.regions); + } + + @Override + public int hashCode() { + return Arrays.hashCode(regions); + } +} diff --git a/core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/S2Shape.java b/core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/S2Shape.java new file mode 100644 index 0000000000..d2f2d7da9b --- /dev/null +++ b/core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/S2Shape.java @@ -0,0 +1,249 @@ +/* + * Copyright 2014 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.mogo.eagle.core.utilcode.geometry; + +import com.google.common.annotations.GwtCompatible; +import com.google.common.base.Preconditions; +import java.util.AbstractList; +import java.util.List; + +/** + * S2Shape is an abstract base class that defines a shape. Typically it wraps some other geometric + * object in order to provide access to its edges without duplicating the edge data. + */ +@GwtCompatible +public interface S2Shape { + /** Returns the number of edges in this shape. */ + int numEdges(); + + /** + * Returns the edge for the given index in {@code result}. Must not return zero-length edges. + * + * @param index which edge to set into {@code result}, from 0 to {@link #numEdges()} - 1 + */ + void getEdge(int index, MutableEdge result); + + /** + * Returns true if this shape has an interior, i.e. the shape consists of one or more closed + * non-intersecting loops. + */ + boolean hasInterior(); + + /** + * Returns true if this shape contains {@link S2#origin()}. Should return false for shapes that do + * not have an interior. + */ + boolean containsOrigin(); + + /** + * A simple receiver for the endpoints of an edge. + * + *

        <>The {@link S2Edge} class is not suitable for retrieving large numbers of edges, as it + * often triggers allocations. This class is intended to allow fast retrieval of the endpoints in + * a single call. + */ + final class MutableEdge { + /** + * Endpoints of this edge last set by passing this instance to {@link S2Shape#getEdge(int, + * MutableEdge)}. + */ + S2Point a; + + S2Point b; + + /** + * Returns the leading point of the last edge retrieved via {@link S2Shape#getEdge(int, + * MutableEdge)}, or null if no edge has been retrieved. + */ + public S2Point getStart() { + return a; + } + + /** + * Returns the trailing point of the last edge retrieved via {@link S2Shape#getEdge(int, + * MutableEdge)}, or null if no edge has been retrieved. + */ + public S2Point getEnd() { + return b; + } + + /** Returns true iff 'point' is either endpoint of this edge. */ + public boolean isEndpoint(S2Point point) { + return a.equalsPoint(point) || b.equalsPoint(point); + } + + /** + * Called by implementations of {@link S2Shape#getEdge(int, MutableEdge)} to update the + * endpoints of this mutable edge to the given values. + */ + public void set(S2Point start, S2Point end) { + this.a = start; + this.b = end; + } + } + + /** + * Returns the number of contiguous edge chains in the shape. For example, a shape whose edges are + * [AB, BC, CD, AE, EF] may consist of two chains [A, B, C, D] and [A, E, F]. Every chain is + * assigned a chain id numbered sequentially starting from zero. + * + *

        An empty shape has no chains. A full shape (which contains the entire globe) has one chain + * with no edges. Other shapes should have at least one chain, and the sum of all valid {@link + * #getChainLength(int) chain lengths} should equal {@link #numEdges()} (that is, edges may only + * be used by a single chain). + * + *

        Note that it is always acceptable to implement this method by returning {@link #numEdges()} + * (i.e. every chain consists of a single edge), but this may reduce the efficiency of some + * algorithms. + */ + int numChains(); + + /** + * Returns the first edge id corresponding to the edge chain for the given chain id. The edge + * chains must form contiguous, non-overlapping ranges that cover the entire range of edge ids. + * + * @param chainId which edge chain to return its start, from 0 to {@link #numChains()} - 1 + */ + int getChainStart(int chainId); + + /** + * Returns the number of edge ids corresponding to the edge chain for the given chain id. The edge + * chains must form contiguous, non-overlapping ranges that cover the entire range of edge ids. + * + * @param chainId which edge chain to return its length, from 0 to {@link #numChains()} - 1 + */ + int getChainLength(int chainId); + + /** + * Returns the edge for the given chain id and offset in {@code result}. Must not return + * zero-length edges. + * + * @param chainId which chain contains the edge to return, from 0 to {@link #numChains()} - 1 + * @param offset position from chain start for the edge to return, from 0 to {@link + * #getChainLength(int)} - 1 + */ + void getChainEdge(int chainId, int offset, MutableEdge result); + + /** + * Returns the start point of the edge that would be returned by {@link S2Shape#getChainEdge}, + * or the endpoint of the last edge if {@code edgeOffset==getChainLength(chainId)}. + */ + S2Point getChainVertex(int chainId, int edgeOffset); + + /** + * Returns a view of the vertices in the given chain. Note {@link S2Shape#dimension 2D} shapes + * omit the last vertex, as it's a duplicate of the first. + */ + default List chain(int chain) { + return new AbstractList() { + int length = getChainLength(chain) + (dimension() & 1); + @Override public int size() { + return length; + } + @Override public S2Point get(int index) { + return getChainVertex(chain, index); + } + }; + } + + /** Returns a view of the {@link #chain chains} in this shape. */ + default List> chains() { + return new AbstractList>() { + @Override public int size() { + return numChains(); + } + @Override public List get(int index) { + return chain(index); + } + }; + } + + /** + * Returns the dimension of the geometry represented by this shape. + * + *

          + *
        • 0 - Point geometry. Each point is represented as a degenerate edge. + *
        • 1 - Polyline geometry. Polyline edges may be degenerate. A shape may represent any number + * of polylines. Polylines edges may intersect. + *
        • 2 - Polygon geometry. Edges should be oriented such that the polygon interior is always + * on the left. In theory the edges may be returned in any order, but typically the edges + * are organized as a collection of edge chains where each chain represents one polygon + * loop. Polygons may have degeneracies, e.g., degenerate edges or sibling pairs consisting + * of an edge and its corresponding reversed edge. A polygon loop may also be full + * (containing all points on the sphere); by convention this is represented as a chain with + * no edges. + *
        + * + *

        Note that this method allows degenerate geometry of different dimensions to be + * distinguished, e.g., it allows a point to be distinguished from a polyline or polygon that has + * been simplified to a single point. + */ + int dimension(); + + /** Returns a point referenced to, i.e. indicating containment by, this shape. */ + default ReferencePoint getReferencePoint() { + Preconditions.checkState(dimension() == 2); + return ReferencePoint.create(S2.origin(), containsOrigin()); + } + + /** A point with a known containment relationship. */ + abstract class ReferencePoint extends S2Point { + private static final ReferencePoint ORIGIN_INSIDE = create(S2.origin(), true); + private static final ReferencePoint ORIGIN_OUTSIDE = create(S2.origin(), false); + + private ReferencePoint(S2Point p) { + super(p.x, p.y, p.z); + } + + /** Returns true if this point is contained by the reference shape. */ + public abstract boolean contained(); + + /** + * Returns a referenced point at an arbitrary position, suitable for shapes that contain all + * points or no points. + */ + public static ReferencePoint create(boolean contained) { + return contained ? ORIGIN_INSIDE : ORIGIN_OUTSIDE; + } + + /** Creates a referenced point at position 'p', with known containment 'contained'. */ + public static ReferencePoint create(S2Point p, boolean contained) { + if (contained) { + return new ReferencePoint(p) { + @Override + public boolean contained() { + return true; + } + }; + } else { + return new ReferencePoint(p) { + @Override + public boolean contained() { + return false; + } + }; + } + } + + @Override + public boolean equals(Object o) { + return o instanceof ReferencePoint + && super.equals(o) + && contained() == ((ReferencePoint) o).contained(); + } + } +} diff --git a/core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/S2ShapeAspect.java b/core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/S2ShapeAspect.java new file mode 100644 index 0000000000..81fd7d133d --- /dev/null +++ b/core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/S2ShapeAspect.java @@ -0,0 +1,426 @@ +/* + * Copyright 2019 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.mogo.eagle.core.utilcode.geometry; + +import com.google.common.annotations.GwtIncompatible; +import com.google.common.base.Preconditions; +import com.google.common.collect.Iterables; +import com.mogo.eagle.core.utilcode.geometry.S2Shape.MutableEdge; +import java.util.AbstractList; +import java.util.Arrays; +import java.util.List; + +/** + * A set of partial {@link S2Shape shape} implementations, effectively breaking down the S2Shape API + * into several aspects, each focused on a subset of the overall API: + * + *

          + *
        • {@link VertexAspect} provides a logical list of vertices, where the 'vertexId' is + * at least 0 and less than {@link VertexAspect#numVertices}. Each implementation stores the list in + * a different way, for example a {@link ChainAspect.Simple.Packed packed array}. This isn't part of + * the S2Shape API, but is provided for use by the other aspects. + *
        • {@link EdgeAspect} provides the 'vertexId' that starts and ends each edge or each + * chain/offset, where the 'edgeId' is at least 0 and less than {@link EdgeAspect#numEdges}, the + * 'chainId' is at least 0 and less than {@link ChainAspect#numChains}, and the 'edgeOffset' is at + * least {@code edgeId(chainId)} and less than {@code edgeId(chainid+1)}. For example, the endpoint + * of the last {@link EdgeAspect.Closed closed} edge wraps back to the first vertex of that chain. + *
        • {@link ChainAspect} provides a mapping between chains and edge ranges, where 'chainId' is at + * least 0 and less than {@link ChainAspect#numChains}, and the {@link ChainAspect#getChainStart} + * and {@link ChainAspect#getChainLength} methods provide the 'edgeId' range of each chain. + *
        • {@link TopoAspect} provides the methods to relate a point in the world to the interior, + * exterior, or boundary of the shape. + * + *

          There may be fewer edges than vertices, e.g. 2 vertices can define 1 edge. + */ +@GwtIncompatible("Insufficient support for generics") +interface S2ShapeAspect { + /** A provider of S2Point given a 'vertexId', allowing alternate storage options. */ + interface VertexAspect { + /** Returns the number of vertices. May be different from {@link S2Shape#numEdges}. */ + int numVertices(); + + /** Returns a vertex of this shape, from 0 (inclusive) to {@link #numVertices} (exclusive). */ + S2Point vertex(int vertexId); + + /** Returns the vertices in this shape. Less efficient but may be more convenient. */ + default List vertices() { + return new AbstractList() { + @Override public int size() { + return numVertices(); + } + @Override public S2Point get(int index) { + return vertex(index); + } + }; + } + } + + /** + * A provider of the 'vertexId' for the start and end of each 'edgeId' or 'chainId'/'edgeOffset', + * allowing alternate edge/vertex mappings. + */ + interface EdgeAspect { + /** + * Returns the vertexId that starts 'edgeId', assuming + * {@code edgeId(chainId) <= edgeId && edgeId < edgeId(chainId + 1)}. + */ + int vertexId(int chainId, int edgeId); + + /** + * Converts the given array of 'vertexId' values in place, yielding an array of 'edgeId' values + * that start each chain. This requires knowledge of the edge/vertex mapping, and hence this + * aspect of S2Shape construction is delegated here. + */ + void adjustChains(int ... chainStarts); + + /** + * Returns the start point of the edge that would be returned by {@link S2Shape#getChainEdge}, + * or the endpoint of the last edge if {@code edgeOffset==getChainLength(chainId)}. + */ + S2Point getChainVertex(int chainId, int edgeOffset); + + /** Provides {@link S2Shape#numEdges}. */ + int numEdges(); + + /** Provides {@link S2Shape#getEdge}. */ + void getEdge(int edgeId, MutableEdge result); + + /** Provides {@link S2Shape#getChainEdge}. */ + void getChainEdge(int chainId, int edgeOffset, MutableEdge result); + + /** Chains are closed, that is, there is an implicit edge between the ends of each chain. */ + interface Closed extends Mixed { + @Override default void adjustChains(int ... chainStarts) { + } + + @Override default int numEdges() { + return numVertices(); + } + + @Override default void getEdge(int edgeId, MutableEdge result) { + // Note edgeId=vertexId, since the last edge is implicit. + result.set(vertex(edgeId), vertex(vertexId(chainId(edgeId), edgeId + 1))); + } + + @Override default void getChainEdge(int chainId, int edgeOffset, MutableEdge result) { + int edgeId = getChainStart(chainId) + edgeOffset; + result.set(vertex(edgeId), vertex(vertexId(chainId, edgeId + 1))); + } + + @Override default S2Point getChainVertex(int chainId, int edgeOffset) { + return vertex(vertexId(chainId, getChainStart(chainId) + edgeOffset)); + } + + @Override default int vertexId(int chainId, int edgeId) { + return edgeId < edgeId(chainId + 1) ? edgeId : getChainStart(chainId); + } + } + + /** Chains are open, that is, there is no implicit edge between the ends of each chain. */ + interface Open extends Mixed { + @Override default void adjustChains(int ... chainStarts) { + Preconditions.checkArgument(chainStarts.length > 0, "Must have at least 1 chain."); + int last = chainStarts[0]; + for (int i = 1; i < chainStarts.length; i++) { + int offset = chainStarts[i]; + chainStarts[i] -= i; + Preconditions.checkArgument(last != offset, "Must have at least 1 edge."); + last = offset; + } + } + + @Override default int numEdges() { + return numVertices() - numChains(); + } + + @Override default void getEdge(int edgeId, MutableEdge result) { + int vertexId = vertexId(chainId(edgeId), edgeId); + result.set(vertex(vertexId), vertex(vertexId + 1)); + } + + @Override default void getChainEdge(int chainId, int edgeOffset, MutableEdge result) { + int vertexId = vertexId(chainId, getChainStart(chainId) + edgeOffset); + result.set(vertex(vertexId), vertex(vertexId + 1)); + } + + @Override default S2Point getChainVertex(int chainId, int edgeOffset) { + return vertex(vertexId(chainId, getChainStart(chainId) + edgeOffset)); + } + + @Override default int vertexId(int chainId, int edgeId) { + return chainId + edgeId; + } + } + } + + /** A provider of the 'edgeId' ranges for each chain, allowing alternate chain representations. */ + interface ChainAspect { + /** Returns the chain ID of a given edge. */ + int chainId(int edgeId); + + /** Returns start edge ID of a chain, or the number of edges if {@code chainId==numChains()}. */ + int edgeId(int chainId); + + /** Provides {@link S2Shape#numChains}. */ + int numChains(); + + /** Provides {@link S2Shape#getChainStart}. */ + int getChainStart(int chainId); + + /** Provides {@link S2Shape#getChainLength}. */ + int getChainLength(int chainId); + + /** A single non-empty chain. */ + abstract class Simple implements Mixed { + @Override public int numChains() { + return 1; + } + + @Override public int getChainStart(int chainId) { + Preconditions.checkElementIndex(chainId, 1); + return 0; + } + + @Override public int getChainLength(int chainId) { + Preconditions.checkElementIndex(chainId, 1); + return numEdges(); + } + + @Override public int edgeId(int chainId) { + switch (chainId) { + case 0: + return 0; + case 1: + return numEdges(); + default: + throw new IndexOutOfBoundsException("Invalid chain " + chainId); + } + } + + @Override public int chainId(int edgeIndex) { + Preconditions.checkElementIndex(edgeIndex, numEdges()); + return 0; + } + + /** A simple chain of S2Point references. */ + abstract static class Array extends Simple { + private final S2Point[] vertices; + + Array(Iterable vertices) { + this.vertices = toArray(vertices); + } + + @Override public int numVertices() { + return vertices.length; + } + + @Override public S2Point vertex(int index) { + return vertices[index]; + } + + /** Returns an array of the given vertices. */ + // Note this implementation overcomes lack of GWT support for Iterables.toArray. + private static S2Point[] toArray(Iterable vertices) { + S2Point[] array = new S2Point[Iterables.size(vertices)]; + int offset = 0; + for (S2Point v : vertices) { + array[offset++] = v; + } + return array; + } + } + + /** A simple chain of packed coordinates. */ + abstract static class Packed extends Simple { + private final double[] coordinates; + + public Packed(Iterable vertices) { + this.coordinates = toArray(vertices); + } + + @Override public int numVertices() { + return coordinates.length / 3; + } + + @Override public S2Point vertex(int index) { + return vertex(coordinates, index); + } + + private static double[] toArray(Iterable vertices) { + double[] coordinates = new double[3 * Iterables.size(vertices)]; + int offset = 0; + for (S2Point v : vertices) { + coordinates[offset++] = v.x; + coordinates[offset++] = v.y; + coordinates[offset++] = v.z; + } + return coordinates; + } + + private static S2Point vertex(double[] coordinates, int index) { + int offset = 3 * index; + return new S2Point(coordinates[offset], coordinates[offset + 1], coordinates[offset + 2]); + } + } + + /** A simple chain of packed cell centers. */ + abstract static class Snapped extends Simple { + private final long[] vertices; + + public Snapped(Iterable vertices) { + this.vertices = toArray(vertices); + } + + @Override public int numVertices() { + return vertices.length; + } + + @Override public S2Point vertex(int index) { + return new S2CellId(vertices[index]).toPoint(); + } + + private static long[] toArray(Iterable vertices) { + long[] ids = new long[Iterables.size(vertices)]; + int offset = 0; + for (S2CellId vertex : vertices) { + ids[offset++] = vertex.id(); + } + return ids; + } + } + } + + /** A sequence of chains, represented as an array of the first 'edgeId' for each chain. */ + abstract class Multi implements Mixed { + private final int[] cumulativeEdges; + + public Multi(Iterable> chains) { + this.cumulativeEdges = new int[Iterables.size(chains) + 1]; + int sum = 0; + int offset = 0; + for (Iterable chain : chains) { + cumulativeEdges[offset++] = sum; + sum += Iterables.size(chain); + } + cumulativeEdges[offset] = sum; + adjustChains(cumulativeEdges); + } + + Multi(int[] cumulativeEdges) { + this.cumulativeEdges = cumulativeEdges; + adjustChains(cumulativeEdges); + } + + @Override public final int numChains() { + return cumulativeEdges.length - 1; + } + + @Override public final int edgeId(int chainId) { + return cumulativeEdges[chainId]; + } + + @Override public final int getChainStart(int chainId) { + Preconditions.checkElementIndex(chainId, numChains()); + return edgeId(chainId); + } + + @Override public final int getChainLength(int chainId) { + return edgeId(chainId + 1) - edgeId(chainId); + } + + @Override public final int chainId(int edgeId) { + int chainId = Arrays.binarySearch(cumulativeEdges, edgeId); + if (chainId < 0) { + chainId = -chainId - 2; + } + // The binary search may have landed on an empty chain, which cannot match 'edgeId'. + while (getChainLength(chainId) == 0) { + chainId++; + } + return chainId; + } + + /** An array of S2Point references for multiple chains. */ + abstract static class Array extends Multi { + private final S2Point[] vertices; + + Array(Iterable> chains) { + super(chains); + this.vertices = Simple.Array.toArray(Iterables.concat(chains)); + } + + @Override public int numVertices() { + return vertices.length; + } + + @Override public S2Point vertex(int index) { + return vertices[index]; + } + } + + /** Packed coordinates for multiple chains. */ + abstract static class Packed extends Multi { + private final double[] coordinates; + + public Packed(Iterable> chains) { + super(chains); + this.coordinates = Simple.Packed.toArray(Iterables.concat(chains)); + } + + @Override public int numVertices() { + return coordinates.length / 3; + } + + @Override public S2Point vertex(int index) { + return Simple.Packed.vertex(coordinates, index); + } + } + + /** Snapped cell centers for multiple chains. */ + abstract static class Snapped extends Multi { + private final long[] vertices; + + public Snapped(Iterable> chains) { + super(chains); + this.vertices = Simple.Snapped.toArray(Iterables.concat(chains)); + } + + @Override public int numVertices() { + return vertices.length; + } + + @Override public S2Point vertex(int index) { + return new S2CellId(vertices[index]).toPoint(); + } + } + } + } + + /** How world positions are classified as exterior, interior, or on the boundary of the object. */ + interface TopoAspect { + /** Provides {@link S2Shape#hasInterior}. */ + boolean hasInterior(); + + /** Provides {@link S2Shape#containsOrigin}. */ + boolean containsOrigin(); + + /** Provides {@link S2Shape#dimension}. */ + int dimension(); + } + + /** A full S2Shape that mixes together each aspect. */ + interface Mixed extends S2Shape, VertexAspect, EdgeAspect, ChainAspect, TopoAspect {} +} diff --git a/core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/S2ShapeIndex.java b/core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/S2ShapeIndex.java new file mode 100644 index 0000000000..961a44a757 --- /dev/null +++ b/core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/S2ShapeIndex.java @@ -0,0 +1,2051 @@ +/* + * Copyright 2014 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.mogo.eagle.core.utilcode.geometry; + +import com.google.common.annotations.GwtCompatible; +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Preconditions; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Lists; +import com.mogo.eagle.core.utilcode.geometry.S2Shape.MutableEdge; +import java.io.Serializable; +import java.util.AbstractList; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.RandomAccess; +import javax.annotation.Nullable; + +@GwtCompatible +public strictfp class S2ShapeIndex implements Serializable { + private static final long serialVersionUID = 1L; + + /** + * The amount in UV coordinates by which cells are "padded" to compensate for numerical errors + * when clipping line segments to cell boundaries. The total error when clipping an edge comes + * from two sources: + * + *

            + *
          1. Clipping the original spherical edge to a cube face (the "face edge"). The maximum error + * in this step is {@link S2EdgeUtil#FACE_CLIP_ERROR_UV_COORD}. + *
          2. Clipping the face edge to the u- or v-coordinate of a cell boundary. The maximum error in + * this step is {@link S2EdgeUtil#EDGE_CLIP_ERROR_UV_COORD}. + *
          + * + *

          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. + */ + public static final double CELL_PADDING = + 2 * (S2EdgeUtil.FACE_CLIP_ERROR_UV_COORD + S2EdgeUtil.EDGE_CLIP_ERROR_UV_COORD); + + /** + * Default maximum number of edges per cell (not counting 'long' edges). Reasonable values range + * from 10 to 50. Small values makes queries faster, while large values make construction faster + * and use less memory. + */ + public static final int DEFAULT_MAX_EDGES_PER_CELL = 10; + + /** + * Default maximum cell size, relative to an edge's length, for which that edge is considered + * 'long'. Long edges are not counted towards {@link Options#maxEdgesPerCell}. The size and speed + * of the index are typically not very sensitive to this parameter. Reasonable values range from + * 0.1 to 10, with smaller values causing more aggressive subdivision of long edges grouped + * closely together. + */ + public static final double DEFAULT_CELL_SIZE_TO_LONG_EDGE_RATIO = 1.0; + + /** + * The minimum fraction of 'short' edges that must be present in a cell in order for it to be + * subdivided. If this parameter is non-zero then the total index size and construction time are + * guaranteed to be linear in the number of input edges, where the constant of proportionality has + * the form (c1 + c2 * (1 - f) / f). Reasonable values range from 0.1 to perhaps 0.95. Values up + * to about 0.8 have almost no effect on 'normal' geometry except for a small increase in index + * construction time (proportional to f) for very large inputs. For worst-case geometry, larger + * parameter values result in indexes that are smaller and faster to construct but have worse + * query performance (due to having more edges per cell). Essentially this parameter provides + * control over a space-time trade-off that largely affects only pathological geometry. + * + *

          Specifically, the maximum index size is

          O((c1 + c2 * (1 - f) / f) * n)
          , where n + * is the number of input edges, f is this parameter value, and constant c2 is roughly 20 times + * larger than constant c1. The exact values of c1 and c2 depend on + * {@link Options#cellSizeToLongEdgeRatio} and {@link Options#maxEdgesPerCell} parameters and + * certain properties of the input geometry such as whether it consists of O(1) shapes, whether it + * includes polygons, and whether the polygon interiors are disjoint. + * + *

          The main factors to consider when choosing this parameter are: + *

            + *
          • For pathological geometry, larger values result in indexes that are smaller and faster to + * construct but have worse query performance, due to having more edges per cell. However, note + * that even a setting of 0.1 reduces the worst case by 100x compared with a setting of 0.001. + *
          • For normal geometry, values up to about 0.8 result in indexes that are virtually unchanged + * except for a slight increase in index construction time, proportional to the parameter value f, + * for very large inputs. With millions of edges, indexing time increases by about (15% * f), e.g. + * a parameter value of 0.5 slows down indexing for very large inputs by about 7.5%. Indexing time + * for small inputs is unaffected. + *
          • Values larger than about 0.8 start to affect index construction even for normal geometry, + * resulting in smaller indexes and faster construction times but gradually worse query + * performance. + *
          + * + *

          The default value of 0.2 was chosen to make index construction as fast as possible while + * still protecting against possible quadratic space usage. + */ + static final double MIN_SHORT_EDGE_FRACTION = 0.2; + + /** + * The current encoding version. When adding a new encoding, be aware that old binaries will not + * be able to decode it. + */ + public static final int CURRENT_ENCODING_VERSION = 0; + + /** The options supplied for this index. */ + private final Options options; + + /** Shapes currently in the index. */ + protected List shapes; + + /** + * Essentially a map from each non-overlapping cell id to the shapes that intersect that cell, + * clipped to include only the edges that intersect. + * + *

          Note that this field is updated lazily. Accessing the iterator is the most common way to + * construct the index. + */ + private List cells = Collections.emptyList(); + + /** The index of the first shape that has been queued for insertion but not processed yet. */ + private int pendingInsertionsBegin = 0; + + /** The shapes that have been queued for removal but not processed yet (not yet used.) */ + private final List pendingRemovals = Lists.newArrayList(); + + /** + * If true, the index is up to date. If false the index is updating or stale and requires an + * update. This is marked volatile to avoid memory consistency errors with this field, which + * allows us to avoid taking a lock when no update is required. + */ + private volatile boolean isIndexFresh = true; + + /** Creates an S2ShapeIndex that uses the default options, {@link Options}. */ + public S2ShapeIndex() { + this(new Options()); + } + + /** Creates an S2ShapeIndex with the given options. */ + public S2ShapeIndex(Options options) { + this.options = options; + shapes = new ArrayList<>(); + } + + /** Returns the options used for this index. */ + public Options options() { + return options; + } + + /** + * Returns an immutable list view of shapes in the index. When shapes are added or removed, the + * returned view is updated as well. + */ + public List getShapes() { + return Collections.unmodifiableList(shapes); + } + + /** Adds the given shape to this index. Invalidates all iterators and their associated data. */ + public void add(S2Shape shape) { + // Insertions are processed lazily by applyUpdates(). All we do now is assign a unique id to the + // shape, sequentially starting from 0 in the order shapes are inserted. + shapes.add(shape); + isIndexFresh = false; + } + + // TODO(user): Implement remove() and incremental calls to add(). + + /** + * Currently not implemented. Will eventually remove the given shape from the index, and + * invalidate all iterators and their associated data. + * + * @param shape the shape to remove + */ + public void remove(S2Shape shape) { + throw new UnsupportedOperationException("Not implemented yet"); + } + + /** Clears the contents of the index and resets it to its original state. */ + public void reset() { + cells = Collections.emptyList(); + pendingRemovals.clear(); + shapes.clear(); + isIndexFresh = false; + pendingInsertionsBegin = 0; + } + + /** + * Returns a new iterator over the cells of this index, positioned at the first cell in the index, + * after initializing any pending updates. + */ + public S2Iterator iterator() { + applyUpdates(); + return S2Iterator.create(cells); + } + + /** + * Returns true if there are no pending updates that need to be applied. This can be useful to + * avoid building the index unnecessarily, or for choosing between two different algorithms + * depending on whether the index is available. + */ + public boolean isFresh() { + return isIndexFresh; + } + + /** + * Ensures pending updates have been applied, returning immediately if the index is fresh as + * reported by {@link #isFresh()}, and otherwise blocking while the index is built. + * + *

          This operation is thread safe, guarded by 'this'. + */ + @VisibleForTesting + void applyUpdates() { + // The most common case is that the index is already fresh. So just return in that case without + // taking a lock. + if (isIndexFresh) { + return; + } + + // Otherwise an update is needed, so lock on 'this'. One of the contending threads will "win" + // and update the index, and the others will just immediately exit the lock. + synchronized (this) { + if (!isIndexFresh) { + // This thread won the race and must do the update. + + Preconditions.checkState(cells.isEmpty(), "Incremental updates not supported yet"); + + int numEdges = 0; + for (int i = pendingInsertionsBegin; i < shapes.size(); i++) { + numEdges += shapes.get(i).numEdges(); + } + + // As a a pessimistic overestimate, assume we will have about 50% of the edges cross into + // other cells, and cells end up about 50% full. + cells = createList(3 * numEdges / options.maxEdgesPerCell / 4); + + // Create a list to hold edges that intersect each face, assuming the worse case scenario of + // every edge intersecting every face. By far the most common case is that all edges + // intersect one face, and the other lists are unused. The createList() method thus attempts + // to avoid overhead for lists until they are actively used; i.e. numEdges edges are not + // allocated right here, this is just advice on what approach to use. + List> allEdges = createList(6); + for (int face = 0; face < 6; face++) { + List edges = createList(numEdges); + allEdges.add(edges); + } + + InteriorTracker tracker = new InteriorTracker(shapes.size() - pendingInsertionsBegin); + for (int i = pendingInsertionsBegin; i < shapes.size(); i++) { + addShapeEdges(i, allEdges, tracker); + } + for (int face = 0; face < 6; face++) { + updateFaceEdges(face, allEdges.get(face), tracker); + // Save memory by clearing each set of face edges after we are done with them. + allEdges.set(face, null); + } + pendingInsertionsBegin = shapes.size(); + isIndexFresh = true; + } + } + } + + /** + * Reserves an appropriate amount of space for the top-level face edges. These lists are + * responsible for most of the temporary memory usage during index construction. Furthermore, if + * the arrays are grown via add() then a large fraction of the total run time consists of copying + * data as these arrays grow, or handling GC events since reallocating a large array generates + * objects that are difficult for the GC to reap cleanly without a stop-the-world collection. + * + *

          However, creating the lists of edges for each face with this method is about 1% slower than + * just using a List with smooth capacity increases. See {@link ShardedList} for details on the + * implementation, but note also that allocating single large lists is hard on the Java garbage + * collection environment, so not only is this method slower in the absence of GC measurements, it + * is significantly faster when you also consider the impact of very large array allocations on + * the garbage collector. + */ + @SuppressWarnings("unused") + private void reserveSpace(int numEdges, List> allEdges) { + // If the number of edges is relatively small, then the fastest approach is to simply reserve + // space on every face for the maximum possible number of edges. + final int maxCheapMemoryBytes = 10 << 20; // 10MB + final int faceEdgeSize = 140; // bytes + final int maxCheapNumEdges = maxCheapMemoryBytes / (6 * faceEdgeSize); + if (numEdges <= maxCheapNumEdges) { + for (int face = 0; face < 6; face++) { + List edges = new SimpleList(numEdges); + allEdges.add(edges); + } + return; + } + + // Otherwise we estimate the number of edges on each face by taking a random sample. The goal is + // to come up with an estimate that is fast and accurate for non-pathological geometry. If our + // estimates happen to be wrong, the list will still grow automatically - the main side effects + // are that memory usage will be larger (by up to a factor of 3), and + // constructing the index will be about 10% slower. + // + // Given a desired sample size, we choose equally spaced edges from throughout the entire data + // set. We use a Bresenham-type algorithm to choose the samples. + final int desiredSampleSize = 10000; + final int sampleInterval = Math.max(1, numEdges / desiredSampleSize); + + // Initialize "edgeId" to be midway through the first sample interval. Because samples are + // equally spaced the actual sample size may differ slightly from the desired sample size. + int edgeId = sampleInterval / 2; + MutableEdge edge = new MutableEdge(); + final int actualSampleSize = (numEdges + edgeId) / sampleInterval; + int[] faceCount = {0, 0, 0, 0, 0, 0}; + for (int i = pendingInsertionsBegin; i < shapes.size(); i++) { + S2Shape shape = shapes.get(i); + edgeId += shape.numEdges(); + while (edgeId >= sampleInterval) { + edgeId -= sampleInterval; + shape.getEdge(edgeId, edge); + // For speed, we only count the face containing one endpoint of the edge. In general the + // edge could span all 6 faces (with padding), but it's not worth the expense to compute + // this more accurately. + faceCount[S2Projections.xyzToFace(edge.a)]++; + } + } + + // Now given the raw face counts, compute a confidence interval such that we will be unlikely to + // allocate too little space. Computing accurate binomial confidence intervals is expensive and + // not really necessary. Instead we use a simple approximation: + // + // - For any face with at least 1 sample, we use at least a 4-sigma confidence interval. (The + // chosen width is adequate for the worst case accuracy, which occurs when the face contains + // approximately 50% of the edges.) Assuming that our sample is representative, the + // probability of reserving too little space is approximately 1 in 30,000. + // - For faces with no samples at all, we don't bother reserving space. It is quite likely that + // such faces are truly empty, so we save time and memory this way. If the face does contain + // some edges, there will only be a few so it is fine to let the list grow automatically. + // + // On average, we reserve 2% extra space for each face that has geometry. + + // maxSemiWidth is the maximum semi-width over all probabilities p of a 4-sigma binomial + // confidence interval with a sample size of 10,000. + final double maxSemiWidth = 0.02; + final double sampleRatio = 1.0 / actualSampleSize; + for (int face = 0; face < 6; face++) { + List edges; + if (faceCount[face] > 0) { + int fraction = (int) (sampleRatio * faceCount[face] + maxSemiWidth); + edges = new SimpleList(1 + fraction * numEdges); + } else { + edges = new SimpleList(1); + } + allEdges.add(edges); + } + } + + /** + * Clips all edges of the given shape to the six cube faces, and adds the clipped edges to {@code + * allEdges}. + */ + private void addShapeEdges(int shapeId, List> allEdges, InteriorTracker tracker) { + S2Shape shape = shapes.get(shapeId); + boolean hasInterior = shape.hasInterior(); + if (hasInterior) { + tracker.addShape(shapeId, shape); + } + int numEdges = shape.numEdges(); + MutableEdge edge = new MutableEdge(); + R2Vector a = new R2Vector(); + R2Vector b = new R2Vector(); + double ratio = options.getCellSizeToLongEdgeRatio(); + + for (int e = 0; e < numEdges; e++) { + shape.getEdge(e, edge); + if (hasInterior) { + tracker.testEdge(shapeId, edge.a, edge.b); + } + + // Fast path: both endpoints are on the same face, and are far enough from + // the edge of the face that don't intersect any (padded) adjacent face. + int aFace = S2Projections.xyzToFace(edge.a); + if (aFace == S2Projections.xyzToFace(edge.b)) { + S2Projections.validFaceXyzToUv(aFace, edge.a, a); + S2Projections.validFaceXyzToUv(aFace, edge.b, b); + final double kMaxUV = 1 - CELL_PADDING; + if (Math.abs(a.x) <= kMaxUV + && Math.abs(a.y) <= kMaxUV + && Math.abs(b.x) <= kMaxUV + && Math.abs(b.y) <= kMaxUV) { + allEdges.get(aFace).add(new FaceEdge(shapeId, e, edge.a, edge.b, a, b, ratio)); + continue; + } + } + + // Otherwise we simply clip the edge to all six faces. + for (int face = 0; face < 6; face++) { + if (S2EdgeUtil.clipToPaddedFace(edge.a, edge.b, face, CELL_PADDING, a, b)) { + allEdges.get(face).add(new FaceEdge(shapeId, e, edge.a, edge.b, a, b, ratio)); + } + } + } + } + + /** + * Given a face and a list of edges that intersect that face, insert or remove all the edges from + * the index. (An edge is inserted if shape(id) is not null, and removed otherwise.) + */ + private void updateFaceEdges(int face, List faceEdges, InteriorTracker tracker) { + int numEdges = faceEdges.size(); + if (numEdges == 0 && tracker.focusCount == 0) { + return; + } + + // Create the initial ClippedEdge for each FaceEdge. Additional clipped edges are created when + // edges are split between child cells. + List clippedEdges = createList(numEdges); + R2Rect bound = R2Rect.empty(); + for (int i = 0; i < numEdges; i++) { + FaceEdge edge = faceEdges.get(i); + ClippedEdge clipped = new ClippedEdge(); + clipped.orig = edge; + clipped.bound.x().initFromPointPair(edge.ax, edge.bx); + clipped.bound.y().initFromPointPair(edge.ay, edge.by); + clippedEdges.add(clipped); + bound.addRect(clipped.bound); + } + + // Construct the initial face cell containing all the edges, and then update all the edges in + // the index recursively. + S2CellId faceId = S2CellId.fromFace(face); + S2PaddedCell pcell = new S2PaddedCell(faceId, CELL_PADDING); + EdgeAllocator alloc = new EdgeAllocator(clippedEdges.size()); + if (numEdges > 0) { + S2CellId shrunkId = pcell.shrinkToFit(bound); + if (shrunkId.id() != pcell.id().id()) { + // All the edges are contained by some descendant of the face cell. We can save a lot of + // work by starting directly with that cell, but if we are in the interior of at least one + // shape then we need to create index entries for the cells we are skipping over. + skipCellRange(faceId.rangeMin(), shrunkId.rangeMin(), tracker, alloc); + pcell = new S2PaddedCell(shrunkId, CELL_PADDING); + updateEdges(pcell, clippedEdges, tracker, alloc); + skipCellRange(shrunkId.rangeMax().next(), faceId.rangeMax().next(), tracker, alloc); + return; + } + } + // Otherwise (no edges, or no shrinking is possible), subdivide normally. + updateEdges(pcell, clippedEdges, tracker, alloc); + } + + /** + * Skips over the cells in the given range, creating index cells if we are currently in the + * interior of at least one shape. + */ + private void skipCellRange( + S2CellId begin, S2CellId end, InteriorTracker tracker, EdgeAllocator alloc) { + if (tracker.focusCount > 0) { + // If we are in the interior of at least one shape, then generate the list of cell ids that we + // need to visit, and create an index entry with no edges, for each one. + S2CellUnion skipped = new S2CellUnion(); + skipped.initFromBeginEnd(begin, end); + List clippedEdges = Collections.emptyList(); + for (int i = 0; i < skipped.size(); i++) { + S2PaddedCell pcell = new S2PaddedCell(skipped.cellId(i), CELL_PADDING); + updateEdges(pcell, clippedEdges, tracker, alloc); + } + } + } + + /** + * Given a cell and a set of ClippedEdges whose bounding boxes intersect that cell, insert or + * remove all the edges from the index. Temporary space for edges that need to be subdivided is + * allocated from the given EdgeAllocator. + */ + boolean makeIndexCell( + S2PaddedCell pcell, List edges, InteriorTracker tracker) { + if (edges.isEmpty() && tracker.focusCount == 0) { + // No index cell is needed. In most cases this situation is detected before we get to this + // point, but this can happen when all shapes in a cell are removed. + return true; + } + + // We can show using amortized analysis that the total index size is + // + // O(c1 * n + c2 * (1 - f) / f * n) + // + // where n is the number of input edges (and where we also count an "edge" + // for each shape with an interior but no edges), f is the value of + // FLAGS_s2shape_index_min_short_edge_fraction, and c1 and c2 are constants + // where c2 is about 20 times larger than c1. + // + // First observe that the space used by a MutableS2ShapeIndex is + // proportional to the space used by all of its index cells, and the space + // used by an S2ShapeIndexCell is proportional to the number of edges that + // intersect that cell plus the number of shapes that contain the entire + // cell ("containing shapes"). Define an "index entry" as an intersecting + // edge or containing shape stored by an index cell. Our goal is then to + // bound the number of index entries. + // + // We divide the index entries into two groups. An index entry is "short" + // if it represents an edge that was considered short in that index cell's + // parent, and "long" otherwise. (Note that the long index entries also + // include the containing shapes mentioned above.) We then bound the + // maximum number of both types of index entries by associating them with + // edges that were considered short in those index cells' parents. + // + // First consider the short index entries for a given edge E. Let S be the + // set of index cells that intersect E and where E was considered short in + // those index cells' parents. Since E was short in each parent cell, the + // width of those parent cells is at least some fraction "g" of E's length + // (as controlled by FLAGS_s2shape_index_cell_size_to_long_edge_ratio). + // Therefore the minimum width of each cell in S is also at least some + // fraction of E's length (i.e., g / 2). This implies that there are at most + // a constant number c1 of such cells, since they all intersect E and do not + // overlap, which means that there are at most (c1 * n) short entries in + // total. + // + // With index_cell_size_to_long_edge_ratio = 1.0 (the default value), it can + // be shown that c1 = 10. In other words, it is not possible for a given + // edge to intersect more than 10 index cells where it was considered short + // in those cells' parents. The value of c1 can be reduced as low c1 = 4 by + // increasing index_cell_size_to_long_edge_ratio to about 3.1. (The reason + // the minimum value is 3.1 rather than 2.0 is that this ratio is defined in + // terms of the average edge length of cells at a given level, rather than + // their minimum width, and 2 * (S2::kAvgEdge / S2::kMinWidth) ~= 3.1.) + // + // Next we consider the long index entries. Let c2 be the maximum number of + // index cells where a given edge E was considered short in those cells' + // parents. (Unlike the case above, we do not require that these cells + // intersect E.) Because the minimum width of each parent cell is at least + // some fraction of E's length and the parent cells at a given level do not + // overlap, there can be at most a small constant number of index cells at + // each level where E is considered short in those cells' parents. For + // example, consider a very short edge E that intersects the midpoint of a + // cell edge at level 0. There are 16 cells at level 30 where E was + // considered short in the parent cell, 12 cells at each of levels 29..2, and + // 4 cells at levels 1 and 0 (pretending that all 6 face cells share a common + // "parent"). This yields a total of c2 = 360 index cells. This is actually + // the worst case for index_cell_size_to_long_edge_ratio >= 3.1; with the + // default value of 1.0 it is possible to have a few more index cells at + // levels 29 and 30, for a maximum of c2 = 366 index cells. + // + // The code below subdivides a given cell only if + // + // s > f * (s + l) + // + // where "f" is the min_short_edge_fraction parameter, "s" is the number of + // short edges that intersect the cell, and "l" is the number of long edges + // that intersect the cell plus an upper bound on the number of shapes that + // contain the entire cell. (It is an upper bound rather than an exact count + // because we use the number of shapes that contain an arbitrary vertex of + // the cell.) Note that the number of long index entries in each child of + // this cell is at most "l" because no child intersects more edges than its + // parent or is entirely contained by more shapes than its parent. + // + // The inequality above can be rearranged to give + // + // l < s * (1 - f) / f + // + // This says that each long index entry in a child cell can be associated + // with at most (1 - f) / f edges that were considered short when the parent + // cell was subdivided. Furthermore we know that there are at most c2 index + // cells where a given edge was considered short in the parent cell. Since + // there are only n edges in total, this means that the maximum number of + // long index entries is at most + // + // c2 * (1 - f) / f * n + // + // and putting this together with the result for short index entries gives + // the desired bound. + // + // There are a variety of ways to make this bound tighter, e.g. when "n" is + // relatively small. For example when the indexed geometry satisfies the + // requirements of S2BooleanOperation (i.e., shape interiors are disjoint) + // and the min_short_edge_fraction parameter is not too large, then the + // constant c2 above is only about half as big (i.e., c2 ~= 180). This is + // because the worst case under these circumstances requires having many + // shapes whose interiors overlap. + + // Continue subdividing if the proposed index cell would contain too many + // edges that are "short" relative to its size (as controlled by the + // FLAGS_s2shape_index_cell_size_to_long_edge_ratio parameter). Usually "too + // many" means more than options_.max_edges_per_cell(), but this value might + // be increased if the cell has a lot of long edges and/or containing shapes. + // This strategy ensures that the total index size is linear (see above). + if (edges.size() > options.maxEdgesPerCell) { + int maxShortEdges = Math.max( + options.maxEdgesPerCell, + (int) (MIN_SHORT_EDGE_FRACTION * (edges.size() + tracker.focusCount))); + int count = 0; + for (ClippedEdge edge : edges) { + count += pcell.level() < edge.orig.maxLevel ? 1 : 0; + if (count > maxShortEdges) { + return false; + } + } + } + + // There aren't too many edges, so build a leaf cell from these edges. + + // We update the InteriorTracker as follows. For every S2Cell in the index we construct two + // edges: one edge from entry vertex of the cell to its center, and one from the cell center to + // its exit vertex. Here "entry" and "exit" refer to the S2CellId ordering, i.e. the order in + // which points are encountered along the S2 space-filling curve. The exit vertex then becomes + // the entry vertex for the next cell in the index, unless there are one or more empty + // intervening cells, in which case the InteriorTracker state is unchanged because the + // intervening cells have no edges. + + // Shift the InteriorTracker focus point to the center of the current cell. + int numEdges = edges.size(); + if (tracker.isActive() && numEdges > 0) { + if (!tracker.atCellId(pcell.id())) { + tracker.moveTo(pcell.getEntryVertex()); + } + tracker.drawTo(pcell.getCenter()); + for (int i = 0; i < numEdges; i++) { + ClippedEdge edge = edges.get(i); + FaceEdge orig = edge.orig; + if (shapes.get(orig.shapeId).hasInterior()) { + tracker.testEdge(orig.shapeId, orig.va, orig.vb); + } + } + } + + // Allocate and fill a new index cell. To get the total number of shapes we need to merge the + // shapes associated with the intersecting edges together with the shapes that happen to contain + // the cell center. + + // The first clipped shape will contain this cell. After its used this value will be null, so + // we don't waste space storing the cell ID repeatedly. + S2CellId cellId = pcell.id(); + + // To fill the index cell we merge the two sources of shapes: "edge shapes" (those that have at + // least one edge that intersects this cell), and "containing shapes" (those that contain the + // cell center.) We keep track of the index of the next intersecting edge and the next + // containing shape as we go along. + int numShapes = 0; + int edgesIndex = 0; + int trackerIndex = 0; + int nextShapeId = shapes.size(); + while (edgesIndex < numEdges || trackerIndex < tracker.focusCount) { + int edgeId; + if (edgesIndex < numEdges) { + edgeId = edges.get(edgesIndex).orig.shapeId; + } else { + edgeId = nextShapeId; + } + int trackerId; + if (trackerIndex < tracker.focusCount) { + trackerId = tracker.focusedShapes[trackerIndex]; + } else { + trackerId = nextShapeId; + } + S2ClippedShape clipped; + if (trackerId < edgeId) { + // The entire cell is in the shape interior. + clipped = S2ClippedShape.Contained.create(cellId, shapes.get(trackerId)); + cellId = null; + trackerIndex++; + } else { + // Count the number of edges for this shape and allocate space for them. + int firstEdge = edgesIndex; + while (edgesIndex < numEdges && edges.get(edgesIndex).orig.shapeId == edgeId) { + edgesIndex++; + } + boolean containsCenter = trackerId == edgeId; + clipped = + S2ClippedShape.create( + cellId, shapes.get(edgeId), containsCenter, edges, firstEdge, edgesIndex); + cellId = null; + if (containsCenter) { + trackerIndex++; + } + } + tracker.tempClippedShapes[numShapes++] = clipped; + } + + // updateEdges() visits cells in increasing order of S2CellId, so during initial construction of + // the index we can just append new cells at the end of the list. This is much faster than + // sorting the cells afterward. + cells.add(Cell.create(numShapes, tracker.tempClippedShapes)); + + // Shift the InteriorTracker focus point to the exit vertex of this cell. + if (tracker.isActive() && !edges.isEmpty()) { + tracker.drawTo(pcell.getExitVertex()); + for (int i = 0; i < numEdges; i++) { + ClippedEdge edge = edges.get(i); + FaceEdge orig = edge.orig; + if (shapes.get(orig.shapeId).hasInterior()) { + tracker.testEdge(orig.shapeId, orig.va, orig.vb); + } + } + tracker.doneCellId(pcell.id()); + } + + return true; + } + + private void updateEdges( + S2PaddedCell pcell, List edges, InteriorTracker tracker, EdgeAllocator alloc) { + // Cases where an index cell is not needed should be detected before this. + // TODO + assert !edges.isEmpty() || tracker.focusCount > 0; + + // This function is recursive with a maximum recursion depth of 30 (S2CellId.MAX_LEVEL). + + if (makeIndexCell(pcell, edges, tracker)) { + // Skip splitting if we made a cell for 'edges'. + return; + } + + // Reserve space for the edges that will be passed to each child. We select the kind of list to + // use based on how large the child edges lists could possibly be. + int numEdges = edges.size(); + List edges00 = createList(numEdges); + List edges01 = createList(numEdges); + List edges10 = createList(numEdges); + List edges11 = createList(numEdges); + List> ijEdges = ImmutableList.of(edges00, edges01, edges10, edges11); + + // Remember the current size of the EdgeAllocator so that we can free any edges that are + // allocated during edge splitting. + int allocSize = alloc.size(); + + // Compute the middle of the padded cell, defined as the rectangle in (u,v)-space that belongs + // to all four (padded) children. By comparing against the four boundaries of "middle" we can + // determine which children each edge needs to be propagated to. + R2Rect middle = pcell.middle(); + + // Build up a list edges to be passed to each child cell. The (i,j) directions are left (i=0), + // right (i=1), lower (j=0), and upper (j=1). Note that the vast majority of edges are + // propagated to a single child. + for (int i = 0; i < numEdges; i++) { + ClippedEdge edge = edges.get(i); + if (edge.bound.x().hi() <= middle.x().lo()) { + // Edge is entirely contained in the two left children. + clipVAxis(edge, middle.y(), edges00, edges01, alloc); + } else if (edge.bound.x().lo() >= middle.x().hi()) { + // Edge is entirely contained in the two right children. + clipVAxis(edge, middle.y(), edges10, edges11, alloc); + } else if (edge.bound.y().hi() <= middle.y().lo()) { + // Edge is entirely contained in the two lower children. + edges00.add(clipUBound(edge, true, middle.x().hi(), alloc)); + edges10.add(clipUBound(edge, false, middle.x().lo(), alloc)); + } else if (edge.bound.y().lo() >= middle.y().hi()) { + // Edge is entirely contained in the two upper children. + edges01.add(clipUBound(edge, true, middle.x().hi(), alloc)); + edges11.add(clipUBound(edge, false, middle.x().lo(), alloc)); + } else { + // The edge bound spans all four children. The edge itself intersects either three or four + // (padded) children. + ClippedEdge left = clipUBound(edge, true, middle.x().hi(), alloc); + clipVAxis(left, middle.y(), edges00, edges01, alloc); + ClippedEdge right = clipUBound(edge, false, middle.x().lo(), alloc); + clipVAxis(right, middle.y(), edges10, edges11, alloc); + } + } + + // Now recursively update the edges in each child. We call the children in increasing order of + // S2CellId so that when the index is first constructed, all insertions into 'cells' are at the + // end (which is much faster than sorting by cell id afterward.) + for (int pos = 0; pos < 4; pos++) { + List childEdges = ijEdges.get(S2.posToIJ(pcell.orientation(), pos)); + if (!childEdges.isEmpty() || tracker.focusCount > 0) { + S2PaddedCell childCell = pcell.childAtPos(pos); + updateEdges(childCell, childEdges, tracker, alloc); + } + } + alloc.reset(allocSize); + } + + /** + * Given an edge and two bound endpoints that need to be updated, allocates and returns a new edge + * with the updated bound. + */ + private static ClippedEdge updateBound( + ClippedEdge edge, boolean uEnd, double u, boolean vEnd, double v, EdgeAllocator alloc) { + ClippedEdge clipped = alloc.create(); + clipped.orig = edge.orig; + if (uEnd) { + clipped.bound.x().set(edge.bound.x().lo(), u); + } else { + clipped.bound.x().set(u, edge.bound.x().hi()); + } + if (vEnd) { + clipped.bound.y().set(edge.bound.y().lo(), v); + } else { + clipped.bound.y().set(v, edge.bound.y().hi()); + } + // assert !clipped.bound.isEmpty(); + // assert edge.bound.contains(clipped.bound); + return clipped; + } + + private static ClippedEdge clipUBound( + ClippedEdge edge, boolean uEnd, double u, EdgeAllocator alloc) { + // First check whether the edge actually requires any clipping. (Sometimes this method is + // called when clipping is not necessary, e.g. when one edge endpoint is in the overlap area + // between two padded child cells.) + if (!uEnd) { + if (edge.bound.x().lo() >= u) { + return edge; + } + } else { + if (edge.bound.x().hi() <= u) { + return edge; + } + } + + // We interpolate the new v-value from the endpoints of the original edge. This has two + // advantages + // + // (1) we don't need to store the clipped endpoints at all, just their bounding box; and + // (2) it avoids the accumulation of roundoff errors due to repeated interpolations. + // + // The result needs to be clamped to ensure that it is in the appropriate range. + FaceEdge e = edge.orig; + double v = edge.bound.y().clampPoint(S2EdgeUtil.interpolateDouble(u, e.ax, e.bx, e.ay, e.by)); + + // Determine which endpoint of the v-axis bound to update. If the edge slope is positive we + // update the same endpoint, otherwise we update the opposite endpoint. + boolean vEnd = ((e.ax > e.bx) != (e.ay > e.by)) ^ uEnd; + return updateBound(edge, uEnd, u, vEnd, v, alloc); + } + + private static ClippedEdge clipVBound( + ClippedEdge edge, boolean vEnd, double v, EdgeAllocator alloc) { + // See comments in clipUBound. + if (vEnd == false) { + if (edge.bound.y().lo() >= v) { + return edge; + } + } else { + if (edge.bound.y().hi() <= v) { + return edge; + } + } + FaceEdge e = edge.orig; + double u = edge.bound.x().clampPoint(S2EdgeUtil.interpolateDouble(v, e.ay, e.by, e.ax, e.bx)); + boolean uEnd = ((e.ax > e.bx) != (e.ay > e.by)) ^ vEnd; + return updateBound(edge, uEnd, u, vEnd, v, alloc); + } + + private static void clipVAxis( + ClippedEdge edge, + R1Interval middle, + List edges0, + List edges1, + EdgeAllocator alloc) { + if (edge.bound.y().hi() <= middle.lo()) { + // Edge is entirely contained in the lower child. + edges0.add(edge); + } else if (edge.bound.y().lo() >= middle.hi()) { + // Edge is entirely contained in the upper child. + edges1.add(edge); + } else { + // The edge bound spans both children. + edges0.add(clipVBound(edge, true, middle.hi(), alloc)); + edges1.add(clipVBound(edge, false, middle.lo(), alloc)); + } + } + + /** Options that affect construction of the S2ShapeIndex. */ + public static class Options implements Serializable { + private static final long serialVersionUID = 1L; + + private int maxEdgesPerCell = DEFAULT_MAX_EDGES_PER_CELL; + private double cellSizeToLongEdgeRatio = DEFAULT_CELL_SIZE_TO_LONG_EDGE_RATIO; + + /** + * Returns the maximum number of edges per cell (default 10.) If a cell has more than this many + * edges that are "long" relative to the cell size, and it is not a leaf cell, then it is + * subdivided. Whether an edge is considered "long" is controlled by the value returned by + * {@link #getCellSizeToLongEdgeRatio()}. + */ + public int getMaxEdgesPerCell() { + return maxEdgesPerCell; + } + + /** + * Sets the new number of max edges per cell. Only has an effect during index construction, + * usually triggered by calling {@link S2ShapeIndex#iterator()}. + */ + public void setMaxEdgesPerCell(int maxEdgesPerCell) { + this.maxEdgesPerCell = maxEdgesPerCell; + } + + /** + * Returns the cell size relative to the length of an edge at which it is first considered to be + * "long" (default is 1.0). Long edges do not contribute toward the decision to subdivide a cell + * further. The size and speed of the index are typically not very sensitive to this parameter. + * Reasonable values range from 0.1 to 10, with smaller values causing more aggressive + * subdivision of long edges grouped closely together. For example, a value of 2.0 means that + * the cell must be at least twice the size of the edge in order for that edge to be counted. + * There are two reasons for not counting long edges: + * + *

            + *
          1. Such edges typically need to be propagated to several children, which increases time + * and memory costs without much benefit, and + *
          2. In pathological cases, many long edges close together could force subdivision to + * continue all the way to the leaf cell level. + *
          + */ + public double getCellSizeToLongEdgeRatio() { + return cellSizeToLongEdgeRatio; + } + + /** + * Sets the new ratio of cell size to long edges. Only has an effect during index construction, + * usually triggered by calling {@link S2ShapeIndex#iterator()}. + */ + public void setCellSizeToLongEdgeRatio(double cellSizeToLongEdgeRatio) { + this.cellSizeToLongEdgeRatio = cellSizeToLongEdgeRatio; + } + } + + /** + * This class contains the set of clipped shapes within a particular index cell, sorted in + * increasing order of shape id. + * + *

          To be as memory efficient as possible, we specialize two very common cases. + * + *

            + *
          • The Cell class is extended by S2ClippedShape, and in the *very* common case of a cell + * containing just one clipped shape, we return the shape directly without wrapping it (this + * requires that the clipped shapes contain the cell IDs, rather than the Cell; more about + * that on {@link S2ClippedShape}.) + *
          • In the fairly common case of a cell intersecting two shapes, we have a BinaryCell + * implementation that is half the size of the general purpose MultiCell in that case. + *
          + */ + // TODO(b/120887495): This @VisibleForTesting annotation was being ignored by prod code. + // Please check that removing it is correct, and remove this comment along with it. + // @VisibleForTesting + public abstract static class Cell implements S2Iterator.Entry, Serializable { + private static final long serialVersionUID = 1L; + + /** Returns a Cell with a copy of the given shapes, specialized for the number of elements. */ + static Cell create(int size, S2ClippedShape[] tempClippedShapes) { + switch (size) { + case 1: + return tempClippedShapes[0]; + case 2: + return new BinaryCell(tempClippedShapes[0], tempClippedShapes[1]); + default: + return new MultiCell(Arrays.copyOf(tempClippedShapes, size)); + } + } + + @Override + public long id() { + // We store the cell ID on the first clipped shape. + return clipped(0).id(); + } + + /** Returns the number of clipped shapes in this cell. */ + // TODO(b/120887495): This @VisibleForTesting annotation was being ignored by prod code. + // Please check that removing it is correct, and remove this comment along with it. + // @VisibleForTesting + public abstract int numShapes(); + + /** + * Returns the clipped shape at the given index. + * + * @param i must be at least 0 and less than {@link #numShapes()} + */ + // TODO(b/120887495): This @VisibleForTesting annotation was being ignored by prod code. + // Please check that removing it is correct, and remove this comment along with it. + // @VisibleForTesting + public abstract S2ClippedShape clipped(int i); + + /** + * Returns the clipped shape corresponding to the given shape ID, or null if the shape does not + * intersect this cell. + */ + S2ClippedShape findClipped(S2Shape shape) { + // Linear search is fine because the number of shapes per cell is typically very small (most + // often 1), and is large only for pathological inputs (e.g. very deeply nested loops). + for (int i = 0; i < numShapes(); i++) { + S2ClippedShape clipped = clipped(i); + if (clipped.shape == shape) { + return clipped; + } + } + return null; + } + + /** A specialization of Cell for the case of two clipped shapes. Also very common. */ + private static final class BinaryCell extends Cell { + private static final long serialVersionUID = 1L; + private final S2ClippedShape shape1; + private final S2ClippedShape shape2; + + BinaryCell(S2ClippedShape shape1, S2ClippedShape shape2) { + this.shape1 = shape1; + this.shape2 = shape2; + } + + @Override + public int numShapes() { + return 2; + } + + @Override + public S2ClippedShape clipped(int i) { + switch (i) { + case 0: + return shape1; + case 1: + return shape2; + default: + throw new ArrayIndexOutOfBoundsException(); + } + } + } + + /** + * A specialization of Cell for multiple shapes per cell. Last resort, largest-memory + * implementation that is not used very often. + */ + private static final class MultiCell extends Cell { + private final S2ClippedShape[] clippedShapes; + + MultiCell(S2ClippedShape[] shapes) { + this.clippedShapes = shapes; + } + + @Override + public int numShapes() { + return clippedShapes.length; + } + + @Override + public S2ClippedShape clipped(int i) { + return clippedShapes[i]; + } + } + } + + /** + * The possible relationships between a "target" cell and the cells of the S2ShapeIndex. 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". + */ + public enum CellRelation { + /** Target is contained by an index cell. */ + INDEXED, + /** Target is subdivided into one or more index cells. */ + SUBDIVIDED, + /** Target does not intersect any index cells. */ + DISJOINT + } + + /** + * S2ClippedShape represents the part of a shape that intersects an S2Cell. It consists of the set + * of edge ids that intersect that cell, and a boolean indicating whether the center of the cell + * is inside the shape (for shapes that have an interior). Edges themselves are not clipped; we + * always use the original edges for intersection tests so that the results will be the same as + * the original shape. + * + *

          Most of the index memory is consumed here, so we have several strategies to be as efficient + * as possible: + * + *

            + *
          • Since only the first clipped shape in a cell needs to store the cell ID, we have + * subclasses for both cases based on whether a cell ID was provided. + *
          • Since no edges, one edge, and one dense range of edges are very common cases, we have + * more efficient subclasses for those situations, in addition to the general purpose + * implementation. + *
          + */ + // TODO(b/120887495): This @VisibleForTesting annotation was being ignored by prod code. + // Please check that removing it is correct, and remove this comment along with it. + // @VisibleForTesting + public abstract static class S2ClippedShape extends Cell { + static S2ClippedShape create( + S2CellId cellId, + S2Shape shape, + boolean containsCenter, + List edges, + int start, + int end) { + int numEdges = end - start; + if (numEdges == 1) { + return OneEdge.create(cellId, shape, containsCenter, edges.get(start)); + } + int edge = edges.get(start).orig.edgeId; + for (int i = 1; i < numEdges; i++) { + if (edge + i != edges.get(start + i).orig.edgeId) { + return ManyEdges.create(cellId, shape, containsCenter, edges, start, end); + } + } + return EdgeRange.create(cellId, shape, containsCenter, edge, numEdges); + } + + static S2ClippedShape create( + @Nullable S2CellId cellId, S2Shape shape, boolean containsCenter, int[] edges) { + return ManyEdges.create(cellId, shape, containsCenter, edges); + } + + static S2ClippedShape create( + @Nullable S2CellId cellId, S2Shape shape, boolean containsCenter, int offset, int count) { + return EdgeRange.create(cellId, shape, containsCenter, offset, count); + } + + /** + * If positive, this is the shape ID and the shape does not contain the center of the cell. + * Otherwise the shape ID is ~this.shapeId and the shape does contains the center of the cell. + * This is done to save memory, since this single bit of information can otherwise be padded out + * up to 4 or 8 additional bytes, depending on the fields in the subclass. + */ + private final S2Shape shape; + + private S2ClippedShape(S2Shape shape) { + this.shape = shape; + } + + /** Returns the original shape this clipped shape was clipped from. */ + public final S2Shape shape() { + return shape; + } + + /** + * Returns whether the center of the S2CellId is inside the shape, and always returns false for + * shapes that do not have an interior according to {@link S2Shape#hasInterior()}. + */ + public abstract boolean containsCenter(); + + /** Returns the number of edges that intersect the S2CellId. */ + public abstract int numEdges(); + + /** + * Returns the {@code i}th edge ID of this clipped shape. Edges are sorted in + * increasing order of edge ID. The edge IDs may be passed to the corresponding shape's {@link + * S2Shape#getEdge(int, com.mogo.eagle.core.utilcode.geometry.S2Shape.MutableEdge)} method. + * + * @param i must be at least 0 and less than {@link #numEdges} + */ + public abstract int edge(int i); + + /** Returns whether the clipped shape contains the given edge id. */ + public final boolean containsEdge(int edgeId) { + // Linear search is fast because the number of edges per shape is typically very small (less + // than 10). + for (int e = 0; e < numEdges(); e++) { + if (edge(e) == edgeId) { + return true; + } + } + return false; + } + + /** For implementing the Cell interface, this class contains just 1 shape (itself.) */ + @Override + public final int numShapes() { + return 1; + } + + /** For implementing the Cell interface, this class contains just 1 shape (itself.) */ + @Override + public final S2ClippedShape clipped(int i) { + // assert i == 0; + return this; + } + + /** + * An S2ClippedShape for a shape that completely contains the cell (no edge intersections and + * containsCenter is true.) + */ + private abstract static class Contained extends S2ClippedShape { + static Contained create(@Nullable S2CellId cellId, S2Shape shape) { + if (cellId != null) { + final long id = cellId.id(); + return new Contained(shape) { + @Override + public long id() { + return id; + } + }; + } else { + return new Contained(shape) { + @Override + public long id() { + throw new UnsupportedOperationException(); + } + }; + } + } + + private Contained(S2Shape shape) { + super(shape); + } + + @Override + public final boolean containsCenter() { + return true; + } + + @Override + public final int numEdges() { + return 0; + } + + @Override + public final int edge(int i) { + throw new ArrayIndexOutOfBoundsException(); + } + } + + /** An S2ClippedShape that contains a single edge from a given shape. Very common. */ + private abstract static class OneEdge extends S2ClippedShape { + static final OneEdge create( + @Nullable S2CellId cellId, + S2Shape shape, + boolean containsCenter, + ClippedEdge clippedEdge) { + if (cellId != null) { + final long id = cellId.id(); + if (containsCenter) { + return new OneEdge(shape, clippedEdge) { + @Override + public long id() { + return id; + } + + @Override + public boolean containsCenter() { + return true; + } + }; + } else { + return new OneEdge(shape, clippedEdge) { + @Override + public long id() { + return id; + } + + @Override + public boolean containsCenter() { + return false; + } + }; + } + } else { + if (containsCenter) { + return new OneEdge(shape, clippedEdge) { + @Override + public long id() { + throw new UnsupportedOperationException(); + } + + @Override + public boolean containsCenter() { + return true; + } + }; + } else { + return new OneEdge(shape, clippedEdge) { + @Override + public long id() { + throw new UnsupportedOperationException(); + } + + @Override + public boolean containsCenter() { + return false; + } + }; + } + } + } + + private final int edge; + + private OneEdge(S2Shape shape, ClippedEdge clippedEdge) { + super(shape); + this.edge = clippedEdge.orig.edgeId; + } + + @Override + public final int numEdges() { + return 1; + } + + @Override + public final int edge(int i) { + return edge; + } + } + + /** + * An S2ClippedShape that contains the non-contiguous edges from {@code start} to {@code end} in + * {@code edges}. A much larger object than the other subclasses of S2ClippedShape, but also the + * rarest. + */ + private abstract static class ManyEdges extends S2ClippedShape { + static ManyEdges create( + @Nullable S2CellId cellId, + S2Shape shape, + boolean containsCenter, + List edges, + int start, + int end) { + if (cellId != null) { + final long id = cellId.id(); + if (containsCenter) { + return new ManyEdges(shape, edges, start, end) { + @Override + public long id() { + return id; + } + + @Override + public boolean containsCenter() { + return true; + } + }; + } else { + return new ManyEdges(shape, edges, start, end) { + @Override + public long id() { + return id; + } + + @Override + public boolean containsCenter() { + return false; + } + }; + } + } else { + if (containsCenter) { + return new ManyEdges(shape, edges, start, end) { + @Override + public long id() { + throw new UnsupportedOperationException(); + } + + @Override + public boolean containsCenter() { + return true; + } + }; + } else { + return new ManyEdges(shape, edges, start, end) { + @Override + public long id() { + throw new UnsupportedOperationException(); + } + + @Override + public boolean containsCenter() { + return false; + } + }; + } + } + } + + static ManyEdges create( + @Nullable S2CellId cellId, S2Shape shape, boolean containsCenter, int[] edges) { + if (cellId != null) { + final long id = cellId.id(); + if (containsCenter) { + return new ManyEdges(shape, edges) { + @Override + public long id() { + return id; + } + + @Override + public boolean containsCenter() { + return true; + } + }; + } else { + return new ManyEdges(shape, edges) { + @Override + public long id() { + return id; + } + + @Override + public boolean containsCenter() { + return false; + } + }; + } + } else { + if (containsCenter) { + return new ManyEdges(shape, edges) { + @Override + public long id() { + throw new UnsupportedOperationException(); + } + + @Override + public boolean containsCenter() { + return true; + } + }; + } else { + return new ManyEdges(shape, edges) { + @Override + public long id() { + throw new UnsupportedOperationException(); + } + + @Override + public boolean containsCenter() { + return false; + } + }; + } + } + } + + private final int[] edges; + + private ManyEdges(S2Shape shape, List edges, int start, int end) { + super(shape); + this.edges = new int[end - start]; + for (int i = 0; i < this.edges.length; i++) { + this.edges[i] = edges.get(i + start).orig.edgeId; + } + } + + private ManyEdges(S2Shape shape, int[] edges) { + super(shape); + this.edges = edges; + } + + @Override + public final int numEdges() { + return edges.length; + } + + @Override + public final int edge(int i) { + return edges[i]; + } + } + + /** An S2ClippedShape containing a single range of contiguous edge IDs. Very common. */ + private abstract static class EdgeRange extends S2ClippedShape { + static EdgeRange create( + @Nullable S2CellId cellId, S2Shape shape, boolean containsCenter, int offset, int count) { + if (cellId != null) { + final long id = cellId.id(); + if (containsCenter) { + return new EdgeRange(shape, offset, count) { + @Override + public long id() { + return id; + } + + @Override + public boolean containsCenter() { + return true; + } + }; + } else { + return new EdgeRange(shape, offset, count) { + @Override + public long id() { + return id; + } + + @Override + public boolean containsCenter() { + return false; + } + }; + } + } else { + if (containsCenter) { + return new EdgeRange(shape, offset, count) { + @Override + public long id() { + throw new UnsupportedOperationException(); + } + + @Override + public boolean containsCenter() { + return true; + } + }; + } else { + return new EdgeRange(shape, offset, count) { + @Override + public long id() { + throw new UnsupportedOperationException(); + } + + @Override + public boolean containsCenter() { + return false; + } + }; + } + } + } + + private final int offset; + private final int count; + + private EdgeRange(S2Shape shape, int offset, int count) { + super(shape); + this.offset = offset; + this.count = count; + } + + @Override + public final int numEdges() { + return count; + } + + @Override + public final int edge(int i) { + return offset + i; + } + } + } + + /** + * RangeIterator is a wrapper over CellIterator that is specialized for merging shape indices. + * This class is is well-tested by S2Loop. + */ + public static final class RangeIterator { + private static final S2CellId END = S2CellId.end(0); + + private S2Iterator it; + private S2CellId id; + private S2CellId rangeMin; + private S2CellId rangeMax; + private S2ClippedShape clipped; + + public RangeIterator(S2ShapeIndex index) { + it = index.iterator(); + refresh(); + } + + /** Returns the current S2CellId or cell contents. */ + public S2CellId id() { + return id; + } + + public Cell cell() { + return it.entry(); + } + + /** + * Returns the min and max leaf cell ids covered by the current cell. If done() is true, these + * methods return a value larger than any valid cell id. + */ + public S2CellId rangeMin() { + return rangeMin; + } + + public S2CellId rangeMax() { + return rangeMax; + } + + /** Various other convenience methods for the current cell. */ + public S2ClippedShape clipped() { + return clipped; + } + + public int numEdges() { + return clipped().numEdges(); + } + + public boolean containsCenter() { + return clipped().containsCenter(); + } + + public void next() { + it.next(); + refresh(); + } + + public boolean done() { + return id().equals(END); + } + + /** + * Positions the iterator at the first cell that overlaps or follows {@code target}, i.e. such + * that rangeMax() >= target.rangeMin(). + */ + public void seekTo(RangeIterator target) { + it.seek(target.rangeMin()); + // If the current cell does not overlap 'target', it is possible that the previous cell is the + // one we are looking for. This can only happen when the previous cell contains 'target' but + // has a smaller S2CellId. + if (it.done() || it.id().rangeMin().greaterThan(target.rangeMax())) { + it.prev(); + if (it.id().rangeMax().lessThan(target.id())) { + it.next(); + } + } + refresh(); + } + + /** + * Positions the iterator at the first cell that follows {@code target}, i.e. the first cell + * such that rangeMin() > target.rangeMax(). + */ + public void seekBeyond(RangeIterator target) { + it.seek(target.rangeMax().next()); + if (!it.done() && it.id().rangeMin().lessOrEquals(target.rangeMax())) { + it.next(); + } + refresh(); + } + + /** Updates internal state after the iterator has been repositioned. */ + private void refresh() { + if (it.done()) { + id = END; + clipped = null; + } else { + id = it.id(); + clipped = it.entry().clipped(0); + } + rangeMin = id.rangeMin(); + rangeMax = id.rangeMax(); + } + } + + /** + * FaceEdge stores temporary edge data while the index is being updated. FaceEdge represents an + * edge in the UV coordinates of a specific face, without any clipping. ClippedEdge, by + * comparison, contains the UV bound of the portion of the edge that intersects each cell. + * + *

          While it would be possible to combine all the edge information into ClippedEdge, there will + * be many clipped edges for each original face edge, and only the UV bound is different. Keeping + * the shared fields on this separate class provides two advantages: + * + *

            + *
          • Memory usage. Separating the two classes means that we only need to store one copy of the + * per-face data no matter how many times an edge is subdivided, and it also lets us delay + * computing bounding boxes until they are needed for processing each face (when the dataset + * spans multiple faces). + *
          • Performance. UpdateEdges is significantly faster on large polygons when the data is + * separated, because it often only needs to access the data in ClippedEdge and this data is + * cached more successfully. + *
          + */ + private static final class FaceEdge { + /** The shape that this edge belongs to. */ + private final int shapeId; + + /** Edge id within that shape. */ + private final int edgeId; + + /** Not desirable to subdivide this edge beyond this level. */ + private final int maxLevel; + + /** The edge endpoints, clipped to a given face. . */ + private final double ax; + + private final double ay; + private final double bx; + private final double by; + + /** + * The corresponding S2Points, cached here to avoid repeated calls to {@link + * S2Shape#getEdge(int, com.mogo.eagle.core.utilcode.geometry.S2Shape.MutableEdge)}. + */ + private final S2Point va; + + private final S2Point vb; + + private FaceEdge( + int shapeId, + int edgeId, + S2Point va, + S2Point vb, + R2Vector a, + R2Vector b, + double cellSizeToLongEdgeRatio) { + this.shapeId = shapeId; + this.edgeId = edgeId; + this.ax = a.x; + this.ay = a.y; + this.bx = b.x; + this.by = b.y; + this.va = va; + this.vb = vb; + this.maxLevel = getEdgeMaxLevel(va, vb, cellSizeToLongEdgeRatio); + } + + @Override + public String toString() { + return "shape " + shapeId + " edge " + edgeId; + } + } + + /** + * Returns the first level for which the given edge will be considered "long", i.e. it will not + * count towards the {@link Options#maxEdgesPerCell} limit. + */ + @VisibleForTesting + static final int getEdgeMaxLevel(S2Point va, S2Point vb, double cellSizeToLongEdgeRatio) { + // Compute the maximum cell edge length for which this edge is considered "long". The + // calculation does not need to be perfectly accurate, so avoid angle() since it's slower. + double maxCellEdge = va.getDistance(vb) * cellSizeToLongEdgeRatio; + // Now set the first level encountered during subdivision where the average cell edge length at + // that level is at most "cellSize". + return S2Projections.PROJ.avgEdge.getMinLevel(maxCellEdge); + } + + /** ClippedEdge represents the portion of a FaceEdge that has been clipped to an S2Cell. */ + private static class ClippedEdge { + /** + * The original unclipped edge. This field is not final so we can reuse ClippedEdge instances in + * an object pool, {@link EdgeAllocator}. + */ + private FaceEdge orig; + + /** Bounding box for the clipped portion. */ + private final R2Rect bound = new R2Rect(); + } + + /** + * This class provides temporary storage for new ClippedEdges that are created during indexing. It + * is essentially a stack-based object pool, where edges are allocated as the recursion goes down + * the first time, put back in the pool as recursion come back up, and reused when recursion goes + * back down another branch of the S2Cell tree. + */ + private static final class EdgeAllocator { + private int size; + private final List edges; + + public EdgeAllocator(int maxEdges) { + edges = createList(maxEdges); + } + + /** Returns an edge. */ + public ClippedEdge create() { + if (size == edges.size()) { + edges.add(new ClippedEdge()); + } + return edges.get(size++); + } + + /** + * Returns the number of allocated edges. Before a thread calls {@link #create()}, this method + * should be called to assess the size of the stack, and after all created edges are no longer + * needed, call {@link #reset(int)} with the previous size. + */ + public int size() { + return size; + } + + /** Returns all edges after 'size' to the object pool to be reused by another thread. */ + public void reset(int size) { + this.size = size; + } + } + + /** + * Given a set of shapes, InteriorTracker keeps track of which shapes contain a particular point + * (the "focus".) It provides an efficient way to move the focus from one point to another and + * incrementally update the set of shapes which contain it. We use this to compute which shapes + * contain the center of every S2CellId in the index, by advancing the focus from one cell center + * to the next. + * + *

          Initially the focus is S2.origin(), and therefore we can initialize the state of every shape + * to its containsOrigin() value. Next we advance the focus to the start of the S2CellId space- + * filling curve, by drawing a line segment between this point and S2.origin() and testing whether + * every edge of every shape intersects it. Then we visit all the cells that are being added to + * the S2ShapeIndex in increasing order of S2CellId. + * + *

          For each cell, we draw two edges: one from the entry vertex to the center, and another from + * the center to the exit vertex (where "entry" and "exit" refer to the points where the space- + * filling curve enters and exits the cell). By counting edge crossings we can incrementally + * compute which shapes contain the cell center. + * + *

          Note that the same set of shapes will always contain the exit point of one cell and the + * entry point of the next cell in the index, because either (a) these two points are actually the + * same, or (b) the intervening cells in S2CellId order are all empty, and therefore there are no + * edge crossings if we follow this path from one cell to the other. + */ + final class InteriorTracker { + /** Whether any shapes have an interior. */ + private boolean isActive; + + /** The prior focus point. */ + private S2Point oldFocus; + + /** The new focus point. */ + private S2Point newFocus; + + /** The last exit vertex. */ + private S2Point lastEnd; + + /** + * The ideal next cell ID such that the entry vertex of that cell would match the exit vertex of + * this cell. + */ + private S2CellId nextCellId; + + /** A temporary crosser from the old focus to the new focus. */ + private S2EdgeUtil.EdgeCrosser crosser; + + /** + * The set of shape ids (the indices of each shape in the S2ShapeIndex.shapes field) that + * contain the current focus. Sorted by ID in ascending order. + */ + private final int[] focusedShapes; + + /** The number of elements in 'focusedShapes' that are valid and in use. */ + private int focusCount; + + /** A temporary array in which to accumulate the clipped shapes for each cell. */ + private final S2ClippedShape[] tempClippedShapes; + + /** + * Initializes the InteriorTracker. You must call {@link #addShape(int, S2Shape)} for each shape + * that will be tracked before calling {@link #moveTo(S2Point)} or {@link #drawTo(S2Point)}. + */ + public InteriorTracker(int numShapes) { + this.tempClippedShapes = new S2ClippedShape[numShapes]; + this.focusedShapes = new int[numShapes]; + this.isActive = false; + this.nextCellId = S2CellId.begin(S2CellId.MAX_LEVEL); + // Draw from S2.origin() to the entry vertex of the very first cell. When shapes are added, + // they will be initialized based on whether they contain the origin, and the call to testEdge + // will then move the focus to the entry vertex of the first cell. + this.newFocus = S2.origin(); + drawTo(S2Point.normalize(S2Projections.faceUvToXyz(0, -1, -1))); // S2CellId curve start + } + + /** Returns true if {@link #addShape(int, S2Shape)} has been called at least once. */ + public boolean isActive() { + return isActive; + } + + /** + * Adds a shape whose interior should be tracked. This should be followed by calling {@link + * #testEdge(int, S2Point, S2Point)} with every edge of the given shape. + */ + public void addShape(int shapeId, S2Shape shape) { + // assert shapeId == shapes.indexOf(shape); + isActive = true; + if (shape.containsOrigin()) { + toggleShape(shapeId); + } + } + + /** + * Moves the focus to the given point. This method should only be used when it is known that + * there are no edge crossings between the old and new focus locations; otherwise use {@link + * #drawTo(S2Point)}. + */ + public void moveTo(S2Point b) { + newFocus = b; + } + + /** + * Moves the focus to the given point. After this method is called, {@link #testEdge(int, + * S2Point, S2Point)} should be called with all edges that may cross the line segment between + * the old and new focus locations. + */ + public void drawTo(S2Point focus) { + oldFocus = newFocus; + newFocus = focus; + crosser = new S2EdgeUtil.EdgeCrosser(oldFocus, newFocus); + lastEnd = null; + } + + /** + * Tests whether the given edge of the given shape may cross the line segment between the old + * and new focus locations (see {@link #drawTo(S2Point)}), and if there is a crossing the + * shape's containment of the focus is toggled. + */ + public void testEdge(int shapeId, S2Point start, S2Point end) { + // Just check that 'lastEnd' set by the last call is the new 'start', so comparison by + // reference is okay. + if (start != lastEnd) { + crosser.restartAt(start); + } + if (crosser.edgeOrVertexCrossing(end)) { + toggleShape(shapeId); + } + lastEnd = end; + } + + /** + * Indicates that the caller has finished processing the given S2CellId. By using this method + * together with {@link #atCellId(S2CellId)}, the caller can avoid calling {@link + * #moveTo(S2Point)} in cases where the exit vertex of the previous cell is the same as the + * entry vertex of the current cell. + */ + public void doneCellId(S2CellId cellid) { + nextCellId = cellid.rangeMax().next(); + } + + /** + * Returns true if the focus is already at the entry vertex of the given S2CellId (provided that + * the caller calls {@link #doneCellId(S2CellId)} as each cell is processed). + */ + public boolean atCellId(S2CellId cellid) { + return cellid.rangeMin().id() == nextCellId.id(); + } + + /** + * Toggles the given shape ID from the list of shapes that contain the current focus; if the + * shape was not in the set, add it; if it was in the set, remove it. + */ + private void toggleShape(int shapeId) { + // Since focusCount is typically *very* small (0, 1, or 2), it turns out to be significantly + // faster to maintain a sorted array rather than using a Set. + if (focusCount == 0) { + focusedShapes[0] = shapeId; + focusCount++; + } else if (focusedShapes[0] == shapeId) { + if (focusCount-- > 1) { + System.arraycopy(focusedShapes, 1, focusedShapes, 0, focusCount); + } + } else { + int pos = 0; + while (focusedShapes[pos] < shapeId) { + if (++pos == focusCount) { + focusedShapes[focusCount++] = shapeId; + return; + } + } + if (focusedShapes[pos] == shapeId) { + focusCount--; + System.arraycopy(focusedShapes, pos + 1, focusedShapes, pos, focusCount - pos); + } else { + System.arraycopy(focusedShapes, pos, focusedShapes, pos + 1, focusCount - pos); + focusedShapes[pos] = shapeId; + focusCount++; + } + } + } + } + + /** + * Creates a new list, using a SimpleList when the predicted maximum size is small, and a sharded + * list when the predicted size is large enough to be worth it. + */ + static final List createList(int maxSize) { + if (maxSize < 256) { + return new SimpleList(maxSize); + } else { + return new ShardedList(maxSize); + } + } + + /** A simple append-only RandomAccess List similar to (but about 10% faster than) ArrayList. */ + private static final class SimpleList extends AbstractList + implements RandomAccess, Serializable { + private static final long serialVersionUID = 1L; + + private Object[] elements; + private int size; + + public SimpleList(int maxSize) { + elements = new Object[Math.max(1, maxSize)]; + } + + @Override + public T get(int index) { + @SuppressWarnings("unchecked") + T result = (T) elements[index]; + return result; + } + + @Override + public T set(int index, T value) { + @SuppressWarnings("unchecked") + T old = (T) elements[index]; + elements[index] = value; + return old; + } + + @Override + public int size() { + return size; + } + + @Override + public boolean add(T item) { + if (size == elements.length) { + elements = Arrays.copyOf(elements, size * 2); + } + elements[size++] = item; + return true; + } + } + + /** + * A more complex append-only RandomAccess List that allocates space in shards of 256 elements + * each, avoiding reallocation as the list grows, and avoiding single allocations larger than 2KB. + * This is about 1% faster than SimpleList with reserveEdges in use, but when the heap is anywhere + * near full, this approach is dramatically faster than any technique requiring a large contiguous + * allocation, since it will probably require a GC to supply one. + */ + private static final class ShardedList extends AbstractList + implements RandomAccess, Serializable { + private static final long serialVersionUID = 1L; + + private Object[][] elements; + private int size; + + public ShardedList(int maxItems) { + // Don't allocate the inner list until items are actually added. + elements = new Object[1 + (maxItems >> 8)][]; + } + + @Override + public int size() { + return size; + } + + @Override + public boolean add(T item) { + int shard = size >> 8; + if (shard == elements.length) { + elements = Arrays.copyOf(elements, shard * 2); + elements[shard] = new Object[256]; + } else if (elements[shard] == null) { + elements[shard] = new Object[256]; + } + elements[shard][size & 0xFF] = item; + size++; + return true; + } + + @Override + public T get(int index) { + @SuppressWarnings("unchecked") + T result = (T) elements[index >> 8][index & 0xFF]; + return result; + } + + @Override + public T set(int index, T value) { + @SuppressWarnings("unchecked") + T result = (T) elements[index]; + elements[index >> 8][index & 0xFF] = value; + return result; + } + } +} diff --git a/core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/S2ShapeIndexCoder.java b/core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/S2ShapeIndexCoder.java new file mode 100644 index 0000000000..9e7a78da93 --- /dev/null +++ b/core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/S2ShapeIndexCoder.java @@ -0,0 +1,532 @@ +/* + * Copyright 2018 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.mogo.eagle.core.utilcode.geometry; + +import com.google.common.annotations.GwtIncompatible; +import com.google.common.base.Preconditions; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Multimap; +import com.mogo.eagle.core.utilcode.geometry.PrimitiveArrays.Bytes; +import com.mogo.eagle.core.utilcode.geometry.PrimitiveArrays.Cursor; +import com.mogo.eagle.core.utilcode.geometry.S2ShapeIndex.Cell; +import com.mogo.eagle.core.utilcode.geometry.S2ShapeIndex.S2ClippedShape; +import com.mogo.eagle.core.utilcode.geometry.S2ShapeUtil.S2EdgeVectorShape; +import com.google.common.primitives.Ints; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.util.AbstractList; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import javax.annotation.Nullable; + +/** + * An encoder/decoder of {@link S2ShapeIndex}s. + * + *

          Values from the {@link S2ShapeIndex} returned by {@link #decode(Bytes, Cursor)} are decoded + * only when they are accessed. This allows for very fast initialization and no additional memory + * use beyond the encoded data, and a cache of the clipped shapes that have been accessed. When + * accessing the entire index, this uses slightly more memory than {@link S2ShapeIndex}, but uses + * dramatically less memory when accessing only a few cells of the index. + */ +@GwtIncompatible("S2LaxPolylineShape and S2LaxPolygonShape") +public class S2ShapeIndexCoder implements S2Coder { + + /** + * An instance of a {@code S2ShapeIndexCoder} which can encode an {@link S2ShapeIndex} but will + * throw an {@link IllegalArgumentException} if used to decode an {@link S2ShapeIndex}. + */ + public static final S2ShapeIndexCoder INSTANCE = new S2ShapeIndexCoder(null); + + private final List shapes; + + /** + * Constructs a {@code S2ShapeIndexCoder}. + * + * @param shapes the list of shapes, used only by {@link #decode}, commonly the result of {@link + * VectorCoder#FAST_SHAPE#decode(Bytes, Cursor)}. + */ + public S2ShapeIndexCoder(@Nullable List shapes) { + this.shapes = shapes; + } + + @Override + public void encode(S2ShapeIndex value, OutputStream output) throws IOException { + // The version number is encoded in 2 bits, under the assumption that by the time we need 5 + // versions the first version can be permanently retired. This only saves 1 byte, but that's + // significant for very small indexes. + long maxEdges = value.options().getMaxEdgesPerCell(); + EncodedInts.writeVarint64(output, maxEdges << 2 | EncodedS2ShapeIndex.CURRENT_ENCODING_VERSION); + + List cellIds = new ArrayList<>(); + List encodedCells = new ArrayList<>(); + Multimap shapeIds = S2ShapeUtil.shapeToShapeId(value); + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + for (S2Iterator it = value.iterator(); !it.done(); it.next()) { + cellIds.add(it.id()); + encodeCell(it.entry(), shapeIds, baos); + encodedCells.add(baos.toByteArray()); + baos.reset(); + } + S2CellIdVectorCoder.INSTANCE.encode(cellIds, output); + VectorCoder.BYTE_ARRAY.encode(encodedCells, output); + } + + @Override + public S2ShapeIndex decode(Bytes data, Cursor cursor) { + Preconditions.checkNotNull(shapes); + return new EncodedS2ShapeIndex(data, cursor, shapes); + } + + /** + * Represents an encoded {@link S2ShapeIndex}. + * + *

          This class is thread-safe. + */ + private static final class EncodedS2ShapeIndex extends S2ShapeIndex { + /** + * Internal representation of an undecoded shape, which must be distinguished from a null shape. + */ + private static final S2Shape UNDECODED_SHAPE = new S2EdgeVectorShape(); + + /** The decoded options of this index. */ + private final Options options; + + /** + * The array of not-yet-decoded and decoded shapes. The default value is {@link + * #UNDECODED_SHAPE}. A value of {@code null} represents a null shape. + */ + private final S2Shape[] cachedShapes; + + /** The encoded vector of cell IDs of this index. */ + private final S2CellIdVector encodedCellIds; + + /** The encoded cells of this index. */ + private final List encodedCells; + + /** The list of {@link Cell}s. */ + private final List decodedCells; + + /** A coder of {@code S2ClippedShape[]}s. */ + private final S2Coder clippedShapeArrayCoder = + new S2Coder() { + @Override + public void encode(S2ClippedShape[] values, OutputStream output) { + throw new UnsupportedOperationException(); + } + + @Override + public S2ClippedShape[] decode(Bytes data, Cursor cursor) { + return decodeClippedShapes(shapes, data, cursor); + } + }; + + /** + * Initializes an {@link EncodedS2ShapeIndex} backed by {@code data} at {@code offset}. + * + *

          Values are decoded only when they are accessed. This allows for very fast initialization + * and little additional memory use beyond the encoded data. + */ + EncodedS2ShapeIndex(Bytes data, Cursor cursor, List shapeFactory) { + long maxEdgesVersion = data.readVarint64(cursor); + int version = (int) maxEdgesVersion & 3; + Preconditions.checkArgument( + version == S2ShapeIndex.CURRENT_ENCODING_VERSION, "Unknown encoding."); + options = new Options(); + options.setMaxEdgesPerCell(Ints.checkedCast(maxEdgesVersion >> 2)); + cachedShapes = new S2Shape[Ints.checkedCast(shapeFactory.size())]; + Arrays.fill(cachedShapes, UNDECODED_SHAPE); + shapes = + new AbstractList() { + @Override + public synchronized S2Shape get(int i) { + return cachedShapes[i] == UNDECODED_SHAPE + ? cachedShapes[i] = shapeFactory.get(i) + : cachedShapes[i]; + } + + @Override + public int size() { + return cachedShapes.length; + } + }; + encodedCellIds = S2CellIdVectorCoder.INSTANCE.decode(data, cursor); + encodedCells = new VectorCoder<>(clippedShapeArrayCoder).decode(data, cursor); + ImmutableList.Builder cellsBuilder = ImmutableList.builder(); + for (int i = 0; i < encodedCellIds.size(); i++) { + cellsBuilder.add(new LazyCell(i)); + } + decodedCells = cellsBuilder.build(); + } + + @Override + public Options options() { + return options; + } + + @Override + public void add(S2Shape shape) { + throw new UnsupportedOperationException(); + } + + @Override + public void remove(S2Shape shape) { + throw new UnsupportedOperationException(); + } + + @Override + public void reset() { + throw new UnsupportedOperationException(); + } + + @Override + public S2Iterator iterator() { + return S2Iterator.create(decodedCells, encodedCellIds::lowerBound); + } + + @Override + public boolean isFresh() { + return true; + } + + @Override + void applyUpdates() { + throw new UnsupportedOperationException(); + } + + /** A lazy implementation of {@link Cell} which decodes members on demand. */ + private final class LazyCell extends Cell { + + /** The index of this cell. */ + private final int i; + + private S2CellId cachedCellId = null; + private volatile S2ClippedShape[] cachedClippedShapes; + + LazyCell(int i) { + this.i = i; + } + + /** + * Returns {@link #cachedClippedShapes} if it's already cached. Otherwise, loads the clipped + * shapes from {@link #encodedCells} and stores it in {@link #cachedClippedShapes}. + */ + private S2ClippedShape[] loadClippedShapesFromCache() { + if (cachedClippedShapes == null) { + cachedClippedShapes = encodedCells.get(i); + } + return cachedClippedShapes; + } + + @Override + public long id() { + synchronized (EncodedS2ShapeIndex.this) { + if (cachedCellId == null) { + cachedCellId = encodedCellIds.get(i); + } + return cachedCellId.id(); + } + } + + @Override + public int numShapes() { + return loadClippedShapesFromCache().length; + } + + @Override + public S2ClippedShape clipped(int i) { + return loadClippedShapesFromCache()[i]; + } + } + } + + private static void encodeCell( + Cell cell, Multimap shapeIds, OutputStream output) throws IOException { + // The encoding is designed to be especially compact in certain common situations: + // + // 1. The S2ShapeIndex contains exactly one shape. + // + // 2. The S2ShapeIndex contains more than one shape, but a particular index cell contains only + // one shape (numShapes() == 1). + // + // 3. The edge ids for a given shape in a cell form a contiguous range. + // + // The details were optimized by constructing an S2ShapeIndex for each feature in Google's + // geographic repository and measuring their total encoded size. The MutableS2ShapeIndex + // encoding (of which this function is just one part) uses an average of 1.88 bytes per vertex + // for features consisting of polygons or polylines. + // + // Note that this code does not bother handling numShapes() >= 2**28 or numEdges >= 2**29. + // This could be fixed using varint64 in a few more places, but if a single cell contains this + // many shapes or edges then we have bigger problems than just the encoding format :) + Preconditions.checkArgument(cell.numShapes() < (1 << 28), "Too many shapes."); + + if (shapeIds.size() == 1) { + S2ClippedShape clipped = cell.clipped(0); + int n = clipped.numEdges(); + Preconditions.checkArgument(n < (1 << 29), "Too many edges."); + + int containsCenter = clipped.containsCenter() ? 1 : 0; + if (n >= 2 && n <= 17 && clipped.edge(n - 1) - clipped.edge(0) == n - 1) { + // The cell contains a contiguous range of edges (*most common case*). + // If the starting edge id is small then we can encode the cell in one byte. (The n == 0 + // and n == 1 cases are encoded compactly below). This encoding uses a 1-bit tag because + // it is by far the most common. + // + // Encoding: bit 0: 0 + // bit 1: containsCenter + // bits 2-5: (numEdges - 2) + // bits 6+: edgeId + EncodedInts.writeVarint64( + output, clipped.edge(0) << 6 | (n - 2) << 2 | containsCenter << 1); + } else if (n == 1) { + // The cell contains only one edge. For edge ids up to 15, we can encode the cell in a + // single byte. + // + // Encoding: bits 0-1: 1 + // bit 2: containsCenter + // bits 3+: edgeId + EncodedInts.writeVarint64(output, clipped.edge(0) << 3 | containsCenter << 2 | 1); + } else { + // General case (including n == 0, which is encoded compactly here). + // + // Encoding: bits 0-1: 3 + // bit 2: containsCenter + // bits 3+: numEdges + EncodedInts.writeVarint64(output, n << 3 | containsCenter << 2 | 3); + encodeEdges(clipped, output); + } + } else { + // Note that there are exactly two possible values for the first encoded tag: + // 1. numShapes() > 1: a 3-bit tag with value 3. + // 2. numShapes() == 0: a 3-bit tag with value 7. + if (cell.numShapes() > 1) { + // The cell contains more than one shape. The tag for this encoding must be + // distinguishable from the cases encoded below. We can afford to use a 3-bit tag because + // numShapes() is generally small. + EncodedInts.writeVarint64(output, (cell.numShapes() << 3) | 3); + } + // The shape ids are delta-encoded. + int shapeIdBase = 0; + for (int i = 0; i < cell.numShapes(); i++) { + S2ClippedShape clipped = cell.clipped(i); + int containsCenter = clipped.containsCenter() ? 1 : 0; + int clippedShapeId = -1; + Iterable clippedShapeIds = shapeIds.get(clipped.shape()); + for (int id : clippedShapeIds) { + if (id >= shapeIdBase) { + clippedShapeId = id; + break; + } + } + assert clippedShapeId >= shapeIdBase; + int shapeDelta = clippedShapeId - shapeIdBase; + shapeIdBase = clippedShapeId + 1; + + // Like the code above except that we also need to encode shapeId(s). + // Because of this some choices are slightly different. + int n = clipped.numEdges(); + Preconditions.checkArgument(n < (1 << 29), "Too many edges."); + + if (n >= 1 && n <= 16 && clipped.edge(n - 1) - clipped.edge(0) == n - 1) { + // The clipped shape has a contiguous range of up to 16 edges. This encoding uses a + // 1-bit tag because it is by far the most common. + // + // Encoding: bit 0: 0 + // bit 1: containsCenter + // bits 2+: edgeId + // Next value: bits 0-3: (numEdges - 1) + // bits 4+: shapeDelta + EncodedInts.writeVarint64(output, (clipped.edge(0) << 2) | (containsCenter << 1)); + EncodedInts.writeVarint64(output, (shapeDelta << 4) | (n - 1)); + } else if (n == 0) { + // Special encoding for clipped shapes with no edges. Such shapes are common in polygon + // interiors. This encoding uses a 3-bit tag in order to leave more bits available for + // the other encodings. + // + // NOTE(user): When numShapes() > 1, this tag could be 2 bits (because the tag used to + // indicate numShapes() > 1 can't appear). Alternatively, that tag can be considered + // reserved for future use. + // + // Encoding: bits 0-2: 7 + // bit 3: containsCenter + // bits 4+: shapeDelta + EncodedInts.writeVarint64(output, (shapeDelta << 4) | (containsCenter << 3) | 7); + } else { + // General case. This encoding uses a 2-bit tag, and the first value typically is + // encoded into one byte. + // + // Encoding: bits 0-1: 1 + // bit 2: containsCenter + // bits 3+: (numEdges - 1) + // Next value: shapeDelta + EncodedInts.writeVarint64(output, ((n - 1) << 3) | (containsCenter << 2) | 1); + EncodedInts.writeVarint64(output, shapeDelta); + encodeEdges(clipped, output); + } + } + } + } + + /** Encodes the edge IDs of the given {@link S2ClippedShape}. */ + private static void encodeEdges(S2ClippedShape clipped, OutputStream output) throws IOException { + // Each entry is an (edgeId, count) pair representing a contiguous range of edges. The edge + // ids are delta-encoded such that 0 represents the minimum valid next edge id. + // + // Encoding: if bits 0-2 < 7: encodes (count - 1) + // - bits 3+: edge delta + // if bits 0-2 == 7: + // - bits 3+ encode (count - 8) + // - Next value is edge delta + // + // No count is encoded for the last edge (saving 3 bits). + int edgeIdBase = 0; + int numEdges = clipped.numEdges(); + for (int i = 0; i < numEdges; i++) { + int edgeId = clipped.edge(i); + assert edgeId >= edgeIdBase; + int delta = edgeId - edgeIdBase; + if (i + 1 == numEdges) { + // This is the last edge; no need to encode an edge count. + EncodedInts.writeVarint64(output, delta); + } else { + // Count the edges in this contiguous range. + int count = 1; + for (; i + 1 < numEdges && clipped.edge(i + 1) == edgeId + count; i++) { + count++; + } + if (count < 8) { + // Count is encoded in low 3 bits of delta. + EncodedInts.writeVarint64(output, delta << 3 | (count - 1)); + } else { + // Count and delta are encoded separately. + EncodedInts.writeVarint64(output, (count - 8) << 3 | 7); + EncodedInts.writeVarint64(output, delta); + } + edgeIdBase = edgeId + count; + } + } + } + + /** Decodes {@code numEdges} edge IDs of a {@link S2ClippedShape}. */ + private static int[] decodeEdges(int numEdges, Bytes data, Cursor cursor) { + // This function inverts the encodings documented above. + int[] edges = new int[numEdges]; + int edgeId = 0; + for (int i = 0; i < numEdges; ) { + long delta = data.readVarint64(cursor); + if (i + 1 == numEdges) { + // The last edge is encoded without an edge count. + edges[i++] = Ints.checkedCast(edgeId + delta); + } else { + // Otherwise decode the count and edge delta. + long count = (delta & 7) + 1; + delta >>>= 3; + if (count == 8) { + count = delta + 8; + delta = data.readVarint64(cursor); + } + edgeId += Ints.checkedCast(delta); + for (; count > 0; count--, i++, edgeId++) { + edges[i] = edgeId; + } + } + } + return edges; + } + + /** + * Decodes an array of {@link S2ClippedShape} from {@code input} from the given {@code shapes}. + * The {@link S2ClippedShape} at index 0 will store {@code cellId}. + */ + private static S2ClippedShape[] decodeClippedShapes( + List shapes, Bytes data, Cursor cursor) { + // This function inverts the encodings documented above. + if (shapes.size() == 1) { + S2ClippedShape[] clippedShapes = new S2ClippedShape[1]; + + // Entire S2ShapeIndex contains only one shape. + long header = data.readVarint64(cursor); + if ((header & 1) == 0) { + // The cell contains a contiguous range of edges. + int numEdges = Ints.checkedCast(((header >>> 2) & 15) + 2); + clippedShapes[0] = + S2ClippedShape.create( + null, shapes.get(0), (header & 2) != 0, Ints.checkedCast(header >>> 6), numEdges); + } else if ((header & 2) == 0) { + // The cell contains a single edge. + clippedShapes[0] = + S2ClippedShape.create( + null, shapes.get(0), (header & 4) != 0, Ints.checkedCast(header >>> 3), 1); + } else { + // The cell contains some other combination of edges. + int numEdges = Ints.checkedCast(header >> 3); + int[] edges = decodeEdges(numEdges, data, cursor); + clippedShapes[0] = S2ClippedShape.create(null, shapes.get(0), (header & 4) != 0, edges); + } + return clippedShapes; + } + + // S2ShapeIndex contains more than one shape. + long header = data.readVarint64(cursor); + int numClipped = 1; + if ((header & 7) == 3) { + // This cell contains more than one shape. + numClipped = Ints.checkedCast(header >>> 3); + header = data.readVarint64(cursor); + } + + S2ClippedShape[] clippedShapes = new S2ClippedShape[numClipped]; + + long shapeId = 0; + for (int j = 0; j < numClipped; j++, shapeId++) { + if (j > 0) { + header = data.readVarint64(cursor); + } + if ((header & 1) == 0) { + // The clipped shape contains a contiguous range of edges. + long shapeIdCount = data.readVarint64(cursor); + shapeId += shapeIdCount >> 4; + int numEdges = Ints.checkedCast((shapeIdCount & 15) + 1); + clippedShapes[j] = + S2ClippedShape.create( + null, + shapes.get(Ints.checkedCast(shapeId)), + (header & 2) != 0, + Ints.checkedCast(header >>> 2), + numEdges); + } else if ((header & 7) == 7) { + // The clipped shape has no edges. + shapeId += header >> 4; + clippedShapes[j] = + S2ClippedShape.create( + null, shapes.get(Ints.checkedCast(shapeId)), (header & 8) != 0, 0, 0); + } else { + // The clipped shape contains some other combination of edges. + assert (header & 3) == 1; + long shapeDelta = data.readVarint64(cursor); + shapeId += shapeDelta; + int numEdges = Ints.checkedCast((header >>> 3) + 1); + int[] edges = decodeEdges(numEdges, data, cursor); + clippedShapes[j] = + S2ClippedShape.create( + null, shapes.get(Ints.checkedCast(shapeId)), (header & 4) != 0, edges); + } + } + return clippedShapes; + } +} diff --git a/core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/S2ShapeIndexMeasures.java b/core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/S2ShapeIndexMeasures.java new file mode 100644 index 0000000000..42e886875b --- /dev/null +++ b/core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/S2ShapeIndexMeasures.java @@ -0,0 +1,117 @@ +/* + * Copyright 2019 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.mogo.eagle.core.utilcode.geometry; + +import com.google.common.annotations.GwtCompatible; + +/** + * Defines various angle and area measures for {@link S2ShapeIndex} objects. In general, these + * methods return the sum of the corresponding measure for all {@link S2Shape} in the index. + */ +@GwtCompatible +public final class S2ShapeIndexMeasures { + + private S2ShapeIndexMeasures() {} + + /** + * Returns the maximum dimension of any shape in shapeIndex, or -1 if shapeIndex has no shapes. + * + *

          The dimension does not depend on whether the shapes in shapeIndex contain any points. + * For example, the dimension of an empty point set is 0, and the dimension of an empty polygon is + * 2. + */ + public static int dimension(S2ShapeIndex shapeIndex) { + int dimension = -1; + for (S2Shape shape : shapeIndex.getShapes()) { + dimension = Math.max(dimension, shape.dimension()); + } + return dimension; + } + + /** + * Returns the total length of all polylines in shapeIndex, or {@link S1Angle#ZERO} if shapeIndex + * contains no polylines. + */ + public static S1Angle length(S2ShapeIndex shapeIndex) { + S1Angle.Builder builder = new S1Angle.Builder(); + for (S2Shape shape : shapeIndex.getShapes()) { + builder.add(S2ShapeMeasures.length(shape)); + } + return builder.build(); + } + + /** + * Returns the total perimeter of all polygons in shapeIndex (including both "shells" and + * "holes"), or {@link S1Angle#ZERO} shapeIndex contains no polygons. + */ + public static S1Angle perimeter(S2ShapeIndex shapeIndex) { + S1Angle.Builder builder = new S1Angle.Builder(); + for (S2Shape shape : shapeIndex.getShapes()) { + builder.add(S2ShapeMeasures.perimeter(shape)); + } + return builder.build(); + } + + /** + * Returns the total area of all polygons in shapeIndex. Returns 0 if no polygons are present. + * This method has good relative accuracy for both very large and very small regions. Note that + * the result may exceed 4*Pi if shapeIndex contains overlapping polygons. + */ + public static double area(S2ShapeIndex shapeIndex) { + double area = 0; + for (S2Shape shape : shapeIndex.getShapes()) { + area += S2ShapeMeasures.area(shape); + } + return area; + } + + /** + * Returns the centroid of all shapes whose dimension is maximal within shapeIndex, multiplied by + * the measure of those shapes. For example, if shapeIndex contains points and polylines, then the + * result is defined as the centroid of the polylines multiplied by the total length of those + * polylines. The points would be ignored when computing the centroid. + * + *

          The measure of a given shape is defined as follows: + * + *

            + *
          • For dimension 0 shapes, the measure is {@link S2Shape#numEdges()}. + *
          • For dimension 1 shapes, the measure is {@link #length(S2ShapeIndex)}. + *
          • For dimension 2 shapes, the measure is {@link #area(S2ShapeIndex)}. + *
          + * + *

          The returned centroid is not unit length, so {@link S2Point#normalize()} may need to be + * called before passing it to other S2 functions. (0, 0, 0) is returned if the index contains no + * geometry. + * + *

          The centroid is scaled by the total measure of the shapes for two reasons: + * + *

            + *
          1. It is cheaper to compute this way. + *
          2. This makes it easier to compute the centroid of a collection of shapes (since the + * individual centroids can simply be summed) + *
          + */ + public static S2Point centroid(S2ShapeIndex shapeIndex) { + int dimension = dimension(shapeIndex); + S2Point.Builder builder = new S2Point.Builder(); + for (S2Shape shape : shapeIndex.getShapes()) { + if (shape.dimension() == dimension) { + builder.add(S2ShapeMeasures.centroid(shape)); + } + } + return builder.build(); + } +} diff --git a/core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/S2ShapeIndexRegion.java b/core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/S2ShapeIndexRegion.java new file mode 100644 index 0000000000..8f9b3dd0f5 --- /dev/null +++ b/core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/S2ShapeIndexRegion.java @@ -0,0 +1,299 @@ +/* + * Copyright 2018 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.mogo.eagle.core.utilcode.geometry; + +import com.google.common.annotations.GwtCompatible; +import com.google.common.base.Preconditions; +import com.mogo.eagle.core.utilcode.geometry.S2ContainsPointQuery.S2VertexModel; +import com.mogo.eagle.core.utilcode.geometry.S2Shape.MutableEdge; +import com.mogo.eagle.core.utilcode.geometry.S2ShapeIndex.S2ClippedShape; +import java.util.List; + +/** + * This class wraps an S2ShapeIndex object with the additional methods needed to implement the + * S2Region API, in order to allow S2RegionCoverer to compute S2CellId coverings of arbitrary + * collections of geometry. + * + *

          Example usage: + * + *

          + * S2CellUnion getCovering(S2ShapeIndex index) {
          + *   S2RegionCoverer coverer = new S2RegionCoverer();
          + *   coverer.setMaxCells(20);
          + *   return coverer.getCovering(new S2ShapeIndexRegion(index));
          + * }
          + * 
          + * + *

          This class uses a number of temporary mutable objects to keep allocation down, and so is not + * thread-safe. To use it in parallel, each thread should construct its own instance (this is not + * expensive). + */ +@GwtCompatible(serializable = false) +public class S2ShapeIndexRegion implements S2Region { + /** The vertex model for contains(S2Point) tests. */ + private final S2VertexModel model; + + /** The iterator. */ + private final S2Iterator it; + + /** Temporary cell union for internal usage. */ + private final S2CellUnion union = new S2CellUnion(); + + /** Temporary edge for internal usage. */ + private final MutableEdge edge = new MutableEdge(); + + /** Temporary bound for internal usage. */ + private final R2Rect bound = new R2Rect(); + + /** Temporary R2 point for internal usage. */ + private final R2Vector p0 = new R2Vector(); + + /** Temporary R2 point for internal usage. */ + private final R2Vector p1 = new R2Vector(); + + /** + * Creates a new region with the given index, and a {@link S2VertexModel#SEMI_OPEN semi-open} + * vertex model. + */ + public S2ShapeIndexRegion(S2ShapeIndex index) { + this(index, S2VertexModel.SEMI_OPEN); + } + + /** Creates a new region with the given index, and a given {@link S2VertexModel}. */ + public S2ShapeIndexRegion(S2ShapeIndex index, S2VertexModel model) { + this.it = index.iterator(); + this.model = model; + } + + @Override + public S2Cap getCapBound() { + getCellUnionBound(union.cellIds()); + return union.getCapBound(); + } + + @Override + public S2LatLngRect getRectBound() { + getCellUnionBound(union.cellIds()); + return union.getRectBound(); + } + + /** + * Clears the given list of cells and adds the cell union of this index. An index of shapes in one + * face adds up to 4 cells, otherwise up to 6 may be added. + */ + public void getCellUnionBound(List cellIds) { + // We find the range of S2Cells spanned by the index and choose a level such that the entire + // index can be covered with just a few cells. There are two cases: + // + // - If the index intersects two or more faces, then for each intersected face we add one cell + // to the covering. Rather than adding the entire face, instead we add the smallest S2Cell + // that covers the S2ShapeIndex cells within that face. + // + // - If the index intersects only one face, then we first find the smallest cell S that contains + // the index cells (just like the case above). However rather than using the cell S itself, + // instead we repeat this process for each of its child cells. In other words, for each child + // cell C we add the smallest S2Cell C' that covers the index cells within C. This extra step + // is relatively cheap and produces much tighter coverings when the S2ShapeIndex consists of a + // small region near the center of a large S2Cell. + // + // The following code uses only a single S2Iterator object because creating an S2Iterator may be + // relatively expensive for S2ShapeIndex instances (e.g. it may involve substantial memory + // allocation to build a lazily-assembled index). + cellIds.clear(); + + // Find the last S2CellId in the index. + it.finish(); + if (it.atBegin()) { + // Empty index. + return; + } + it.prev(); + S2CellId lastIndexId = it.id(); + it.restart(); + S2CellId currentIndexId = it.id(); + if (!currentIndexId.equals(lastIndexId)) { + // The index has at least two cells. Choose an S2CellId level such that the entire index can + // be spanned with at most 6 cells (if the index spans multiple faces) or 4 cells (if the + // index spans a single face). + int level = currentIndexId.getCommonAncestorLevel(lastIndexId) + 1; + + // For each cell C at the chosen level, we compute the smallest S2Cell that covers the + // S2ShapeIndex cells within C. + S2CellId lastId = lastIndexId.parent(level); + for (S2CellId id = currentIndexId.parent(level); !id.equals(lastId); id = id.next()) { + // If the cell C does not contain any index cells, then skip it. + S2CellId max = id.rangeMax(); + if (max.lessThan(currentIndexId)) { + continue; + } + + // Find the range of index cells contained by C and then shrink C so that it just covers + // those cells. + it.seek(max.next()); + it.prev(); + coverRange(currentIndexId, it.id(), cellIds); + it.next(); + currentIndexId = it.id(); + } + } + coverRange(currentIndexId, lastIndexId, cellIds); + } + + /** + * Computes the smallest S2Cell that covers the S2Cell range (first, last) and adds this cell to + * "cellIds". + * + * @throws IllegalArgumentException "first" and "last" don't have a common ancestor. + */ + private static void coverRange(S2CellId first, S2CellId last, List cellIds) { + if (first.equals(last)) { + // The range consists of a single index cell. + cellIds.add(first); + } else { + // Add the lowest common ancestor of the given range. + int level = first.getCommonAncestorLevel(last); + Preconditions.checkArgument(level >= 0, "First and last must have a common ancestor."); + cellIds.add(first.parent(level)); + } + } + + /** + * Returns true if the given point is contained by any two-dimensional shape (i.e., polygon). Zero + * and one-dimensional shapes are ignored by this method. + */ + @Override + public boolean contains(S2Point p) { + if (it.locate(p)) { + S2Point center = it.center(); + S2ShapeIndex.Cell cell = it.entry(); + for (int s = 0; s < cell.numShapes(); ++s) { + if (model.shapeContains(center, cell.clipped(s), p)) { + return true; + } + } + } + return false; + } + + /** + * Returns true if 'target' is contained by any single shape. If the cell is covered by a union of + * different shapes then it may return false. + * + *

          This implementation is conservative but not exact; if a shape just barely contains the given + * cell then it may return false. The maximum error is less than 10 * DBL_EPSILON radians (or + * about 15 nanometers). + */ + @Override + public boolean contains(S2Cell target) { + S2ShapeIndex.CellRelation relation = it.locate(target.id()); + + // If the relation is DISJOINT, then "target" is not contained. Similarly if the relation is + // SUBDIVIDED then "target" is not contained, since index cells are subdivided only if they + // (nearly) intersect too many edges. + if (relation != S2ShapeIndex.CellRelation.INDEXED) { + return false; + } + + // Otherwise, the iterator points to an index cell containing "target". If any shape contains + // the target cell, we return true. + // assert (it.id().contains(target.id())); + S2ShapeIndex.Cell cell = it.entry(); + S2Point center = it.center(); + for (int s = 0; s < cell.numShapes(); ++s) { + S2ClippedShape clipped = cell.clipped(s); + // The shape contains the target cell iff the shape contains the cell center and none of its + // edges intersects the (padded) cell interior. + if (it.id().equals(target.id())) { + if (clipped.numEdges() == 0 && clipped.containsCenter()) { + return true; + } + } else { + // It is faster to call AnyEdgeIntersects() before Contains(). + if (clipped.shape().hasInterior() + && !anyEdgeIntersects(clipped, target) + && model.shapeContains(center, clipped, target.getCenter())) { + return true; + } + } + } + return false; + } + + /** + * Returns true if any shape intersects "target". + * + *

          This implementation is conservative but not exact; if a shape is just barely disjoint from + * the given cell then it may return true. The maximum error is less than 10 * DBL_EPSILON radians + * (or about 15 nanometers). + */ + @Override + public boolean mayIntersect(S2Cell target) { + S2ShapeIndex.CellRelation relation = it.locate(target.id()); + + // If "target" does not overlap any index cell, there is no intersection. + if (relation == S2ShapeIndex.CellRelation.DISJOINT) { + return false; + } + + // If "target" is subdivided into one or more index cells, then there is an intersection to + // within the S2ShapeIndex error bound. + if (relation == S2ShapeIndex.CellRelation.SUBDIVIDED) { + return true; + } + + // Otherwise, the iterator points to an index cell containing "target". If "target" is an index + // cell itself, there is an intersection because index cells are created only if they have at + // least one edge or they are entirely contained by the loop. + // assert (it.id().contains(target.id())); + if (it.compareTo(target.id()) == 0) { + return true; + } + + // Test whether any shape intersects the target cell or contains its center. + S2ShapeIndex.Cell cell = it.entry(); + S2Point center = it.center(); + for (int s = 0; s < cell.numShapes(); ++s) { + S2ClippedShape clipped = cell.clipped(s); + if (anyEdgeIntersects(clipped, target) + || model.shapeContains(center, clipped, target.getCenter())) { + return true; + } + } + return false; + } + + /** + * Returns true if any edge of the indexed shape "clipped" intersects the cell "target". It may + * also return true if an edge is very close to "target"; the maximum error is less than 10 * + * DBL_EPSILON radians (about 15 nanometers). + */ + private boolean anyEdgeIntersects(S2ClippedShape clipped, S2Cell target) { + target.setBoundUV(bound); + bound.expand(S2EdgeUtil.MAX_CELL_EDGE_ERROR); + int face = target.face(); + S2Shape shape = clipped.shape(); + int numEdges = clipped.numEdges(); + for (int i = 0; i < numEdges; ++i) { + shape.getEdge(clipped.edge(i), edge); + if (S2EdgeUtil.clipToPaddedFace(edge.a, edge.b, face, S2EdgeUtil.MAX_CELL_EDGE_ERROR, p0, p1) + && S2EdgeUtil.intersectsRect(p0, p1, bound)) { + return true; + } + } + return false; + } +} diff --git a/core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/S2ShapeMeasures.java b/core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/S2ShapeMeasures.java new file mode 100644 index 0000000000..a13b2657a8 --- /dev/null +++ b/core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/S2ShapeMeasures.java @@ -0,0 +1,504 @@ +/* + * Copyright 2019 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.mogo.eagle.core.utilcode.geometry; + +import com.google.common.annotations.GwtCompatible; +import com.google.common.annotations.VisibleForTesting; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Iterables; +import com.google.common.collect.Lists; +import com.mogo.eagle.core.utilcode.geometry.S2ShapeUtil.CentroidMeasure; +import java.util.AbstractList; +import java.util.Comparator; +import java.util.List; + +/** + * Defines various angle and area measures for {@link S2Shape} objects. Unlike the built-in {@link + * S2Polygon} and {@link S2Polyline} methods, these methods allow the underlying data to be + * represented arbitrarily. + */ +@GwtCompatible +final strictfp class S2ShapeMeasures { + + private S2ShapeMeasures() {} + + /** + * Returns the sum of all polyline lengths on the unit sphere for shapes of dimension 1, or {@link + * S1Angle#ZERO} otherwise. See {@link #perimeter(S2Shape)} for shapes of dimension 2. + * + *

          See {@link S2ShapeIndexMeasures#length(S2ShapeIndex)} for more info. + */ + public static S1Angle length(S2Shape shape) { + if (shape.dimension() != 1) { + return S1Angle.ZERO; + } + S1Angle.Builder builder = new S1Angle.Builder(); + for (int chainId = 0; chainId < shape.numChains(); chainId++) { + builder.add(polylineLength(shape, chainId)); + } + return builder.build(); + } + + /** + * Returns the length of the polyline, or {@link S1Angle#ZERO} if the polyline has fewer than two + * vertices. + */ + @VisibleForTesting + static S1Angle polylineLength(S2Shape shape, int chainId) { + S1Angle.Builder builder = new S1Angle.Builder(); + forEachChainEdge(shape, chainId, (a, b) -> builder.add(a.angle(b))); + return builder.build(); + } + + /** + * Returns the sum of all loop perimeters on the unit sphere for shapes of dimension 2, or {@link + * S1Angle#ZERO} otherwise. See {@link #length(S2Shape)} for shapes of dimension 1. + */ + public static S1Angle perimeter(S2Shape shape) { + if (shape.dimension() != 2) { + return S1Angle.ZERO; + } + S1Angle.Builder builder = new S1Angle.Builder(); + for (int chainId = 0; chainId < shape.numChains(); chainId++) { + builder.add(loopPerimeter(shape, chainId)); + } + return builder.build(); + } + + /** Returns the perimeter of the loop, or {@link S1Angle#ZERO} if the loop has 0 or 1 vertices. */ + @VisibleForTesting + static S1Angle loopPerimeter(S2Shape shape, int chainId) { + if (shape.getChainLength(chainId) <= 1) { + return S1Angle.ZERO; + } + S1Angle.Builder builder = new S1Angle.Builder(); + forEachChainEdge(shape, chainId, (a, b) -> builder.add(a.angle(b))); + return builder.build(); + } + + /** + * For shapes of dimension 2, returns the area of the shape on the unit sphere. The result is + * between 0 and 4*Pi steradians. Otherwise returns zero. This method has good relative accuracy + * for both very large and very small regions. + */ + public static double area(S2Shape shape) { + if (shape.dimension() != 2) { + return 0; + } + double area = 0; + for (int chainId = 0; chainId < shape.numChains(); chainId++) { + area += signedLoopArea(shape, chainId); + } + // Note that signedLoopArea() guarantees that the full loop (containing all points on the + // sphere) has a very small negative area. + if (area < 0.0) { + area += 4 * S2.M_PI; + } + return area; + } + + /** + * Returns the area of the loop interior, i.e. the region on the left side of the loop. The result + * is between 0 and 4*Pi steradians. The implementation ensures that nearly-degenerate clockwise + * loops have areas close to zero, while nearly-degenerate counter-clockwise loops have areas + * close to 4*Pi. + */ + @VisibleForTesting + static double loopArea(S2Shape shape, int chainId) { + return loopArea(vertices(shape, chainId)); + } + + /** Same as {@link #loopArea(S2Shape, int)}, but takes a loop as a list of vertices. */ + static double loopArea(List loop) { + double area = signedLoopArea(loop); + assert (Math.abs(area) <= 2 * S2.M_PI); + if (area < 0.0) { + area += 4 * S2.M_PI; + } + return area; + } + + /** + * Returns the area of the loop interior, i.e. the region on the left side of the loop. The result + * is between 0 and 4*Pi steradians. The implementation ensures that nearly-degenerate clockwise + * loops have areas close to zero, while nearly-degenerate counter-clockwise loops have areas + * close to 4*Pi. + */ + private static double signedLoopArea(S2Shape shape, int chainId) { + return signedLoopArea(vertices(shape, chainId)); + } + + /** Same as {@link #signedLoopArea(S2Shape, int)}, but takes a loop as a list of vertices. */ + private static double signedLoopArea(List loop) { + MutableDouble mutableArea = new MutableDouble(); + S2ShapeUtil.visitSurfaceIntegral(loop, (a, b, c) -> mutableArea.d += S2.signedArea(a, b, c)); + double maxError = S2.getTurningAngleMaxError(loop.size()); + assert (Math.abs(mutableArea.d) <= 4 * S2.M_PI + maxError); + double area = mutableArea.d % (4 * S2.M_PI); + + if (area == -2 * S2.M_PI) { + area = 2 * S2.M_PI; + } + + if (Math.abs(area) <= maxError) { + double curvature = turningAngle(loop); + // Zero-area loops should have a curvature of approximately +/- 2*Pi. + assert (!(area == 0 && curvature == 0)); + if (curvature == 2 * S2.M_PI) { + // Degenerate + return 0.0; + } + if (area <= 0 && curvature > 0) { + return Double.MIN_VALUE; + } + // Full loops are handled by the case below. + if (area >= 0 && curvature < 0) { + return -Double.MIN_VALUE; + } + } + return area; + } + + static double turningAngle(S2Shape shape, int chainId) { + return turningAngle(vertices(shape, chainId)); + } + + /** + * Returns the geodesic curvature of the loop, defined as the sum of the turn angles at each + * vertex (see {@link S2#turnAngle(S2Point, S2Point, S2Point)}). The result is positive if the + * loop is counter-clockwise, negative if the loop is clockwise, and zero if the loop is a great + * circle. The geodesic curvature is equal to 2*Pi minus the area of the loop. + * + *

          The following cases are handled specially: + * + *

            + *
          • Degenerate loops (consisting of an isolated vertex or composed entirely of sibling edge + * pairs) have a curvature of 2*Pi exactly. + *
          • The full loop (containing all points, and represented as a loop with no vertices) has a + * curvature of -2*Pi exactly. + *
          • All other loops have a non-zero curvature in the range (-2*Pi, 2*Pi). For any such loop, + * reversing the order of the vertices is guaranteed to negate the curvature. This property + * can be used to define a unique normalized orientation for every loop. + *
          + */ + static double turningAngle(List loop) { + // By convention, a loop with no vertices contains all points on the sphere. + if (loop.isEmpty()) { + return -2 * S2.M_PI; + } + + // Remove any degeneracies from the loop. + loop = pruneDegeneracies(loop); + + // If the entire loop was degenerate, it's turning angle is defined as 2*Pi. + if (loop.isEmpty()) { + return 2 * S2.M_PI; + } + + // To ensure that we get the same result when the vertex order is rotated, and that the result + // is negated when the vertex order is reversed, we need to add up the individual turn angles in + // a consistent order. (In general, adding up a set of numbers in a different order can change + // the sum due to rounding errors). + // + // Furthermore, if we just accumulate an ordinary sum then the worst-case error is quadratic in + // the number of vertices. (This can happen with spiral shapes, where the partial sum of the + // turning angles can be linear in the number of vertices). To avoid this we use the Kahan + // summation algorithm (http://en.wikipedia.org/wiki/Kahan_summation_algorithm). + LoopOrder loopOrder = canonicalLoopOrder(loop); + int i = loopOrder.first; + int dir = loopOrder.dir; + int n = loop.size(); + double sum = + S2.turnAngle(loop.get((i + n - dir) % n), loop.get(i % n), loop.get((i + dir) % n)); + double compensation = 0; // Kahan summation algorithm + for (int x = 0; x < n - 1; x++) { + i += dir; + double angle = + S2.turnAngle(loop.get((i - dir) % n), loop.get(i % n), loop.get((i + dir) % n)); + double oldSum = sum; + angle += compensation; + sum += angle; + compensation = (oldSum - sum) + angle; + } + double maxCurvature = 2 * S2.M_PI - 4 * S2.DBL_EPSILON; + sum += compensation; + return Math.max(-maxCurvature, Math.min(maxCurvature, dir * sum)); + } + + /** + * Returns an index "first" and a direction "dir" such that the vertex sequence (first, first + + * dir, ..., first + (n - 1) * dir) does not change when the loop vertex order is rotated or + * reversed. This allows the loop vertices to be traversed in a canonical order. + */ + @VisibleForTesting + static LoopOrder canonicalLoopOrder(List loop) { + // In order to handle loops with duplicate vertices and/or degeneracies, we return the LoopOrder + // that minimizes the entire corresponding vertex *sequence*. For example, suppose that vertices + // are sorted alphabetically, and consider the loop CADBAB. The canonical loop order would be + // (4, 1), corresponding to the vertex sequence ABCADB. (For comparison, loop order (4, -1) + // yields the sequence ABDACB). + // + // If two or more loop orders yield identical minimal vertex sequences, then it doesn't matter + // which one we return (since they yield the same result). + + // For efficiency, we divide the process into two steps. First we find the smallest vertex, and + // the set of vertex indices where that vertex occurs (noting that the loop may contain + // duplicate vertices). Then we consider both possible directions starting from each such vertex + // index, and return the LoopOrder corresponding to the smallest vertex sequence. + if (loop.isEmpty()) { + return new LoopOrder(0, 1); + } + + List minIndexes = Lists.newArrayList(0); + for (int i = 1; i < loop.size(); i++) { + if (loop.get(i).compareTo(loop.get(minIndexes.get(0))) <= 0) { + if (loop.get(i).compareTo(loop.get(minIndexes.get(0))) < 0) { + minIndexes.clear(); + } + minIndexes.add(i); + } + } + LoopOrder minOrder = new LoopOrder(minIndexes.get(0), 1); + Comparator loopOrderComparator = new LoopOrderComparator(loop); + for (int minIndex : minIndexes) { + LoopOrder loopOrder1 = new LoopOrder(minIndex, 1); + LoopOrder loopOrder2 = new LoopOrder(minIndex + loop.size(), -1); + if (loopOrderComparator.compare(loopOrder1, minOrder) < 0) { + minOrder = loopOrder1; + } + if (loopOrderComparator.compare(loopOrder2, minOrder) < 0) { + minOrder = loopOrder2; + } + } + return minOrder; + } + + /** + * Returns a new loop obtained by removing all degeneracies from "input". In particular, the + * result will not contain any adjacent duplicate vertices or sibling edge pairs, i.e. vertex + * sequences of the form (A, A) or (A, B, A). + */ + @VisibleForTesting + static List pruneDegeneracies(List input) { + List loop = Lists.newArrayListWithCapacity(input.size()); + for (S2Point p : input) { + if (loop.isEmpty() || !p.equalsPoint(Iterables.getLast(loop))) { + if (loop.size() >= 2 && p.equalsPoint(loop.get(loop.size() - 2))) { + loop.remove(loop.size() - 1); + } else { + loop.add(p); + } + } + } + + // Check whether the loop was completely degenerate. + if (loop.size() < 3) { + return ImmutableList.of(); + } + + // Otherwise some portion of the loop is guaranteed to be non-degenerate. + // However there may still be some degenerate portions to remove. + if (loop.get(0).equalsPoint(Iterables.getLast(loop))) { + loop.remove(loop.size() - 1); + } + + // If the loop begins with BA and ends with A, then there is an edge pair of the form ABA at the + // end/start of the loop. Remove all such pairs. As noted above, this is guaranteed to leave a + // non-degenerate loop. + int i = 0; + while (loop.get(i + 1).equalsPoint(loop.get(loop.size() - i - 1))) { + i++; + } + return loop.subList(i, loop.size() - i); + } + + /** + * Returns the centroid of shape multiplied by the measure of shape. + * + *

          See {@link S2ShapeIndexMeasures#centroid(S2ShapeIndex)} for more info. + */ + public static S2Point centroid(S2Shape shape) { + S2Point.Builder builder = new S2Point.Builder(); + int dimension = shape.dimension(); + int numChains = shape.numChains(); + switch (dimension) { + case 0: + for (int chainId = 0; chainId < numChains; chainId++) { + builder.add(shape.getChainVertex(chainId, 0)); + } + break; + case 1: + for (int chainId = 0; chainId < numChains; chainId++) { + builder.add(polylineCentroid(shape, chainId)); + } + break; + case 2: + for (int chainId = 0; chainId < numChains; chainId++) { + builder.add(loopCentroid(shape, chainId)); + } + break; + default: + throw new IllegalArgumentException("Unexpected S2Shape dimension: " + shape.dimension()); + } + return builder.build(); + } + + /** + * Returns the true centroid of the polyline multiplied by the length of the polyline. + * + *

          Scaling by the polyline length makes it easy to compute the centroid of several polylines + * (by simply adding up their centroids). + * + *

          CAVEAT: Returns {@link S2Point#ORIGIN} for degenerate polylines (e.g., AA). [Note that this + * answer is correct; the result of this function is a line integral over the polyline, whose + * value is always zero if the polyline is degenerate]. + */ + @VisibleForTesting + static S2Point polylineCentroid(S2Shape shape, int chainId) { + S2Point.Builder builder = new S2Point.Builder(); + forEachChainEdge(shape, chainId, (a, b) -> builder.add(S2.trueCentroid(a, b))); + return builder.build(); + } + + /** + * Returns the true centroid of the loop multiplied by the area of the loop. + * + *

          See {@link S2ShapeIndexMeasures#centroid(S2ShapeIndex)} for more info. + */ + @VisibleForTesting + static S2Point loopCentroid(S2Shape shape, int chainId) { + CentroidMeasure centroidMeasure = new CentroidMeasure(); + S2ShapeUtil.visitSurfaceIntegral( + new AbstractList() { + @Override + public S2Point get(int i) { + return shape.getChainVertex(chainId, i); + } + + @Override + public int size() { + return shape.getChainLength(chainId); + } + }, + centroidMeasure); + return centroidMeasure.value(); + } + + private static List vertices(S2Shape shape, int chainId) { + return new AbstractList() { + int size = shape.getChainLength(chainId); + + @Override + public S2Point get(int i) { + return shape.getChainVertex(chainId, i); + } + + @Override + public int size() { + return size; + } + }; + } + + /** Passes each edge (a, b) in the chain of shape at index chainId to edgeConsumer. */ + private static void forEachChainEdge( + S2Shape shape, int chainId, BiConsumer edgeConsumer) { + int chainLength = shape.getChainLength(chainId); + if (chainLength == 0) { + return; + } + S2Point prev = shape.getChainVertex(chainId, 0); + for (int edgeOffset = 1; edgeOffset <= chainLength; edgeOffset++) { + S2Point next = shape.getChainVertex(chainId, edgeOffset); + edgeConsumer.accept(prev, next); + prev = next; + } + } + + /** + * Represents a cyclic ordering of the loop vertices, starting at the index "first" and proceeding + * in direction "dir" (either +1 or -1). "first" and "dir" must be chosen such that (first, ..., + * first + n * dir) are all in the range [0, 2*n-1]. + */ + @VisibleForTesting + static class LoopOrder { + final int first; + final int dir; + + LoopOrder(int first, int dir) { + this.first = first; + this.dir = dir; + } + + boolean equalsLoopOrder(LoopOrder other) { + return first == other.first && dir == other.dir; + } + + @Override + public boolean equals(Object other) { + return other instanceof LoopOrder && equalsLoopOrder((LoopOrder) other); + } + + @Override + public int hashCode() { + return first + dir; + } + } + + private static class LoopOrderComparator implements Comparator { + private final List loop; + private final IntFunction vertex; + + LoopOrderComparator(List loop) { + this.loop = loop; + vertex = i -> loop.get(i % loop.size()); + } + + @Override + public int compare(LoopOrder loopOrder1, LoopOrder loopOrder2) { + if (loopOrder1.equalsLoopOrder(loopOrder2)) { + return 0; + } + assert (vertex.apply(loopOrder1.first).equalsPoint(vertex.apply(loopOrder2.first))); + int i1 = loopOrder1.first; + int i2 = loopOrder2.first; + for (int n = loop.size(); --n > 0; ) { + i1 += loopOrder1.dir; + i2 += loopOrder2.dir; + int compare = vertex.apply(i1).compareTo(vertex.apply(i2)); + if (compare != 0) { + return compare; + } + } + return 0; + } + } + + /** A consumer which accepts two arguments. */ + private interface BiConsumer { + void accept(T t, U u); + } + + /** A function which accepts an int. */ + private interface IntFunction { + T apply(int i); + } + + /** Wraps a mutable primitive double. */ + private static class MutableDouble { + double d = 0; + } +} diff --git a/core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/S2ShapeUtil.java b/core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/S2ShapeUtil.java new file mode 100644 index 0000000000..f53f9d99a7 --- /dev/null +++ b/core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/S2ShapeUtil.java @@ -0,0 +1,925 @@ +/* + * Copyright 2014 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.mogo.eagle.core.utilcode.geometry; + +import com.google.common.annotations.GwtCompatible; +import com.google.common.base.Preconditions; +import com.google.common.base.Supplier; +import com.google.common.collect.Lists; +import com.google.common.collect.Maps; +import com.google.common.collect.Multimap; +import com.google.common.collect.Multimaps; +import com.google.common.collect.Sets; +import com.mogo.eagle.core.utilcode.geometry.S2EdgeUtil.EdgeCrosser; +import com.mogo.eagle.core.utilcode.geometry.S2EdgeUtil.WedgeRelation; +import com.mogo.eagle.core.utilcode.geometry.S2Error.Code; +import com.mogo.eagle.core.utilcode.geometry.S2Shape.MutableEdge; +import com.mogo.eagle.core.utilcode.geometry.S2Shape.ReferencePoint; +import com.mogo.eagle.core.utilcode.geometry.S2ShapeIndex.Cell; +import com.mogo.eagle.core.utilcode.geometry.S2ShapeIndex.S2ClippedShape; +import java.util.AbstractList; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; + +/** Utilities for working with S2Shape. */ +@GwtCompatible +public strictfp class S2ShapeUtil { + /** Utility methods only. */ + private S2ShapeUtil() {} + + /** + * S2EdgeVectorShape is an S2Shape representing a set of unrelated edges. It contains no area and + * has no interior. Although it implements List, only the {@link #add(S2Point, S2Point)} + * method can mutate the list of edges. + * + *

          It is mainly used for testing, but it can also be useful if you have, say, a collection of + * polylines and don't care about memory efficiency (since this class would store most of the + * vertices twice.) If the vertices are already stored somewhere else, you would be better off + * writing your own subclass of S2Shape that points to the existing vertex data rather than + * copying it. + */ + static class S2EdgeVectorShape extends AbstractList implements S2Shape { + + private final List edges = Lists.newArrayList(); + + /** Default constructor creates a vector with no edges. */ + public S2EdgeVectorShape() {} + + /** Convenience constructor for creating a vector of length 1. */ + public S2EdgeVectorShape(S2Point a, S2Point b) { + add(a, b); + } + + /** Adds an edge to the vector. */ + public void add(S2Point a, S2Point b) { + Preconditions.checkArgument(!a.equalsPoint(b)); + edges.add(new S2Edge(a, b)); + } + + @Override + public void getEdge(int index, MutableEdge result) { + S2Edge edge = edges.get(index); + result.set(edge.getStart(), edge.getEnd()); + } + + @Override + public boolean hasInterior() { + return false; + } + + @Override + public boolean containsOrigin() { + return false; + } + + @Override + public int numEdges() { + return edges.size(); + } + + @Override + public int numChains() { + return edges.size(); + } + + @Override + public int getChainStart(int chainId) { + Preconditions.checkElementIndex(chainId, numChains()); + return chainId; + } + + @Override + public int getChainLength(int chainId) { + Preconditions.checkElementIndex(chainId, numChains()); + return 1; + } + + @Override + public void getChainEdge(int chainId, int offset, MutableEdge result) { + Preconditions.checkElementIndex(offset, getChainLength(chainId)); + getEdge(chainId, result); + } + + @Override + public S2Point getChainVertex(int chainId, int edgeOffset) { + Preconditions.checkElementIndex(edgeOffset, getChainLength(chainId)); + S2Edge edge = edges.get(chainId); + return edgeOffset == 0 ? edge.getStart() : edge.getEnd(); + } + + @Override + public int dimension() { + return 1; + } + + @Override + public S2Edge get(int index) { + return edges.get(index); + } + + @Override + public int size() { + return edges.size(); + } + } + + /** + * Given an S2ShapeIndex containing a single loop, return true if the loop has a self-intersection + * (including duplicate vertices) and set "error" to a human-readable error message. Otherwise + * return false and leave "error" unchanged. + */ + static boolean findSelfIntersection(S2ShapeIndex index, S2Loop loop, S2Error error) { + Preconditions.checkArgument(1 == index.shapes.size()); + for (S2Iterator it = index.iterator(); !it.done(); it.next()) { + if (findSelfIntersection(it.entry().clipped(0), loop, error)) { + return true; + } + } + return false; + } + + /** + * Given an S2ShapeIndex containing a set of loops, return true if any loop has a + * self-intersection (including duplicate vertices) or crosses any other loop (including vertex + * crossings and duplicate edges) and set "error" to a human-readable error message. Otherwise + * return false and leave "error" unchanged. + */ + static boolean findAnyCrossing(S2ShapeIndex index, List loops, S2Error error) { + for (S2Iterator it = index.iterator(); !it.done(); it.next()) { + if (findSelfIntersection(loops, it.entry(), error)) { + return true; + } + if (it.entry().numShapes() >= 2 && findLoopCrossing(loops, it.entry(), error)) { + return true; + } + } + return false; + } + + /** + * Test for crossings between all edge pairs that do not share a vertex. This means that (a) the + * loop edge indices must differ by 2 or more, and (b) the pair cannot consist of the first and + * last loop edges. Part (b) is worthwhile in the case of very small loops; e.g. it reduces the + * number of crossing tests in a loop with four edges from two to one. + */ + static boolean findSelfIntersection(S2ClippedShape aClipped, S2Loop aLoop, S2Error error) { + int aNumClipped = aClipped.numEdges(); + for (int i = 0; i < aNumClipped - 1; i++) { + int ai = aClipped.edge(i); + int j = i + 1; + if (aClipped.edge(j) == ai + 1) { + // Adjacent edges + if (++j >= aNumClipped) { + continue; + } + } + + EdgeCrosser crosser = new EdgeCrosser(aLoop.vertex(ai), aLoop.vertex(ai + 1)); + for (int ajPrev = -2; j < aNumClipped; j++) { + int aj = aClipped.edge(j); + if (aj - ai == aLoop.numVertices() - 1) { + // First and last edges + break; + } + if (aj != ajPrev + 1) { + crosser.restartAt(aLoop.vertex(aj)); + } + ajPrev = aj; + + // This test also catches duplicate vertices. + int crossing = crosser.robustCrossing(aLoop.vertex(aj + 1)); + if (crossing < 0) { + continue; + } + if (crossing == 0) { + error.init(Code.DUPLICATE_VERTICES, "Edge %d has duplicate vertex with edge %d", ai, aj); + } else { + error.init(Code.LOOP_SELF_INTERSECTION, "Edge %d crosses edge %d", ai, aj); + } + + return true; + } + } + + return false; + } + + /** + * Returns the index of {@code shape} in {@code shapes} as {@link List#indexOf(Object)}, but using + * identity instead of equality to honor the semantics of S2ShapeIndex (where adding two S2Loops + * that are equal but not the same instance is treated as adding two separate shapes, with + * distinct shape IDs.) + */ + static int indexOf(List shapes, S2Shape shape) { + for (int i = 0; i < shapes.size(); i++) { + if (shapes.get(i) == shape) { + return i; + } + } + return -1; + } + + /** A filter of indexes. */ + public interface IntPredicate { + /** Returns whether the given value tests as true. */ + boolean test(int index); + } + + /** Returns the lowest index in the range {@code [low, high)} not smaller than a target. */ + public static int lowerBound(int low, int high, IntPredicate targetIsGreater) { + while (low < high) { + int middle = low + (high - low) / 2; + if (targetIsGreater.test(middle)) { + low = middle + 1; + } else { + high = middle; + } + } + return low; + } + + /** Returns the lowest index in the range {@code [low, high)} greater than a target. */ + public static int upperBound(int low, int high, IntPredicate targetIsSmaller) { + while (low < high) { + int middle = low + (high - low) / 2; + if (targetIsSmaller.test(middle)) { + high = middle; + } else { + low = middle + 1; + } + } + return low; + } + + /** + * Returns true if any of the given loops has a self-intersection (including a duplicate vertex), + * and set "error" to a human-readable error message. Otherwise return false and leave "error" + * unchanged. All tests are limited to edges that intersect the given cell. + */ + static boolean findSelfIntersection(List loops, Cell cell, S2Error error) { + for (int a = 0; a < cell.numShapes(); a++) { + S2ClippedShape aClipped = cell.clipped(a); + S2Loop loop = (S2Loop) aClipped.shape(); + if (findSelfIntersection(aClipped, loop, error)) { + error.init(error.code(), "Loop %d: %s", indexOf(loops, loop), error.text()); + return true; + } + } + return false; + } + + /** + * Given two loop edges for which RobustCrossing returned a non-negative result "crossing", + * returns true if there is a crossing and sets "error" to a human-readable error message, + * otherwise returns false. + */ + static boolean getCrossingError( + List loops, S2Loop aLoop, int ai, S2Loop bLoop, int bj, int crossing, S2Error error) { + if (crossing > 0) { + error.init( + Code.POLYGON_LOOPS_CROSS, + "Loop %d edge %d crosses loop %d edge %d", + indexOf(loops, aLoop), + ai, + indexOf(loops, bLoop), + bj); + return true; + } + + // Loops are not allowed to share edges or cross at vertices. We only need to check this once + // per edge pair, so we also require that the two edges have the same end vertex. (This is only + // valid because we are iterating over all the cells in the index.) + if (aLoop.vertex(ai + 1).equalsPoint(bLoop.vertex(bj + 1))) { + if (aLoop.vertex(ai).equalsPoint(bLoop.vertex(bj)) + || aLoop.vertex(ai).equalsPoint(bLoop.vertex(bj + 2))) { + // The second edge index is sometimes off by one, hence "near". + error.init( + Code.POLYGON_LOOPS_SHARE_EDGE, + "Loop %d edge %d has duplicate near loop %d edge %d", + indexOf(loops, aLoop), + ai, + indexOf(loops, bLoop), + bj); + return true; + } + + // Note that we don't need to maintain any state regarding loop crossings because duplicate + // edges are not allowed. + if (WedgeRelation.WEDGE_PROPERLY_OVERLAPS + == S2EdgeUtil.getWedgeRelation( + aLoop.vertex(ai), + aLoop.vertex(ai + 1), + aLoop.vertex(ai + 2), + bLoop.vertex(bj), + bLoop.vertex(bj + 2))) { + error.init( + Code.POLYGON_LOOPS_CROSS, + "Loop %d edge %d crosses loop %d edge %d", + indexOf(loops, aLoop), + ai, + indexOf(loops, bLoop), + bj); + return true; + } + } + + return false; + } + + /** + * Returns true if any of the given loops crosses a different loop (including vertex crossings) or + * two loops share a common edge, and sets "error" to a human-readable error message. Otherwise + * return false and leaves "error" unchanged. All tests are limited to edges that intersect the + * given cell. + */ + static boolean findLoopCrossing(List loops, Cell cell, S2Error error) { + // Possible optimization: + // Sort the ClippedShapes by edge count to reduce the number of calls to S2Predicates.sign(). + // If n is the total number of shapes in the cell, n_i is the number of edges in shape i, and + // c_i is the number of continuous chains formed by these edges, the total number of calls is + // + // sum(n_i * (1 + c_j + n_j), i=0..n-2, j=i+1..n-1) + // + // So for example if n=2, shape 0 has one chain of 1 edge, and shape 1 has one chain of 8 edges, + // the number of calls to S2Predicates.sign() is 1*10=10 if the shapes are sorted by edge count, + // and 8*3=24 otherwise. + for (int a = 0; a < cell.numShapes() - 1; a++) { + S2ClippedShape aClipped = cell.clipped(a); + S2Loop aLoop = (S2Loop) aClipped.shape(); + int aNumClipped = aClipped.numEdges(); + for (int i = 0; i < aNumClipped; i++) { + int ai = aClipped.edge(i); + EdgeCrosser crosser = new EdgeCrosser(aLoop.vertex(ai), aLoop.vertex(ai + 1)); + for (int b = a + 1; b < cell.numShapes(); b++) { + S2ClippedShape bClipped = cell.clipped(b); + S2Loop bLoop = (S2Loop) bClipped.shape(); + int bjPrev = -2; + int bNumClipped = bClipped.numEdges(); + for (int j = 0; j < bNumClipped; j++) { + int bj = bClipped.edge(j); + if (bj != bjPrev + 1) { + crosser.restartAt(bLoop.vertex(bj)); + } + bjPrev = bj; + int crossing = crosser.robustCrossing(bLoop.vertex(bj + 1)); + if (crossing < 0) { + // No crossing + continue; + } + if (getCrossingError(loops, aLoop, ai, bLoop, bj, crossing, error)) { + return true; + } + } + } + } + } + return false; + } + + /** + * Returns true if all methods of the two S2Shapes return identical results, except for id() and + * typeTag(). Also returns true if both instances are null. + */ + public static boolean equals(S2Shape a, S2Shape b) { + // Check null on either side. + if (a == null) { + return b == null; + } else if (b == null) { + return a == null; + } + + // Check geometry type properties of the shapes. + if (a.hasInterior() != b.hasInterior()) { + return false; + } + if (a.hasInterior() && (a.containsOrigin() != b.containsOrigin())) { + return false; + } + if (a.dimension() != b.dimension()) { + return false; + } + + // Check chain methods. + if (a.numChains() != b.numChains()) { + return false; + } + for (int i = 0; i < a.numChains(); i++) { + if (a.getChainStart(i) != b.getChainStart(i)) { + return false; + } + if (a.getChainLength(i) != b.getChainLength(i)) { + return false; + } + } + + // Check edge methods. + if (a.numEdges() != b.numEdges()) { + return false; + } + MutableEdge edge = new MutableEdge(); + for (int i = 0; i < a.numEdges(); i++) { + a.getEdge(i, edge); + S2Point p = edge.a; + S2Point q = edge.b; + b.getEdge(i, edge); + if (!p.equalsPoint(edge.a) || !q.equalsPoint(edge.b)) { + return false; + } + } + + return true; + } + + /** + * Returns true if the lists 'a' and 'b' have identical shapes according to {@link + * #equals(S2Shape, S2Shape)}. + */ + public static boolean equals(List a, List b) { + if (a.size() != b.size()) { + return false; + } + for (int i = 0; i < a.size(); i++) { + if (!equals(a.get(i), b.get(i))) { + return false; + } + } + return true; + } + + /** + * Returns true if the clipped shapes 'a' and 'b' have identical edge offsets. + * + *

          This method does not check that {@code a.shape()} and {@code b.shape()} are equal. + */ + public static boolean equals(S2ClippedShape a, S2ClippedShape b) { + if (a.containsCenter() != b.containsCenter()) { + return false; + } + if (a.numEdges() != b.numEdges()) { + return false; + } + for (int i = 0; i < a.numEdges(); i++) { + if (a.edge(i) != b.edge(i)) { + return false; + } + } + return true; + } + + /** Returns true if the index cells 'a' and 'b' contain identical contents. */ + public static boolean equals(S2ShapeIndex.Cell a, S2ShapeIndex.Cell b) { + if (a.id() != b.id()) { + return false; + } + if (a.numShapes() != b.numShapes()) { + return false; + } + for (int i = 0; i < a.numShapes(); i++) { + if (!equals(a.clipped(i), b.clipped(i))) { + return false; + } + } + return true; + } + + /** + * Returns true if all methods of the two S2ShapeIndex values return identical results, including + * all the S2Shapes in both indexes. + */ + public static boolean equals(S2ShapeIndex a, S2ShapeIndex b) { + // Check that both indexes have identical shapes. + if (!equals(a.getShapes(), b.getShapes())) { + return false; + } + + // In order to test that the shapes referenced by all clipped shapes of all cells in each index + // are equal, we build a Multimap where each S2Shape is distinguished by its + // identity hash code. + Multimap shapes = + Multimaps.newSetMultimap(Maps.newIdentityHashMap(), Sets::newIdentityHashSet); + for (int i = 0; i < a.getShapes().size(); i++) { + shapes.put(a.getShapes().get(i), b.getShapes().get(i)); + } + + // Check that both indexes have identical cell contents. + S2Iterator aIt = a.iterator(); + S2Iterator bIt = b.iterator(); + for (; !aIt.done(); aIt.next(), bIt.next()) { + if (bIt.done()) { + return false; + } + if (!equals(aIt.entry(), bIt.entry())) { + return false; + } + // Check that each clipped shape references the same shape. + for (int i = 0; i < aIt.entry().numShapes(); i++) { + if (!shapes.containsEntry(aIt.entry().clipped(i).shape(), bIt.entry().clipped(i).shape())) { + return false; + } + } + } + if (!bIt.done()) { + return false; + } + + return true; + } + + /** Compares edges by start point, and then by end point. */ + private static final Comparator EDGE_ORDER = (e1, e2) -> { + int result = e1.getStart().compareTo(e2.getStart()); + if (result != 0) { + return result; + } else { + return e1.getEnd().compareTo(e2.getEnd()); + } + }; + + /** + * Returns true if the given shape contains the given point. Most clients should not use this + * method, since its running time is linear in the number of shape edges. Instead clients should + * create an S2ShapeIndex and use {@link S2ContainsPointQuery}, since that strategy is much more + * efficient when many points need to be tested. + * + *

          Polygon boundaries are treated as being semi-open. See {@link + * S2ContainsPointQuery.S2VertexModel} for other options. + */ + public static boolean containsBruteForce(S2Shape shape, S2Point point) { + if (shape.dimension() < 2) { + return false; + } + ReferencePoint refPoint = shape.getReferencePoint(); + if (refPoint.equalsPoint(point)) { + return refPoint.contained(); + } + EdgeCrosser crosser = new EdgeCrosser(refPoint, point); + boolean inside = refPoint.contained(); + MutableEdge edge = new MutableEdge(); + for (int i = 0; i < shape.numEdges(); i++) { + shape.getEdge(i, edge); + inside ^= crosser.edgeOrVertexCrossing(edge.getStart(), edge.getEnd()); + } + return inside; + } + + /** + * This is a helper function for implementing S2Shape.getReferencePoint(). + * + *

          Given a shape consisting of closed polygonal loops, the interior of the shape is defined as + * the region to the left of all edges (which must be oriented consistently). This function then + * chooses an arbitrary point and returns true if that point is contained by the shape. + * + *

          Unlike S2Loop and S2Polygon, this method allows duplicate vertices and edges, which requires + * some extra care with definitions. The rule that we apply is that an edge and its reverse edge + * "cancel" each other: the result is the same as if that edge pair were not present. Therefore + * shapes that consist only of degenerate loop(s) are either empty or full; by convention, the + * shape is considered full if and only if it contains an empty loop (see S2LaxPolygonShape for + * details). + * + *

          Determining whether a loop on the sphere contains a point is harder than the corresponding + * problem in 2D plane geometry. It cannot be implemented just by counting edge crossings because + * there is no such thing as a "point at infinity" that is guaranteed to be outside the loop. + */ + public static ReferencePoint getReferencePoint(S2Shape shape) { + assert shape.dimension() == 2; + + if (shape.numEdges() == 0) { + // A shape with no edges is defined to be full if and only if it contains at least one chain. + return ReferencePoint.create(shape.numChains() > 0); + } + + // Define a "matched" edge as one that can be paired with a corresponding reversed edge. + // Define a vertex as "balanced" if all of its edges are matched. In order to determine + // containment, we must find an unbalanced vertex. Often every vertex is unbalanced, so we + // start by trying an arbitrary vertex. + MutableEdge edge = new MutableEdge(); + shape.getEdge(0, edge); + Boolean result; + if (null != (result = getReferencePointAtVertex(shape, edge.a))) { + return ReferencePoint.create(edge.a, result); + } + + // That didn't work, so now we do some extra work to find an unbalanced vertex (if any). + // Essentially we gather a list of edges and a list of reversed edges, and then sort them. + // The first edge that appears in one list but not the other is guaranteed to be unmatched. + int n = shape.numEdges(); + List fwdEdges = new ArrayList<>(n); + List revEdges = new ArrayList<>(n); + for (int i = 0; i < n; ++i) { + shape.getEdge(i, edge); + fwdEdges.add(new S2Edge(edge.a, edge.b)); + revEdges.add(new S2Edge(edge.b, edge.a)); + } + Collections.sort(fwdEdges, EDGE_ORDER); + Collections.sort(revEdges, EDGE_ORDER); + for (int i = 0; i < n; i++) { + S2Edge fwd = fwdEdges.get(i); + S2Edge rev = revEdges.get(i); + S2Point v; + int cmp = EDGE_ORDER.compare(fwd, rev); + if (cmp < 0) { + // fwd is unmatched + v = fwd.getStart(); + } else if (cmp > 0) { + // rev is unmatched + v = rev.getStart(); + } else { + // This edge is matched, so move on to the next one. + continue; + } + // We have an unbalanced vertex, so reference it and return. + return ReferencePoint.create(v, getReferencePointAtVertex(shape, v)); + } + + // All vertices are balanced, so this polygon is either empty or full except for degeneracies. + // By convention it is defined to be full if it contains any chain with no edges. + for (int i = 0; i < shape.numChains(); i++) { + if (shape.getChainLength(i) == 0) { + return ReferencePoint.create(true); + } + } + return ReferencePoint.create(false); + } + + /** + * Returns null if 'vtest' is balanced (see definition above), otherwise 'vtest' is unbalanced and + * the return value indicates whether it is contained by 'shape'. + */ + private static Boolean getReferencePointAtVertex(S2Shape shape, S2Point vtest) { + // Let P be an unbalanced vertex. Vertex P is defined to be inside the region if the region + // contains a particular direction vector starting from P, namely the direction of + // S2.ortho(target). This can be calculated using S2ContainsVertexQuery. + S2ContainsVertexQuery query = new S2ContainsVertexQuery(vtest); + MutableEdge edge = new MutableEdge(); + int n = shape.numEdges(); + for (int e = 0; e < n; ++e) { + shape.getEdge(e, edge); + if (vtest.equalsPoint(edge.a)) { + query.addOutgoing(edge.b); + } + if (vtest.equalsPoint(edge.b)) { + query.addIncoming(edge.a); + } + } + int containsSign = query.containsSign(); + if (containsSign == 0) { + // There are no unmatched edges incident to this vertex. + return null; + } else { + return containsSign > 0; + } + } + + /** + * Returns a multimap of {@link S2Shape} from {@code index} to the shape's ID (i.e., its position + * within {@code index.shapes}). + */ + static Multimap shapeToShapeId(S2ShapeIndex index) { + Multimap shapeToShapeId = + Multimaps.newListMultimap(Maps.newIdentityHashMap(), Lists::newArrayList); + for (S2Shape shape : index.shapes) { + shapeToShapeId.put(shape, shapeToShapeId.size()); + } + return shapeToShapeId; + } + + /** A consumer of triangles. Implementations may sum area, turning angle, etc. */ + public interface TriangleConsumer { + void accept(S2Point a, S2Point b, S2Point c); + } + + // It is surprisingly difficult to compute the area of a loop robustly. The main issues are + // + // (1) whether degenerate loops are considered to be CCW or not + // i.e., whether their area is close to 0 or 4*Pi, and + // (2) computing the areas of small loops with good relative accuracy. + // + // With respect to degeneracies, we would like getArea() to be consistent with + // S2Loop.contains(S2Point), such that loops containing many points should have large areas, and + // loops that contain few points should have small areas. + // + // For example, if a degenerate triangle is considered CCW according to S2Predicates.sign(), then + // it will contain very few points and its area should be approximately zero. On the other hand if + // it is considered clockwise, then it will contain virtually all points and so its area should + // be approximately 4*Pi. + // + // More precisely, let U be the set of S2Points for which S2.isUnitLength() is true, let P(U) be + // the projection of those points onto the mathematical unit sphere, and let V(P(U)) be the + // Voronoi diagram of the projected points. Then for every loop x, we would like getArea() to + // approximately equal the sum of the areas of the Voronoi regions of the points p for which + // x.contains(p) is true. + // + // The second issue is that we want to compute the area of small loops accurately. This requires + // having good relative precision rather than good absolute precision. For example, if the area + // of a loop is 1e-12 and the error is 1e-15, then the area only has 3 digits of accuracy. (For + // reference, 1e-12 is about 40 square meters on the surface of the earth.) We would like to + // have good relative accuracy even for small loops. + // + // To achieve these goals, we combine two different methods of computing the area. This first + // method is based on the Gauss-Bonnet theorem, which says that the area enclosed by the loop + // equals 2*Pi minus the total geodesic curvature of the loop (i.e., the sum of the "turning + // angles" at all the loop vertices). The big advantage of this method is that as long as we + // use S2Predicates.sign() to compute the turning angle at each vertex, then degeneracies are + // always handled correctly. In other words, if a degenerate loop is CCW according to the symbolic + // perturbations used by S2Predicates.sign(), then its turning angle will be approximately 2*Pi. + // + // The disadvantage of the Gauss-Bonnet method is that its absolute error is about 2e-15 times + // the number of vertices (see S2Loop.getTurningAngleMaxError). So, it cannot compute the area + // of small loops accurately. + // + // The second method is based on splitting the loop into triangles and summing the area of each + // triangle. To avoid the difficulty and expense of decomposing the loop into a union of non- + // overlapping triangles, instead we compute a signed sum over triangles that may overlap (see + // the comments for S2Loop.visitSurfaceIntegral). The advantage of this method is that the area + // of each triangle can be computed with much better relative accuracy (using l'Huilier's + // theorem). The disadvantage is that the result is a signed area: CCW loops may yield a small + // positive value, while CW loops may yield a small negative value (which is converted to a + // positive area by adding 4*Pi). This means that small errors in computing the signed area may + // translate into a very large error in the result (if the sign of the sum is incorrect). + // + // So, our strategy is to combine these two methods as follows. First we compute the area using + // the "signed sum over triangles" approach (since it is generally more accurate). We also + // estimate the maximum error in this result. If the signed area is too close to zero (i.e., + // zero is within the error bounds), then we double-check the sign of the result using the + // Gauss-Bonnet method. (In fact we just call isNormalized(), which is based on this method.) + // If the two methods disagree, we return either 0 or 4*Pi based on the result of + // isNormalized(). Otherwise we return the area that we computed originally. + + /** A collector of the steradian area. */ + public static final class AreaMeasure implements TriangleConsumer { + private double area; + + @Override + public void accept(S2Point a, S2Point b, S2Point c) { + area += S2.signedArea(a, b, c); + } + + /** Returns the area. Only call after all triangles have been consumed. */ + public double value(int numVertices, Supplier isNormalized) { + // TODO(user): This error estimate is very approximate. There are two issues: + // + // (1) signedArea needs some improvements to ensure that its error is actually never higher + // than girardArea, and + // (2) although the number of triangles in the sum is typically N-2, in theory it could be as + // high as 2*N for pathological inputs. + // + // But in other respects this error bound is very conservative since it assumes that the + // maximum error is achieved on every triangle. + double maxError = S2.getTurningAngleMaxError(numVertices); + + // The signed area should be between approximately -4*Pi and 4*Pi. + assert (Math.abs(area) <= 4 * S2.M_PI + maxError); + if (area < 0) { + // We have computed the negative of the area of the loop exterior. + area += 4 * S2.M_PI; + } + area = Math.max(0.0, Math.min(4 * S2.M_PI, area)); + + // If the area is close enough to zero or 4*Pi so that the loop orientation + // is ambiguous, then we compute the loop orientation explicitly. + if (area < maxError && !isNormalized.get()) { + return 4 * S2.M_PI; + } else if (area > (4 * S2.M_PI - maxError) && isNormalized.get()) { + return 0.0; + } else { + return area; + } + } + } + + /** A collector of the center of mass. */ + public static final class CentroidMeasure implements TriangleConsumer { + private final double[] sum = new double[3]; + + @Override + public void accept(S2Point a, S2Point b, S2Point c) { + S2Point centroid = S2.trueCentroid(a, b, c); + sum[0] += centroid.x; + sum[1] += centroid.y; + sum[2] += centroid.z; + } + + /** Returns the centroid. Only call after all triangles have been consumed. */ + public S2Point value() { + return new S2Point(sum[0], sum[1], sum[2]); + } + } + + /** A collector of both combined area and centroid values. */ + public static final class AreaCentroidMeasure implements TriangleConsumer { + private final AreaMeasure area = new AreaMeasure(); + private final CentroidMeasure centroid = new CentroidMeasure(); + + @Override + public void accept(S2Point a, S2Point b, S2Point c) { + area.accept(a, b, c); + centroid.accept(a, b, c); + } + + /** Returns the area and centroid. Only call after all triangles have been consumed. */ + public S2AreaCentroid value(int numVertices, Supplier isNormalized) { + return new S2AreaCentroid(area.value(numVertices, isNormalized), centroid.value()); + } + } + + /** + * Visits the surface integral of the vertices, that is, a collection of oriented triangles, + * possibly overlapping. + * + *

          Let the sign of a triangle be +1 if it is CCW and -1 otherwise, and let the sign of a point + * "x" be the sum of the signs of the triangles containing "x". Then the collection of triangles T + * is chosen such that either: + * + *

            + *
          1. Each point in the loop interior has sign +1, and sign 0 otherwise; or + *
          2. Each point in the loop exterior has sign -1, and sign 0 otherwise. + *
          + * + *

          The triangles basically consist of a "fan" from vertex 0 to every loop edge that does not + * include vertex 0. These triangles will always satisfy either (1) or (2). + * + *

          However, what makes this a bit tricky is that spherical edges become numerically unstable as + * their length approaches 180 degrees. Of course there is not much we can do if the loop itself + * contains such edges, but we would like to make sure that all the triangle edges under our + * control (i.e., the non-loop edges) are stable. For example, consider a loop around the equator + * consisting of four equally spaced points. This is a well-defined loop, but we cannot just split + * it into two triangles by connecting vertex 0 to vertex 2. + * + *

          We handle this type of situation by moving the origin of the triangle fan whenever we are + * about to create an unstable edge. We choose a new location for the origin such that all + * relevant edges are stable. We also create extra triangles with the appropriate orientation so + * that the sum of the triangle signs is still correct at every point. + */ + public static void visitSurfaceIntegral(List vertices, TriangleConsumer consumer) { + if (vertices.size() < 3) { + return; + } + // The maximum length of an edge for it to be considered numerically stable. The exact value is + // fairly arbitrary since it depends on the stability of the consumer's processing. The value + // below is quite conservative but could be reduced further if desired. + final double maxLength = S2.M_PI - 1e-5; + S2Point v0 = vertices.get(0); + S2Point origin = v0; + + // Let V_i be vertices.get(i), let O be the current origin, and let length(A,B) be the length of + // edge (A,B). At the start of each loop iteration, the "leading edge" of the triangle fan is + // (O,V_i), and we want to extend the triangle fan so that the leading edge is (O,V_i+1). + // + // Invariants: + // 1. length(O,V_i) < kMaxLength for all (i > 1). + // 2. Either O == V_0, or O is approximately perpendicular to V_0. + // 3. "sum" is the oriented integral of f over the area defined by + // (O, V_0, V_1, ..., V_i). + int numVertices = vertices.size(); + for (int i = 1; i + 1 < numVertices; i++) { + assert (i == 1 || origin.angle(vertices.get(i)) < maxLength); + assert (origin.equals(v0) || Math.abs(origin.dotProd(v0)) < 1e-15); + S2Point v1 = vertices.get(i); + S2Point v2 = vertices.get(i + 1); + if (v2.angle(origin) > maxLength) { + // We are about to create an unstable edge, so choose a new origin O' for the triangle fan. + S2Point oldOrigin = origin; + if (origin.equalsPoint(v0)) { + // The following point is well-separated from V_i and V_0 (and therefore V_i+1 as well). + origin = S2Point.normalize(S2.robustCrossProd(v0, v1)); + } else if (v1.angle(v0) < maxLength) { + // All edges of tri (O, V_0, V_i) are stable, so we can revert to using V_0 as the origin. + origin = v0; + } else { + // (O, V_i+1) and (V_0, V_i) are antipodal pairs, and O and V_0 are perpendicular. + // Therefore V_0.crossProd(O) is approximately perpendicular to all of + // {O, V_0, V_i, V_i+1}, and we can choose this point O' as the new origin. + origin = S2Point.crossProd(v0, oldOrigin); + + // Advance the edge (V_0,O) to (V_0,O'). + consumer.accept(v0, oldOrigin, origin); + } + // Advance the edge (O,V_i) to (O',V_i). + consumer.accept(oldOrigin, v1, origin); + } + // Advance the edge (O,V_i) to (O,V_i+1). + consumer.accept(origin, v1, v2); + } + + // If the origin is not V_0, we need to sum one more triangle. + if (!origin.equalsPoint(v0)) { + // Advance the edge (O,V_n-1) to (O,V_0). + consumer.accept(origin, vertices.get(numVertices - 1), v0); + } + } +} diff --git a/core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/S2TaggedShapeCoder.java b/core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/S2TaggedShapeCoder.java new file mode 100644 index 0000000000..bdd8478877 --- /dev/null +++ b/core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/S2TaggedShapeCoder.java @@ -0,0 +1,259 @@ +/* + * Copyright 2018 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.mogo.eagle.core.utilcode.geometry; + +import com.google.common.annotations.GwtIncompatible; +import com.google.common.base.Preconditions; +import com.google.common.collect.ImmutableList; +import com.mogo.eagle.core.utilcode.geometry.PrimitiveArrays.Bytes; +import com.mogo.eagle.core.utilcode.geometry.PrimitiveArrays.Cursor; +import com.google.common.primitives.Ints; +import java.io.IOException; +import java.io.OutputStream; +import java.util.HashMap; +import java.util.IdentityHashMap; +import java.util.List; +import java.util.Map; + +/** An encoder/decoder of tagged {@link S2Shape}s. */ +@GwtIncompatible("S2LaxPolylineShape and S2LaxPolygonShape") +public class S2TaggedShapeCoder implements S2Coder { + + private static final int POLYGON_TYPE_TAG = 1; + private static final int POLYLINE_TYPE_TAG = 2; + private static final int POINT_TYPE_TAG = 3; + private static final int LAX_POLYLINE_TYPE_TAG = 4; + private static final int LAX_POLYGON_TYPE_TAG = 5; + + private static final S2Coder FAST_POLYGON_SHAPE_CODER = + new S2Coder() { + @Override + public void encode(S2Polygon.Shape value, OutputStream output) throws IOException { + value.polygon().encodeUncompressed(new LittleEndianOutput(output)); + } + + @Override + public S2Polygon.Shape decode(Bytes data, Cursor cursor) { + try { + return S2Polygon.decode(data.toInputStream(cursor)).shape(); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + }; + + private static final S2Coder COMPACT_POLYGON_SHAPE_CODER = + new S2Coder() { + @Override + public void encode(S2Polygon.Shape value, OutputStream output) throws IOException { + value.polygon().encode(output); + } + + @Override + public S2Polygon.Shape decode(Bytes data, Cursor cursor) { + return FAST_POLYGON_SHAPE_CODER.decode(data, cursor); + } + }; + + private static final S2Coder FAST_POLYLINE_SHAPE_CODER = + new S2Coder() { + @Override + public void encode(S2Polyline value, OutputStream output) throws IOException { + value.encode(output); + } + + @Override + public S2Polyline decode(Bytes data, Cursor cursor) { + try { + return S2Polyline.decode(data.toInputStream(cursor)); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + }; + + private static final ImmutableList> POLYGON_SHAPE_CLASSES = + ImmutableList.of( + new S2Polygon().binarySearchShape().getClass(), + new S2Polygon().linearSearchShape().getClass()); + private static final ImmutableList> POINT_SHAPE_CLASSES = + ImmutableList.of( + S2Point.Shape.class, + S2Point.Shape.singleton(S2Point.ORIGIN).getClass(), + S2Point.Shape.fromList(ImmutableList.of(S2Point.ORIGIN, S2Point.ORIGIN)).getClass()); + private static final ImmutableList> + LAX_POLYLINE_SHAPE_CLASSES = + ImmutableList.of( + S2LaxPolylineShape.SimpleArray.class, + S2LaxPolylineShape.SimpleList.class, + S2LaxPolylineShape.SimplePacked.class, + S2LaxPolylineShape.SimpleSnapped.class); + private static final ImmutableList> LAX_POLYGON_SHAPE_CLASSES = + ImmutableList.of( + S2LaxPolygonShape.SimpleArray.class, + S2LaxPolygonShape.SimpleList.class, + S2LaxPolygonShape.SimplePacked.class, + S2LaxPolygonShape.SimpleSnapped.class, + S2LaxPolygonShape.MultiArray.class, + S2LaxPolygonShape.MultiList.class, + S2LaxPolygonShape.MultiPacked.class, + S2LaxPolygonShape.MultiSnapped.class); + + /** + * An instance of a {@code S2TaggedShapeCoder} which encodes/decodes {@link S2Shape}s in the FAST + * encoding format. The FAST format is optimized for fast encoding/decoding. + */ + public static final S2TaggedShapeCoder FAST = + new Builder(true) + .add(POLYGON_SHAPE_CLASSES, FAST_POLYGON_SHAPE_CODER, POLYGON_TYPE_TAG) + .add(S2Polyline.class, FAST_POLYLINE_SHAPE_CODER, POLYLINE_TYPE_TAG) + .add(POINT_SHAPE_CLASSES, S2Point.Shape.Coder.FAST, POINT_TYPE_TAG) + .add(LAX_POLYLINE_SHAPE_CLASSES, S2LaxPolylineShape.Coder.FAST, LAX_POLYLINE_TYPE_TAG) + .add(LAX_POLYGON_SHAPE_CLASSES, S2LaxPolygonShape.Coder.FAST, LAX_POLYGON_TYPE_TAG) + .build(); + + /** + * An instance of a {@code S2TaggedShapeCoder} which encodes/decodes {@link S2Shape}s in the + * COMPACT encoding format. The COMPACT format is optimized for disk usage and memory footprint. + */ + public static final S2TaggedShapeCoder COMPACT = + new Builder(true) + .add(POLYGON_SHAPE_CLASSES, COMPACT_POLYGON_SHAPE_CODER, POLYGON_TYPE_TAG) + .add(S2Polyline.class, FAST_POLYLINE_SHAPE_CODER, POLYLINE_TYPE_TAG) + .add(POINT_SHAPE_CLASSES, S2Point.Shape.Coder.COMPACT, POINT_TYPE_TAG) + .add(LAX_POLYLINE_SHAPE_CLASSES, S2LaxPolylineShape.Coder.COMPACT, LAX_POLYLINE_TYPE_TAG) + .add(LAX_POLYGON_SHAPE_CLASSES, S2LaxPolygonShape.Coder.COMPACT, LAX_POLYGON_TYPE_TAG) + .build(); + + private final IdentityHashMap, Integer> classToTypeTag; + private final Map> typeTagToCoder; + + private S2TaggedShapeCoder( + IdentityHashMap, Integer> classToTypeTag, + Map> typeTagToCoder) { + this.classToTypeTag = classToTypeTag; + this.typeTagToCoder = typeTagToCoder; + } + + @Override + @SuppressWarnings("unchecked") // safe covariant cast + public void encode(S2Shape value, OutputStream output) throws IOException { + if (value == null) { + // A null shape is encoded as 0 bytes. + return; + } + Integer typeTag = classToTypeTag.get(value.getClass()); + Preconditions.checkArgument( + typeTag != null, "No S2Coder matched S2Shape with type %s", value.getClass().getName()); + EncodedInts.writeVarint64(output, typeTag); + ((S2Coder) typeTagToCoder.get(typeTag)).encode(value, output); + } + + @Override + public S2Shape decode(Bytes data, Cursor cursor) { + if (cursor.remaining() == 0) { + // A null shape is encoded as 0 bytes. + return null; + } + int typeTag = Ints.checkedCast(data.readVarint64(cursor)); + S2Coder coder = typeTagToCoder.get(typeTag); + Preconditions.checkArgument(coder != null, "No S2Coder matched type tag %s", typeTag); + return typeTagToCoder.get(typeTag).decode(data, cursor); + } + + /** Returns a new {@link Builder}. */ + public static Builder builder() { + return new Builder(false); + } + + /** Returns a new {@link Builder} initialized with the current {@link S2TaggedShapeCoder}. */ + public Builder toBuilder() { + return new Builder(classToTypeTag, typeTagToCoder); + } + + /** A builder for creating {@link S2TaggedShapeCoder} instances. */ + public static class Builder { + /** The minimum non-reserved type tag. */ + public static final int MIN_USER_TYPE_TAG = 8192; + + private final boolean allowReservedTags; + private final IdentityHashMap, Integer> classToTypeTag; + private final Map> typeTagToCoder; + + private Builder(boolean allowReservedTags) { + this.allowReservedTags = allowReservedTags; + classToTypeTag = new IdentityHashMap<>(); + typeTagToCoder = new HashMap<>(); + } + + private Builder( + IdentityHashMap, Integer> classToTypeTag, + Map> typeTagToCoder) { + this.allowReservedTags = false; + this.classToTypeTag = classToTypeTag; + this.typeTagToCoder = typeTagToCoder; + } + + /** + * Associates {@code clazz} with a unique {@code coder} and {@code typeTag}. + * + *

          If {@code clazz} or {@code typeTag} was already added, an {@link IllegalArgumentException} + * is thrown. + */ + Builder add(Class clazz, S2Coder coder, int typeTag) { + validateTypeTag(typeTag); + validateClass(clazz); + classToTypeTag.put(clazz, typeTag); + typeTagToCoder.put(typeTag, coder); + return this; + } + + /** + * Same as {@link #add(Class, S2Coder, int)}, but associates all elements of {@code clazzes} + * with a unique {@code coder} and {@code typeTag}. + */ + Builder add( + List> clazzes, S2Coder coder, int typeTag) { + validateTypeTag(typeTag); + for (Class clazz : clazzes) { + validateClass(clazz); + classToTypeTag.put(clazz, typeTag); + } + typeTagToCoder.put(typeTag, coder); + return this; + } + + private void validateTypeTag(int typeTag) { + Preconditions.checkArgument( + allowReservedTags || typeTag >= MIN_USER_TYPE_TAG, + "Type tag must be greater than %s, got: %s", + MIN_USER_TYPE_TAG, + typeTag); + Preconditions.checkArgument( + !typeTagToCoder.containsKey(typeTag), "Duplicate type tag: %s", typeTag); + } + + private void validateClass(Class clazz) { + Preconditions.checkArgument( + !classToTypeTag.containsKey(clazz), "Duplicate class: %s", clazz.getName()); + } + + /** Returns a newly-created {@link S2TaggedShapeCoder}. */ + S2TaggedShapeCoder build() { + return new S2TaggedShapeCoder(classToTypeTag, typeTagToCoder); + } + } +} diff --git a/core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/S2TextFormat.java b/core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/S2TextFormat.java new file mode 100644 index 0000000000..f0399adbe7 --- /dev/null +++ b/core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/S2TextFormat.java @@ -0,0 +1,738 @@ +/* + * Copyright 2005 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.mogo.eagle.core.utilcode.geometry; + +import static java.lang.Double.parseDouble; + +import com.google.common.annotations.GwtCompatible; +import com.google.common.annotations.GwtIncompatible; +import com.google.common.base.Preconditions; +import com.mogo.eagle.core.utilcode.geometry.S2Shape.MutableEdge; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import javax.annotation.Nullable; + +/** + * S2TextFormat contains a collection of functions for converting geometry to and from a human- + * readable format. It is mainly intended for testing and debugging. Be aware that the human- + * readable format is *not* designed to preserve the full precision of the original object, so it + * should not be used for data storage. + */ +@GwtCompatible +public strictfp class S2TextFormat { + + /** + * Returns an S2Point corresponding to the given a latitude-longitude coordinate in degrees. + * Example of the input format: "-20:150" + */ + public static S2Point makePointOrDie(String str) { + S2Point point = makePoint(str); + Preconditions.checkState(null != point, ": str == \"%s\"", str); + return point; + } + + /** + * As above, but do not CHECK-fail on invalid input. Returns null if conversion is unsuccessful. + */ + @Nullable + public static S2Point makePoint(String str) { + List vertices = parsePoints(str); + if (vertices == null || vertices.size() != 1) { + return null; + } + return vertices.get(0); + } + + /** + * Parses a string of one or more latitude-longitude coordinates in degrees, and return the + * corresponding List of S2LatLng points. + * + *

          Examples of the input format: + * + *

          +   *     ""                          // no points
          +   *     "-20:150"                   // one point
          +   *     "-20:150, -20:151, -19:150" // three points
          +   * 
          + */ + public static List parseLatLngsOrDie(String str) { + List latlngs = parseLatLngs(str); + Preconditions.checkState(latlngs != null, ": str == \"%s\"", str); + return latlngs; + } + + /** + * As above, but does not CHECK-fail on invalid input. Returns null if conversion is unsuccessful. + */ + @Nullable + static List parseLatLngs(String str) { + List ps = dictionaryParse(str); + if (ps == null) { + return null; + } + List latlngs = new ArrayList<>(); + for (ParseEntry p : ps) { + Double lat = parseDouble(p.key); + if (lat == null) { + return null; + } + Double lng = parseDouble(p.value); + if (lng == null) { + return null; + } + latlngs.add(S2LatLng.fromDegrees(lat, lng)); + } + return latlngs; + } + + /** + * Parses a string in the same format as parseLatLngs, and return the corresponding List of + * S2Point values. + */ + public static List parsePointsOrDie(String str) { + List vertices = parsePoints(str); + Preconditions.checkState(vertices != null, ": str == \"%s\"", str); + return vertices; + } + + /** + * As above, but does not CHECK-fail on invalid input. Returns null if conversion is unsuccessful. + */ + @Nullable + public static List parsePoints(String str) { + List latlngs = parseLatLngs(str); + if (latlngs == null) { + return null; + } + List vertices = new ArrayList<>(); + for (S2LatLng latlng : latlngs) { + vertices.add(latlng.toPoint()); + } + return vertices; + } + + /** Given a string in the same format as ParseLatLngs, returns a single S2LatLng. */ + public static S2LatLng makeLatLngOrDie(String str) { + S2LatLng latlng = makeLatLng(str); + Preconditions.checkState(null != latlng, ": str == \"%s\"", str); + return latlng; + } + + /** + * As above, but does not CHECK-fail on invalid input. Returns null if conversion is unsuccessful. + */ + @Nullable + public static S2LatLng makeLatLng(String str) { + List latlngs = parseLatLngs(str); + if (null == latlngs || latlngs.size() != 1) { + return null; + } + return latlngs.get(0); + } + + /** + * Given a string in the same format as ParseLatLngs, returns the minimal bounding S2LatLngRect + * that contains the coordinates. + */ + S2LatLngRect makeLatLngRectOrDie(String str) { + S2LatLngRect rect = makeLatLngRect(str); + Preconditions.checkState(null != rect, ": str == \"%s\"", str); + return rect; + } + + /** + * As above, but does not CHECK-fail on invalid input. Returns null if conversion is unsuccessful. + */ + @Nullable + public static S2LatLngRect makeLatLngRect(String str) { + List latlngs = parseLatLngs(str); + if (null == latlngs || latlngs.isEmpty()) { + return null; + } + S2LatLngRect rect = S2LatLngRect.fromPoint(latlngs.get(0)); + for (int i = 1; i < latlngs.size(); ++i) { + rect = rect.addPoint(latlngs.get(i)); + } + return rect; + } + + /** + * Parses an S2CellId in the format "f/dd..d" where "f" is a digit in the range [0-5] representing + * the S2CellId face, and "dd..d" is a string of digits in the range [0-3] representing each + * child's position with respect to its parent. (Note that the latter string may be empty.) + * + *

          For example "4/" represents S2CellId.fromFace(4), and "3/02" represents + * S2CellId.fromFace(3).child(0).child(2). + * + *

          This function is a wrapper for S2CellId.fromDebugString(). + */ + public static S2CellId makeCellIdOrDie(String str) { + S2CellId cellId = makeCellId(str); + Preconditions.checkState(null != cellId, ": str == \"%s\"", str); + return cellId; + } + + /** + * As above, but does not CHECK-fail on invalid input. Returns null if conversion is unsuccessful. + */ + @Nullable + public static S2CellId makeCellId(String str) { + S2CellId cellId = S2CellId.fromDebugString(str); + if (cellId.equals(S2CellId.none())) { + return null; + } + return cellId; + } + + /** + * Parses a comma-separated list of S2CellIds in the format above, and returns the corresponding + * S2CellUnion. (Note that S2CellUnions are automatically normalized by sorting, removing + * duplicates, and replacing groups of 4 child cells by their parent cell.) + */ + public static S2CellUnion makeCellUnionOrDie(String str) { + S2CellUnion cellUnion = makeCellUnion(str); + Preconditions.checkState(null != cellUnion, ": str == \"%s\"", str); + return cellUnion; + } + + /** + * As above, but does not CHECK-fail on invalid input. Returns null if conversion is unsuccessful. + */ + @Nullable + public static S2CellUnion makeCellUnion(String str) { + ArrayList cellIds = new ArrayList<>(); + for (String cellStr : splitString(str, ",")) { + cellStr = cellStr.trim(); + S2CellId cellId = makeCellId(cellStr); + if (null == cellId) { + return null; + } + cellIds.add(cellId); + } + S2CellUnion cellUnion = new S2CellUnion(); + cellUnion.initFromCellIds(cellIds); + return cellUnion; + } + + /** + * Given a string of latitude-longitude coordinates in degrees, returns a newly allocated loop. + * Example of the input format: + * + *

          "-20:150, 10:-120, 0.123:-170.652" + * + *

          The strings "empty" or "full" create an empty or full loop respectively. + */ + public static S2Loop makeLoopOrDie(String str) { + S2Loop loop = makeLoop(str); + Preconditions.checkState(loop != null, ": str == \"%s\"", str); + return loop; + } + + /** + * As above, but does not CHECK-fail on invalid input. Returns null if conversion is unsuccessful. + */ + @Nullable + public static S2Loop makeLoop(String str) { + if (str.equals("empty")) { + return S2Loop.empty(); + } + if (str.equals("full")) { + return S2Loop.full(); + } + List vertices = parsePoints(str); + if (vertices == null) { + return null; + } + return new S2Loop(vertices); + } + + /** Similar to makeLoop(), but returns an S2Polyline rather than an S2Loop. */ + public static S2Polyline makePolylineOrDie(String str) { + S2Polyline polyline = makePolyline(str); + Preconditions.checkState(null != polyline, ": str == \"%s\"", str); + return polyline; + } + + /** + * As above, but does not CHECK-fail on invalid input. Returns null if conversion is unsuccessful. + */ + @Nullable + public static S2Polyline makePolyline(String str) { + List vertices = parsePoints(str); + if (null == vertices) { + return null; + } + return new S2Polyline(vertices); + } + + /** Like makePolyline, but returns an S2LaxPolylineShape instead. */ + @GwtIncompatible("S2LaxPolylineShape") + public static S2LaxPolylineShape makeLaxPolylineOrDie(String str) { + S2LaxPolylineShape laxPolyline = makeLaxPolyline(str); + Preconditions.checkState(null != laxPolyline, ": str == \"%s\"", str); + return laxPolyline; + } + + /** + * As above, but does not CHECK-fail on invalid input. Returns null if conversion is unsuccessful. + */ + @Nullable + @GwtIncompatible("S2LaxPolylineShape") + public static S2LaxPolylineShape makeLaxPolyline(String str) { + List vertices = parsePoints(str); + if (null == vertices) { + return null; + } + return S2LaxPolylineShape.create(vertices); + } + + /** + * Given a sequence of loops separated by semicolons, returns a newly allocated polygon. Loops are + * automatically normalized by inverting them if necessary so that they enclose at most half of + * the unit sphere. (Historically this was once a requirement of polygon loops. It also hides the + * problem that if the user thinks of the coordinates as X:Y rather than LAT:LNG, it yields a loop + * with the opposite orientation.) + * + *

          Examples of the input format: + * + *

          +   *     "10:20, 90:0, 20:30"                                 // one loop
          +   *     "10:20, 90:0, 20:30; 5.5:6.5, -90:-180, -15.2:20.3"  // two loops
          +   *     ""                   // the empty polygon (consisting of no loops)
          +   *     "empty"              // the empty polygon (consisting of no loops)
          +   *     "full"               // the full polygon (consisting of one full loop).
          +   * 
          + */ + public static S2Polygon makePolygonOrDie(String str) { + S2Polygon polygon = makePolygon(str); + Preconditions.checkState(polygon != null, ": str == \"%s\"", str); + return polygon; + } + + /** + * As above, but does not CHECK-fail on invalid input. Returns null if conversion is unsuccessful. + */ + @Nullable + public static S2Polygon makePolygon(String str) { + return internalMakePolygon(str, true); + } + + /** + * Like MakePolygon(), except that it does not normalize loops (i.e., it gives you exactly what + * you asked for). + */ + public static S2Polygon makeVerbatimPolygonOrDie(String str) { + S2Polygon polygon = makeVerbatimPolygon(str); + Preconditions.checkState(polygon != null, ": str == \"%s\"", str); + return polygon; + } + + /** + * As above, but does not CHECK-fail on invalid input. Returns null if conversion is unsuccessful. + */ + @Nullable + public static S2Polygon makeVerbatimPolygon(String str) { + return internalMakePolygon(str, false); + } + + /** + * Parses a string in the same format as MakePolygon, except that loops must be oriented so that + * the interior of the loop is always on the left, and polygons with degeneracies are supported. + * As with MakePolygon, "full" denotes the full polygon, and "" or "empty" denote the empty + * polygon. + */ + @GwtIncompatible("S2LaxPolygonShape") + public static S2LaxPolygonShape makeLaxPolygonOrDie(String str) { + S2LaxPolygonShape laxPolygon = makeLaxPolygon(str); + Preconditions.checkState(null != laxPolygon, ": str == \"%s\"", str); + return laxPolygon; + } + + /** + * As above, but does not CHECK-fail on invalid input. Returns null if conversion is unsuccessful. + */ + @Nullable + @GwtIncompatible("S2LaxPolygonShape") + public static S2LaxPolygonShape makeLaxPolygon(String str) { + List loopStrs = splitString(str, ";"); + List> loops = new ArrayList<>(); + for (String loopStr : loopStrs) { + if (loopStr.equals("full")) { + loops.add(new ArrayList()); + } else if (!loopStr.equals("empty")) { + List points = parsePoints(loopStr); + if (null == points) { + return null; + } + loops.add(points); + } + } + return S2LaxPolygonShape.create(loops); + } + + /** + * Returns a S2ShapeIndex containing the points, polylines, and loops (in the form of a single + * polygon) described by the following format: + * + *

          point1|point2|... # line1|line2|... # polygon1|polygon2|... + * + *

          Examples: + * + *

          +   *     1:2 | 2:3 # #                     // Two points
          +   *     # 0:0, 1:1, 2:2 | 3:3, 4:4 #      // Two polylines
          +   *     # # 0:0, 0:3, 3:0; 1:1, 2:1, 1:2  // Two nested loops (one polygon)
          +   *     5:5 # 6:6, 7:7 # 0:0, 0:1, 1:0    // One of each
          +   *     # # empty                         // One empty polygon
          +   *     # # empty | full                  // One empty polygon, one full polygon
          +   * 
          + * + *

          Loops should be directed so that the region's interior is on the left. Loops can be + * degenerate (they do not need to meet S2Loop requirements). + * + *

          CAVEAT: Because whitespace is ignored, empty polygons must be specified as the string + * "empty" rather than as the empty string (""). + */ + @GwtIncompatible("S2LaxPolylineShape, S2LaxPolygonShape") + public static S2ShapeIndex makeIndexOrDie(String str) { + S2ShapeIndex index = makeIndex(str); + Preconditions.checkState(index != null, ": str == \"%s\"", str); + return index; + } + + /** + * As above, but does not CHECK-fail on invalid input. Returns null if conversion is unsuccessful. + */ + @Nullable + @GwtIncompatible("S2LaxPolylineShape, S2LaxPolygonShape") + public static S2ShapeIndex makeIndex(String str) { + String[] strs = str.split("#", -1); // Here, we want to include empty strings in split. + if (strs.length != 3) { + return null; + } + List points = new ArrayList<>(); + for (String pointStr : splitString(strs[0], "\\|")) { + S2Point point = makePoint(pointStr); + if (point == null) { + return null; + } + points.add(point); + } + + S2ShapeIndex index = new S2ShapeIndex(); + if (!points.isEmpty()) { + index.add(S2Point.Shape.fromList(points)); + } + + for (String lineStr : splitString(strs[1], "\\|")) { + S2LaxPolylineShape laxPolyline = makeLaxPolyline(lineStr); + if (laxPolyline == null) { + return null; + } + index.add(laxPolyline); + } + + for (String polygonStr : splitString(strs[2], "\\|")) { + S2LaxPolygonShape laxPolygon = makeLaxPolygon(polygonStr); + if (laxPolygon == null) { + return null; + } + index.add(laxPolygon); + } + return index; + } + + /** Convert an S2Point to the S2TextFormat string representation documented above. */ + public static String toString(S2Point s2Point) { + StringBuilder out = new StringBuilder(); + appendVertex(s2Point, out); + return out.toString(); + } + + /** Convert an S2LatLng to the S2TextFormat string representation documented above. */ + public static String toString(S2LatLng latlng) { + StringBuilder out = new StringBuilder(); + appendVertex(latlng, out); + return out.toString(); + } + + /** Convert an S2LatLngRect to the S2TextFormat string representation documented above. */ + public static String toString(S2LatLngRect rect) { + StringBuilder out = new StringBuilder(); + appendVertex(rect.lo(), out); + out.append(", "); + appendVertex(rect.hi(), out); + return out.toString(); + } + + /** Convert an S2CellId to the S2TextFormat string representation documented above. */ + public static String toString(S2CellId cellId) { + return cellId.toString(); + } + + /** Convert an S2CellUnion to the S2TextFormat string representation documented above. */ + public static String toString(S2CellUnion cellUnion) { + StringBuilder out = new StringBuilder(); + for (S2CellId cellId : cellUnion) { + if (out.length() > 0) { + out.append(", "); + } + out.append(cellId.toString()); + } + return out.toString(); + } + + /** Convert an S2Loop to the S2TextFormat string representation documented above. */ + public static String toString(S2Loop loop) { + if (loop.isEmpty()) { + return "empty"; + } else if (loop.isFull()) { + return "full"; + } + StringBuilder out = new StringBuilder(); + if (loop.numVertices() > 0) { + appendVertices(loop.vertices(), out); + } + return out.toString(); + } + + /** Convert an S2Polyline to the S2TextFormat string representation documented above. */ + public static String toString(S2Polyline polyline) { + StringBuilder out = new StringBuilder(); + if (polyline.numVertices() > 0) { + appendVertices(polyline.vertices(), out); + } + return out.toString(); + } + + /** Convert an S2Polygon to the S2TextFormat string representation documented above. */ + public static String toString(S2Polygon polygon) { + return toString(polygon, ";\n"); + } + + /** + * Convert an S2 polygon to the S2TextFormat string representation documented above, using the + * given loopSeparator between each loop. Empty and Full polygons are represented as "empty" and + * "full" respectively. + */ + public static String toString(S2Polygon polygon, String loopSeparator) { + if (polygon.isEmpty()) { + return "empty"; + } else if (polygon.isFull()) { + return "full"; + } + StringBuilder out = new StringBuilder(); + for (int i = 0; i < polygon.numLoops(); ++i) { + if (i > 0) { + out.append(loopSeparator); + } + appendVertices(polygon.loop(i).vertices(), out); + } + return out.toString(); + } + + /** Convert a list of S2Points to the S2TextFormat string representation documented above. */ + public static String s2PointsToString(List points) { + StringBuilder out = new StringBuilder(); + appendVertices(points, out); + return out.toString(); + } + + /** Convert a list of S2LatLngs to the S2TextFormat string representation documented above. */ + public static String s2LatLngsToString(List latlngs) { + StringBuilder out = new StringBuilder(); + for (int i = 0; i < latlngs.size(); ++i) { + if (i > 0) { + out.append(", "); + } + appendVertex(latlngs.get(i), out); + } + return out.toString(); + } + + /** Convert an S2LaxPolylineShape to the S2TextFormat string representation documented above. */ + @GwtIncompatible("S2LaxPolylineShape, S2LaxPolygonShape") + public static String toString(S2LaxPolylineShape polyline) { + StringBuilder out = new StringBuilder(); + if (polyline.numVertices() > 0) { + appendVertices(polyline.vertices(), out); + } + return out.toString(); + } + + /** Convert an S2LaxPolygonShape to the S2TextFormat string representation documented above. */ + @GwtIncompatible("S2LaxPolylineShape, S2LaxPolygonShape") + public static String toString(S2LaxPolygonShape polygon) { + return toString(polygon, ";\n"); + } + + /** + * Convert an S2LaxPolygonShape to the S2TextFormat string representation documented above, using + * the given loopSeparator. + */ + @GwtIncompatible("S2LaxPolylineShape, S2LaxPolygonShape") + public static String toString(S2LaxPolygonShape polygon, String loopSeparator) { + StringBuilder out = new StringBuilder(); + for (int i = 0; i < polygon.numChains(); ++i) { + if (i > 0) { + out.append(loopSeparator); + } + int chainLength = polygon.getChainLength(i); + if (chainLength == 0) { + out.append("full"); + } else { + for (int edgeOffset = 0; edgeOffset < chainLength; edgeOffset++) { + appendVertex(polygon.getChainVertex(i, edgeOffset), out); + if (edgeOffset < chainLength - 1) { + out.append(", "); + } + } + } + } + return out.toString(); + } + + /** + * Convert an S2CellUnion to the S2TextFormat string representation documented above. The index + * may contain S2Shapes of any type. Shapes are reordered if necessary so that all point geometry + * (shapes of dimension 0) are first, followed by all polyline geometry, followed by all polygon + * geometry. + */ + public static String toString(S2ShapeIndex index) { + StringBuilder out = new StringBuilder(); + MutableEdge edge = new MutableEdge(); + + for (int dim = 0; dim < 3; ++dim) { + if (dim > 0) { + out.append("#"); + } + int count = 0; + for (S2Shape shape : index.getShapes()) { + if (shape == null || shape.dimension() != dim) { + continue; + } + out.append((count > 0) ? " | " : (dim > 0) ? " " : ""); + for (int i = 0; i < shape.numChains(); ++i, ++count) { + if (i > 0) { + out.append((dim == 2) ? "; " : " | "); + } + if (shape.getChainLength(i) == 0) { + out.append("full"); + } else { + shape.getChainEdge(i, 0, edge); + appendVertex(edge.getStart(), out); + } + int limit = shape.getChainLength(i); + if (dim != 1) { + --limit; + } + for (int edgeOffset = 0; edgeOffset < limit; ++edgeOffset) { + out.append(", "); + shape.getChainEdge(i, edgeOffset, edge); + appendVertex(edge.getEnd(), out); + } + } + } + // Example output: "# #", "0:0 # #", "# # 0:0, 0:1, 1:0" + if (dim == 1 || (dim == 0 && count > 0)) { + out.append(" "); + } + } + return out.toString(); + } + + // Split on the given regexp. Trim whitespace and skip empty strings to produce the result. + private static List splitString(String str, String regexp) { + String[] parts = str.split(regexp); + List result = new ArrayList<>(); + for (String part : parts) { + if (!part.trim().isEmpty()) { + result.add(part.trim()); + } + } + return result; + } + + private static class ParseEntry { + public String key; + public String value; + + public ParseEntry(String k, String v) { + this.key = k; + this.value = v; + } + } + + /** Modeled on the DictionaryParse method of strings/serialize.cc */ + @Nullable + private static List dictionaryParse(String str) { + List items = new ArrayList<>(); + String[] entries = str.split(",", -1); + for (String entry : entries) { + if (entry.trim().isEmpty()) { // skip empty + continue; + } + String[] fields = entry.split(":", -1); + if (fields.length != 2) { // parsing error + return null; + } + items.add(new ParseEntry(fields[0], fields[1])); + } + return items; + } + + @Nullable + private static S2Polygon internalMakePolygon(String str, boolean normalizeLoops) { + if (str.equals("empty")) { + return new S2Polygon(new ArrayList()); // Can't be an ImmutableList, it is clear()ed. + } + List loopStrs = splitString(str, ";"); + List loops = new ArrayList<>(); + for (String loopStr : loopStrs) { + S2Loop loop = makeLoop(loopStr); + if (loop == null) { + return null; + } + // Don't normalize loops that were explicitly specified as "full". + if (normalizeLoops && !loop.isFull()) { + loop.normalize(); + } + loops.add(loop); + } + return new S2Polygon(loops); + } + + private static void appendVertex(S2LatLng ll, StringBuilder out) { + out.append(Platform.formatDouble((ll.latDegrees()))) + .append(':') + .append(Platform.formatDouble((ll.lngDegrees()))); + } + + private static void appendVertex(S2Point p, StringBuilder out) { + appendVertex(new S2LatLng(p), out); + } + + private static void appendVertices(Iterable points, StringBuilder out) { + Iterator i = points.iterator(); + while (i.hasNext()) { + appendVertex(i.next(), out); + if (i.hasNext()) { + out.append(", "); + } + } + } +} diff --git a/core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/UintVectorCoder.java b/core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/UintVectorCoder.java new file mode 100644 index 0000000000..8971588e80 --- /dev/null +++ b/core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/UintVectorCoder.java @@ -0,0 +1,111 @@ +/* + * Copyright 2019 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.mogo.eagle.core.utilcode.geometry; + +import com.google.common.annotations.GwtCompatible; +import com.mogo.eagle.core.utilcode.geometry.PrimitiveArrays.Bytes; +import com.mogo.eagle.core.utilcode.geometry.PrimitiveArrays.Cursor; +import com.mogo.eagle.core.utilcode.geometry.PrimitiveArrays.Longs; +import com.google.common.primitives.Ints; +import java.io.IOException; +import java.io.OutputStream; + +/** An encoder/decoder of {@link Longs}s. */ +@GwtCompatible +class UintVectorCoder implements S2Coder { + + /** An instance of an {@code UintVectorCoder} which encodes/decodes {@code uint32}s. */ + static final UintVectorCoder UINT32 = new UintVectorCoder(Ints.BYTES); + /** An instance of an {@code UintVectorCoder} which encodes/decodes {@code uint64}s. */ + static final UintVectorCoder UINT64 = + new UintVectorCoder(com.google.common.primitives.Longs.BYTES); + + private final int typeBytes; + + private UintVectorCoder(int typeBytes) { + this.typeBytes = typeBytes; + } + + @Override + public void encode(Longs values, OutputStream output) throws IOException { + // The encoding format is as follows: + // + // totalBytes (varint64): (values.size() * typeBytes) | (bytesPerWord - 1) + // array of values.size() elements [bytesPerWord bytes each] + // + // bytesPerWord must be >= 0 so we can encode it in (log2(typeBytes) - 1) bits. + + // oneBits = 1 ensures that bytesPerWord is at least 1. + long oneBits = 1; + for (int i = 0; i < values.length(); i++) { + oneBits |= values.get(i); + } + + // bytesPerWord is the minimum number of bytes required to encode the largest value in values. + // It is computed by dividing the minimum number of bits required to represent the largest + // integer in values by 8 (the division by 8 is the unsigned right shift by 3 bits). + // + // In the expression below, (63 - Long.numberOfLeadingZeros(oneBits)) is equivalent to + // floor(log2(oneBits)). oneBits must be at least 1, so the largest value of + // Long.numberOfLeadingZeros(oneBits) that is possible is 63. + // + // Examples: + // - oneBits = ~0L: The number of leading 0s in oneBits is 0. + // - ((63 - 0) >>> 3) + 1 == 8 bytes per word. + // - oneBits = 4321L: The number of leading 0s in oneBits is 51. + // - ((63 - 51) >>> 3) + 1 == 2 bytes per word. + // - oneBits = 1L: The number of leading 0s in oneBits is 63. + // - ((63 - 63) >>> 3) + 1 == 1 byte per word. + int bytesPerWord = ((63 - Long.numberOfLeadingZeros(oneBits)) >>> 3) + 1; + + // Since totalBytes must be a multiple of typeBytes, and bytesPerWord must be <= totalBytes, + // (bytesPerWord - 1) can be encoded in the last few bits of totalBytes (e.g., if this is a + // uint64 vector, typeBytes is 8, and bytesPerWord can be at most 8). + // + // For example, if typeBytes were 4, then any value of (values.length() * typeBytes) leaves us + // the last 2 bits of totalBytes to encode the number of bytes in each word. Since there are + // 2 bits to work with, and the largest possible bytesPerWord (4) requires 3 bits to encode, we + // subtract 1 from bytesPerWord so the data fits in 2 bits. Note that this only works because + // bytesPerWord cannot be 0 and because typeBytes is a power of 2. + long totalBytes = ((long) values.length() * typeBytes) | (bytesPerWord - 1); + EncodedInts.writeVarint64(output, totalBytes); + for (int i = 0; i < values.length(); i++) { + EncodedInts.encodeUintWithLength(output, values.get(i), bytesPerWord); + } + } + + @Override + public Longs decode(Bytes data, Cursor cursor) { + // See encode for documentation on the encoding format. + int totalBytes = Ints.checkedCast(data.readVarint64(cursor)); + long offset = cursor.position; + int size = totalBytes / typeBytes; + int bytesPerWord = (totalBytes & (typeBytes - 1)) + 1; + cursor.position += size * bytesPerWord; + + return new Longs() { + @Override + public long get(int position) { + return data.readUintWithLength(offset + position * bytesPerWord, bytesPerWord); + } + + @Override + public int length() { + return size; + } + }; + } +} diff --git a/core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/VectorCoder.java b/core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/VectorCoder.java new file mode 100644 index 0000000000..6bf39c168d --- /dev/null +++ b/core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/geometry/VectorCoder.java @@ -0,0 +1,131 @@ +/* + * Copyright 2019 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.mogo.eagle.core.utilcode.geometry; + +import com.google.common.annotations.GwtCompatible; +import com.google.common.annotations.GwtIncompatible; +import com.mogo.eagle.core.utilcode.geometry.PrimitiveArrays.Bytes; +import com.mogo.eagle.core.utilcode.geometry.PrimitiveArrays.Cursor; +import com.mogo.eagle.core.utilcode.geometry.PrimitiveArrays.Longs; +import com.google.common.primitives.ImmutableLongArray; +import com.google.common.primitives.Ints; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.nio.charset.StandardCharsets; +import java.util.AbstractList; +import java.util.List; + +/** An encoder/decoder of {@link List}s. */ +@GwtCompatible +public class VectorCoder implements S2Coder> { + + /** An encoder/decoder of {@code List}. */ + static final VectorCoder BYTE_ARRAY = + new VectorCoder<>( + new S2Coder() { + @Override + public void encode(byte[] value, OutputStream output) throws IOException { + output.write(value); + } + + @Override + public byte[] decode(Bytes data, Cursor cursor) { + byte[] b = new byte[Ints.checkedCast(cursor.remaining())]; + for (int i = 0; i < b.length; i++) { + b[i] = data.get(cursor.position++); + } + return b; + } + }); + + /** An encoder/decoder of {@code List}. */ + static final VectorCoder STRING = + new VectorCoder<>( + new S2Coder() { + @Override + public void encode(String value, OutputStream output) throws IOException { + output.write(value.getBytes(StandardCharsets.UTF_8)); + } + + @Override + public String decode(PrimitiveArrays.Bytes data, Cursor cursor) { + byte[] b = new byte[Ints.checkedCast(cursor.remaining())]; + for (int i = 0; i < b.length; i++) { + b[i] = data.get(cursor.position++); + } + return new String(b, StandardCharsets.UTF_8); + } + }); + + /** + * An encoder/decoder of {@link S2Shape}s, where the shapes use the {@link + * S2TaggedShapeCoder#FAST} encoding. + */ + @GwtIncompatible("S2TaggedShapeCoder") + public static final VectorCoder FAST_SHAPE = new VectorCoder<>(S2TaggedShapeCoder.FAST); + + /** + * An encoder/decoder of {@link S2Shape}s, where the shapes use the {@link + * S2TaggedShapeCoder#COMPACT} encoding. + */ + @GwtIncompatible("S2TaggedShapeCoder") + public static final VectorCoder COMPACT_SHAPE = + new VectorCoder<>(S2TaggedShapeCoder.COMPACT); + + private final S2Coder coder; + + /** + * Constructs a {@code VectorCoder} which encodes/decodes elements with the given {@code coder}. + */ + public VectorCoder(S2Coder coder) { + this.coder = coder; + } + + @Override + public void encode(List values, OutputStream output) throws IOException { + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + ImmutableLongArray.Builder offsetsBuilder = ImmutableLongArray.builder(values.size()); + + for (T value : values) { + coder.encode(value, bos); + offsetsBuilder.add(bos.size()); + } + UintVectorCoder.UINT64.encode(Longs.fromImmutableLongArray(offsetsBuilder.build()), output); + bos.writeTo(output); + } + + @Override + public List decode(PrimitiveArrays.Bytes data, PrimitiveArrays.Cursor cursor) { + Longs offsets = UintVectorCoder.UINT64.decode(data, cursor); + long offset = cursor.position; + cursor.position += (offsets.length() > 0 ? offsets.get(offsets.length() - 1) : 0); + + return new AbstractList() { + @Override + public T get(int position) { + long start = (position == 0) ? 0 : offsets.get(position - 1); + long end = offsets.get(position); + return coder.decode(data, data.cursor(offset + start, offset + end)); + } + + @Override + public int size() { + return Ints.checkedCast(offsets.length()); + } + }; + } +}