wait to finish
This commit is contained in:
@@ -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<String, TrackObj> mMarkersCaches = new ArrayMap<>();
|
||||
|
||||
/**
|
||||
* 过滤后的数据集合
|
||||
*/
|
||||
private final ArrayList<MessagePad.TrackedObject> mFilterTrafficData = new ArrayList<>();
|
||||
|
||||
|
||||
public ArrayList<MessagePad.TrackedObject> filterTrafficData(List<MessagePad.TrackedObject> 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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
// }
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<BigPoint> {
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
*
|
||||
* <p>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.
|
||||
*
|
||||
* <p>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.
|
||||
*
|
||||
* <p>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).
|
||||
*
|
||||
* <p>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.
|
||||
*
|
||||
* <p>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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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<S2Point> 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;
|
||||
}
|
||||
}
|
||||
@@ -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<ParametrizedS2Point>, 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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
*
|
||||
* <p>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.
|
||||
*
|
||||
* <p>Implementations may support arrays > 2GB in size like so:
|
||||
*
|
||||
* <pre>{@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; }
|
||||
* }
|
||||
* }</pre>
|
||||
*/
|
||||
public interface Bytes {
|
||||
/**
|
||||
* Returns the {@code byte} at position {@code position}.
|
||||
*
|
||||
* <p>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}.
|
||||
*
|
||||
* <p>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}.
|
||||
*
|
||||
* <p>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}.
|
||||
*
|
||||
* <p>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}.
|
||||
*
|
||||
* <p>{@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.
|
||||
*
|
||||
* <p>{@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.
|
||||
*
|
||||
* <p>{@code cursor.position} is updated to the index of the first byte following the uint.
|
||||
*
|
||||
* <p>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.
|
||||
*
|
||||
* <p>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}.
|
||||
*
|
||||
* <p>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[]}.
|
||||
*
|
||||
* <p>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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
*
|
||||
* <p>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.
|
||||
*
|
||||
* <p>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.
|
||||
*
|
||||
* <p>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.
|
||||
*
|
||||
* <p>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.
|
||||
*
|
||||
* <p>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}.
|
||||
*
|
||||
* <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.
|
||||
*
|
||||
* <p>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}.
|
||||
*
|
||||
* <p>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}.
|
||||
*
|
||||
* <p>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 + "]";
|
||||
}
|
||||
}
|
||||
@@ -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 k<super>th</super> 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.
|
||||
*
|
||||
* <p>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() + "]";
|
||||
}
|
||||
}
|
||||
@@ -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 + ")";
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
*
|
||||
* <p>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.
|
||||
*
|
||||
* <p>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.
|
||||
*
|
||||
* <p>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:
|
||||
*
|
||||
* <pre>
|
||||
* Robust Adaptive Floating-Point Geometric Predicates
|
||||
* Jonathan Richard Shewchuk
|
||||
* School of Computer Science
|
||||
* Carnegie Mellon University
|
||||
* </pre>
|
||||
*
|
||||
* <p>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;
|
||||
}
|
||||
}
|
||||
@@ -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<S1Angle>, 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.
|
||||
*
|
||||
* <p>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.
|
||||
*
|
||||
* <p>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.
|
||||
*
|
||||
* <p>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 <code>degrees(x).degrees() == x</code>
|
||||
* 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 <code>(this + a)</code>.
|
||||
*/
|
||||
@CheckReturnValue
|
||||
public S1Angle add(S1Angle a) {
|
||||
return new S1Angle(radians + a.radians);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retuns an {@link S1Angle} whose angle is <code>(this - a)</code>.
|
||||
*/
|
||||
@CheckReturnValue
|
||||
public S1Angle sub(S1Angle a) {
|
||||
return new S1Angle(radians - a.radians);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retuns an {@link S1Angle} whose angle is <code>(this * m)</code>.
|
||||
*/
|
||||
@CheckReturnValue
|
||||
public S1Angle mul(double m) {
|
||||
return new S1Angle(radians * m);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retuns an {@link S1Angle} whose angle is <code>(this / d)</code>.
|
||||
*/
|
||||
@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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
*
|
||||
* <p>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<S1ChordAngle>, 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).
|
||||
*
|
||||
* <p>Angles outside the range [0, Pi] are handled as follows:
|
||||
*
|
||||
* <ul>
|
||||
* <li>{@link S1Angle#INFINITY} is mapped to {@link #INFINITY}
|
||||
* <li>negative angles are mapped to {@link #NEGATIVE}
|
||||
* <li>finite angles larger than Pi are mapped to {@link #STRAIGHT}
|
||||
* </ul>
|
||||
*
|
||||
* <p>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.
|
||||
*
|
||||
* <p>Note the following special cases:
|
||||
*
|
||||
* <ul>
|
||||
* <li>NEGATIVE.successor() == ZERO
|
||||
* <li>STRAIGHT.successor() == INFINITY
|
||||
* <li>INFINITY.Successor() == INFINITY
|
||||
* </ul>
|
||||
*/
|
||||
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.
|
||||
*
|
||||
* <p>Note the following special cases:
|
||||
*
|
||||
* <ul>
|
||||
* <li>INFINITY.predecessor() == STRAIGHT
|
||||
* <li>ZERO.predecessor() == NEGATIVE
|
||||
* <li>NEGATIVE.predecessor() == NEGATIVE
|
||||
* </ul>
|
||||
*/
|
||||
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.
|
||||
*
|
||||
* <p>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.
|
||||
*
|
||||
* <p>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:
|
||||
*
|
||||
* <pre>
|
||||
* {@code S1ChordAngle a = new S1ChordAngle(x, y);}
|
||||
* {@code S1ChordAngle a1 = a.plusError(a.getS2PointConstructorMaxError());}
|
||||
* </pre>
|
||||
*
|
||||
* <p>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);
|
||||
}
|
||||
}
|
||||
@@ -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).
|
||||
*
|
||||
* <p>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)).
|
||||
*
|
||||
* <p>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.
|
||||
*
|
||||
* <p>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.
|
||||
*
|
||||
* <p>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.
|
||||
*
|
||||
* <p>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.
|
||||
*
|
||||
* <p>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}.
|
||||
*
|
||||
* <p>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}.
|
||||
*
|
||||
* <p>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);
|
||||
}
|
||||
}
|
||||
@@ -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:
|
||||
*
|
||||
* <p>(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.
|
||||
*
|
||||
* <p>It satisfies the following properties (RCP == robustCrossProd):
|
||||
*
|
||||
* <p>(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().
|
||||
*
|
||||
* <p>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.
|
||||
*
|
||||
* <p>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.
|
||||
*
|
||||
* <p>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).
|
||||
*
|
||||
* <p>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.
|
||||
*
|
||||
* <p>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).
|
||||
*
|
||||
* <p>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() {}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
*
|
||||
* <p>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.
|
||||
*
|
||||
* <p>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.
|
||||
*
|
||||
* <p>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);
|
||||
}
|
||||
}
|
||||
@@ -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 k<sup>th</sup> 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.
|
||||
*
|
||||
* <p>This method is equivalent to the following:
|
||||
*
|
||||
* <pre>
|
||||
* for (pos=0, id=childBegin(); !id.equals(childEnd()); id = id.next(), ++pos) {
|
||||
* children[i].init(id);
|
||||
* }
|
||||
* </pre>
|
||||
*
|
||||
* <p>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'.
|
||||
*
|
||||
* <p>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);
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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<S2CellId> {
|
||||
|
||||
/**
|
||||
* Returns the index of the first element {@code x} such that {@code (x >= target)}, or {@link
|
||||
* #size()} if no such element exists.
|
||||
*
|
||||
* <p>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);
|
||||
}
|
||||
@@ -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<S2CellId>}s. */
|
||||
@GwtCompatible
|
||||
class S2CellIdVectorCoder implements S2Coder<List<S2CellId>> {
|
||||
|
||||
/** An instance of an {@code S2CellIdVectorCoder}. */
|
||||
static final S2CellIdVectorCoder INSTANCE = new S2CellIdVectorCoder();
|
||||
|
||||
@Override
|
||||
public void encode(List<S2CellId> 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;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
*
|
||||
* <p>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.
|
||||
*
|
||||
* <p>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.
|
||||
*
|
||||
* <p>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.
|
||||
*
|
||||
* <p>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:
|
||||
*
|
||||
* <ul>
|
||||
* <li>{@link RangeIterator}: used to seek and iterate over the non-overlapping leaf cell ranges.
|
||||
* <li>{@link NonEmptyRangeIterator}: like RangeIterator, but skips ranges whose contents are
|
||||
* empty.
|
||||
* <li>{@link ContentsIterator}: iterates over the (cellId, label) pairs that intersect a given
|
||||
* range.
|
||||
* <li>{@link CellIterator}: iterates over the entire set of (cellId, label) pairs.
|
||||
* </ul>
|
||||
*
|
||||
* <p>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<CellNode> 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<RangeNode> 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.
|
||||
*
|
||||
* <p>The S2CellIds in the index may overlap (including duplicate values). Duplicate (cellId,
|
||||
* label) pairs are also allowed, although query tools often remove duplicates.
|
||||
*
|
||||
* <p>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<S2CellId> 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.
|
||||
*
|
||||
* <p>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<Delta> 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.
|
||||
*
|
||||
* <p>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<Integer> {
|
||||
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.
|
||||
*
|
||||
* <p>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.
|
||||
*
|
||||
* <p>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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
*
|
||||
* <p>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<S2CellId>, Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private static final byte LOSSLESS_ENCODING_VERSION = 1;
|
||||
|
||||
/** The CellIds that form the Union */
|
||||
private ArrayList<S2CellId> cellIds = new ArrayList<S2CellId>();
|
||||
|
||||
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<S2CellId> cellIds) {
|
||||
initRawCellIds(cellIds);
|
||||
normalize();
|
||||
}
|
||||
|
||||
/** Populates a cell union with the given 64-bit cell ids, and then calls normalize(). */
|
||||
public void initFromIds(List<Long> cellIds) {
|
||||
initRawIds(cellIds);
|
||||
normalize();
|
||||
}
|
||||
|
||||
/**
|
||||
* Populates a cell union with the given S2CellIds. The input list is copied, and then cleared.
|
||||
*/
|
||||
public void initSwap(List<S2CellId> 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<S2CellId> 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<Long> cellIds) {
|
||||
int size = cellIds.size();
|
||||
this.cellIds = new ArrayList<S2CellId>(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.
|
||||
*
|
||||
* <p>The input list is copied, and then cleared.
|
||||
*/
|
||||
public void initRawSwap(List<S2CellId> cellIds) {
|
||||
this.cellIds = new ArrayList<S2CellId>(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.
|
||||
*
|
||||
* <p>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.
|
||||
*
|
||||
* <p>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<S2CellId> iterator() {
|
||||
return cellIds.iterator();
|
||||
}
|
||||
|
||||
/** Direct access to the underlying vector for iteration . */
|
||||
public ArrayList<S2CellId> 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.
|
||||
*
|
||||
* <p>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.
|
||||
*
|
||||
* <p>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.
|
||||
*
|
||||
* <p>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<S2CellId> 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).
|
||||
*
|
||||
* <p>CAVEAT: If you have constructed a valid but non-normalized S2CellUnion, note that groups of
|
||||
* 4 child cells are <em>not</em> 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}.
|
||||
*
|
||||
* <p>CAVEAT: If you have constructed a valid but non-normalized S2CellUnion, note that groups of
|
||||
* 4 child cells are <em>not</em> 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.
|
||||
*
|
||||
* <p><b>Note:</b> {@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.
|
||||
*
|
||||
* <p><b>Note:</b> {@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.)
|
||||
*
|
||||
* <p><b>Note:</b> {@code x} and {@code y} must be sorted.
|
||||
*/
|
||||
public static void getIntersection(List<S2CellId> x, List<S2CellId> y, List<S2CellId> 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<S2CellId> 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).
|
||||
*
|
||||
* <p>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<S2CellId> output = new ArrayList<S2CellId>();
|
||||
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).
|
||||
*
|
||||
* <p>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.
|
||||
*
|
||||
* <p>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.
|
||||
*
|
||||
* <p>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<S2CellId> 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.
|
||||
*
|
||||
* <p>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.
|
||||
*
|
||||
* <p>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;
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
*
|
||||
* <p>Example usage:
|
||||
*
|
||||
* <pre>
|
||||
* void test(List<S2Point> points, List<S2Point> targets) {
|
||||
* // The template argument allows auxiliary data to be attached to each point (in this case, the
|
||||
* // array index).
|
||||
* S2PointIndex<Integer> index = new S2PointIndex<>();
|
||||
* for (int i = 0; i < points.size(); i++) {
|
||||
* index.add(points.get(i), i);
|
||||
* }
|
||||
* S2ClosestPointQuery<Integer> query = new S2ClosestPointQuery<>(index);
|
||||
* query.setMaxPoints(15);
|
||||
* for (S2Point target : targets) {
|
||||
* for (Result<Integer> 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());
|
||||
* }
|
||||
* }
|
||||
* }</pre>
|
||||
*
|
||||
* <p>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));}.
|
||||
*
|
||||
* <p>You can also restrict the results to an arbitrary S2Region via {@link #setRegion(S2Region)}.
|
||||
*
|
||||
* <p>The implementation is designed to be very fast for both small and large point sets.
|
||||
*
|
||||
* <p>This class is not thread-safe. In particular, setters must not be called during queries.
|
||||
*/
|
||||
@GwtCompatible
|
||||
public final class S2ClosestPointQuery<T> {
|
||||
// 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<T> 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<S2CellId> indexCovering = Lists.newArrayList();
|
||||
|
||||
/** Unprocessed cells for the current query being processed. */
|
||||
private final PriorityQueue<QueueEntry> queue = new PriorityQueue<>();
|
||||
|
||||
/** The iterator for the last-known state of the index. New instance built by {@link #reset()}. */
|
||||
private S2Iterator<Entry<T>> iter;
|
||||
|
||||
/** The covering of {@link #indexCovering}. Type is ArrayList due to {@link S2RegionCoverer}. */
|
||||
private ArrayList<S2CellId> regionCovering = Lists.newArrayList();
|
||||
|
||||
/** The covering of {@link #maxDistance}. Type is ArrayList due to {@link S2RegionCoverer}. */
|
||||
private final ArrayList<S2CellId> maxDistanceCovering = Lists.newArrayList();
|
||||
|
||||
/** The intersection between the index and {@link #regionCovering}. */
|
||||
private final List<S2CellId> intersectionWithRegion = Lists.newArrayList();
|
||||
|
||||
/** The intersection between the index and {@link #maxDistance}. */
|
||||
private final List<S2CellId> intersectionWithMaxDistance = Lists.newArrayList();
|
||||
|
||||
/** Temporary storage for index entries that are of interest during query processing. */
|
||||
@SuppressWarnings({"rawtypes", "unchecked"})
|
||||
private final Entry<T>[] tmpPoints = new Entry[MAX_LEAF_POINTS];
|
||||
|
||||
/** Temporary queue of results sorted in descending order. */
|
||||
private final PriorityQueue<Result<T>> 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.
|
||||
*
|
||||
* <p>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<T> 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<T> 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.
|
||||
*
|
||||
* <p>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.
|
||||
*
|
||||
* <p>This is package private, as it is intended only for testing, benchmarking, and debugging.
|
||||
*
|
||||
* <p>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<Result<T>> toList(List<Result<T>> 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.<Result<T>>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<Result<T>> 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<Result<T>> 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<T> 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<Result<T>> 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<Result<T>> 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<Entry<T>> nextIt = iter.copy();
|
||||
S2CellId indexNext = nextIt.id();
|
||||
S2Iterator<Entry<T>> 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<Entry<T>> 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<T> 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<S2CellId> 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<Entry<T>> 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<ChordComparable> {
|
||||
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<T> extends ChordComparable {
|
||||
private final Entry<T> pointData;
|
||||
|
||||
private Result(S1ChordAngle distance, Entry<T> pointData) {
|
||||
super(distance);
|
||||
this.pointData = pointData;
|
||||
}
|
||||
|
||||
public Entry<T> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
*
|
||||
* <p>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<T> {
|
||||
|
||||
/** 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);
|
||||
}
|
||||
@@ -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.
|
||||
*
|
||||
* <p>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.
|
||||
*
|
||||
* <p>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<S2ShapeIndex.Cell> 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<S2Shape> {}
|
||||
|
||||
/**
|
||||
* 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<S2Shape> 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<S2Shape>() {
|
||||
@Override
|
||||
public Iterator<S2Shape> iterator() {
|
||||
return new AbstractIterator<S2Shape>() {
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -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).
|
||||
*
|
||||
* <p>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.
|
||||
*
|
||||
* <p>This class is not thread-safe.
|
||||
*/
|
||||
@GwtCompatible
|
||||
public class S2ContainsVertexQuery {
|
||||
private final S2Point target;
|
||||
private final List<S2Point> outgoing = new ArrayList<>();
|
||||
private final List<S2Point> 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<S2Point> 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;
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
*
|
||||
* <p>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.
|
||||
*
|
||||
* <p>Containment of input geometry is defined as follows:
|
||||
*
|
||||
* <ul>
|
||||
* <li>Each input loop and polygon is contained by the convex hull exactly (i.e., according to
|
||||
* S2Polygon.contains(S2Polygon)).
|
||||
* <li>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.)
|
||||
* <li>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.)
|
||||
* </ul>
|
||||
*
|
||||
* <p>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).
|
||||
*
|
||||
* <p>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<S2Point> 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.
|
||||
*
|
||||
* <p>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.
|
||||
*
|
||||
* <p>If there is no geometry, this method returns an empty loop containing no points (see
|
||||
* S2Loop.isEmpty()).
|
||||
*
|
||||
* <p>If the geometry spans more than half of the sphere, this method returns a full loop
|
||||
* containing the entire sphere (see S2Loop.isFull()).
|
||||
*
|
||||
* <p>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).
|
||||
*
|
||||
* <p>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<S2Point> 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<S2Point> lower = getMonotoneChain(points);
|
||||
List<S2Point> 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<S2Point> {
|
||||
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<S2Point> getMonotoneChain(List<S2Point> points) {
|
||||
List<S2Point> 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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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 <code>edges</code>. */
|
||||
private long[] cells;
|
||||
|
||||
/** The edge contained by each cell, as given in the parallel array <code>cells</code>. */
|
||||
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<Long> cellList = Lists.newArrayList();
|
||||
List<Integer> edgeList = Lists.newArrayList();
|
||||
for (int i = 0; i < getNumEdges(); ++i) {
|
||||
S2Point from = edgeFrom(i);
|
||||
S2Point to = edgeTo(i);
|
||||
ArrayList<S2CellId> 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 <code>cells</code> and <code>edges</code> 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<Integer>() {
|
||||
@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.
|
||||
*
|
||||
* <p>The value 'n' is the number of iterators we expect to request from this edge index.
|
||||
*
|
||||
* <p>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.
|
||||
*
|
||||
* <p>If we compute the index, the cost becomes: m * costInsert + n * costFind(m)
|
||||
*
|
||||
* <ul>
|
||||
* <li>costInsert can be expected to be reasonably stable, and was measured at 1200ns with the
|
||||
* BM_QuadEdgeInsertionCost benchmark.
|
||||
* <li>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.
|
||||
* </ul>
|
||||
*
|
||||
* <p>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.
|
||||
*
|
||||
* <p>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<Integer> candidateCrossings) {
|
||||
Preconditions.checkState(indexComputed);
|
||||
ArrayList<S2CellId> 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<Integer> uniqueSet = new HashSet<Integer>();
|
||||
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).
|
||||
*
|
||||
* <p>If thickenEdge is true, the edge is thickened and extended by 1% of its length.
|
||||
*
|
||||
* <p>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<S2CellId> 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 <code>
|
||||
* O(log N)</code> 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<S2CellId> cover, Set<Integer> candidateCrossings) {
|
||||
// Find all parent cells of covering cells.
|
||||
Set<S2CellId> 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<S2CellId> cover, Set<Integer> 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<ParametrizedS2Point> 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.
|
||||
*
|
||||
* <p>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<Integer> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
*
|
||||
* <p>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:
|
||||
*
|
||||
* <pre>
|
||||
* void test(Collection<S2Polyline> 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<S2Shape, Edges> results = query.getCrossings(a, b);
|
||||
* for (Map.Entry<S2Shape, Edges> 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);
|
||||
* }
|
||||
* }
|
||||
* }
|
||||
* </pre>
|
||||
*
|
||||
* <p>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.
|
||||
*
|
||||
* <p>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<S2ShapeIndex.Cell> cells;
|
||||
/** The following vectors are temporary storage used while processing a query. */
|
||||
private final S2Iterator<S2ShapeIndex.Cell> 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<S2Shape, Edges> getCrossings(final S2Point a, final S2Point b) {
|
||||
Map<S2Shape, Edges> candidates = getCandidates(a, b);
|
||||
Map<S2Shape, Edges> results = Maps.newIdentityHashMap();
|
||||
for (Map.Entry<S2Shape, Edges> 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.
|
||||
*
|
||||
* <p>CAVEAT: This method may return shapes that have an empty set of candidate edges, i.e. {@code
|
||||
* result.get(shape).isEmpty() == true}.
|
||||
*/
|
||||
public Map<S2Shape, Edges> 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.<S2Shape, Edges>emptyMap();
|
||||
} else {
|
||||
return Collections.<S2Shape, Edges>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.<S2Shape, Edges>emptyMap();
|
||||
} else {
|
||||
S2Shape shape = cell.clipped(0).shape();
|
||||
return Collections.<S2Shape, Edges>singletonMap(shape, new SimpleEdges(clippedShape));
|
||||
}
|
||||
}
|
||||
Map<S2Shape, Edges> 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<S2Shape, Edges> 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<S2ShapeIndex.Cell> 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<S2ShapeIndex.Cell> 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}.
|
||||
*
|
||||
* <p>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<Stepper> steppers = new PriorityQueue<Stepper>();
|
||||
/**
|
||||
* 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<Stepper> {
|
||||
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());
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
*
|
||||
* <p>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<R2Vector> 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<R2Vector> 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<S2Point> makeVertices(Matrix3x3 frame, S1Angle nominalRadius) {
|
||||
List<R2Vector> r2Vertices = Lists.newArrayList();
|
||||
getR2Vertices(r2Vertices);
|
||||
List<S2Point> 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;
|
||||
}
|
||||
}
|
||||
@@ -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<T> with a Multimap<long,T> that is space efficient, and supports
|
||||
// time-efficient inserts, removes, and lookups.
|
||||
@GwtCompatible
|
||||
public final class S2Iterator<T extends S2Iterator.Entry> {
|
||||
/** 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 <T extends Entry> S2Iterator<T> create(List<T> entries) {
|
||||
return new S2Iterator<T>(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 <T extends Entry> S2Iterator<T> create(
|
||||
List<T> entries, Function<S2CellId, Integer> seekFunction) {
|
||||
return new S2Iterator<T>(entries, seekFunction);
|
||||
}
|
||||
|
||||
/** Creates a new iterator with the same entries and position as {@code it}. */
|
||||
static <T extends Entry> S2Iterator<T> copy(S2Iterator<T> it) {
|
||||
S2Iterator<T> copy = new S2Iterator<T>(it.entries, it.seekFunction);
|
||||
copy.pos = it.pos;
|
||||
return copy;
|
||||
}
|
||||
|
||||
private final List<T> entries;
|
||||
private final Function<S2CellId, Integer> 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<T> 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<T> entries, Function<S2CellId, Integer> seekFunction) {
|
||||
this.entries = entries;
|
||||
this.seekFunction = seekFunction;
|
||||
}
|
||||
|
||||
/** Returns a copy of this iterator, positioned as this iterator is. */
|
||||
public S2Iterator<T> copy() {
|
||||
S2Iterator<T> it = new S2Iterator<T>(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 <T extends Entry> boolean equalIterators(S2Iterator<T> 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.
|
||||
*
|
||||
* <p>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:
|
||||
*
|
||||
* <ul>
|
||||
* <li>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.
|
||||
* <li>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.
|
||||
* <li>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<T> it) {
|
||||
// assert index == it.index;
|
||||
pos = it.pos;
|
||||
}
|
||||
}
|
||||
@@ -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}.
|
||||
*
|
||||
* <ul>
|
||||
* <li>Latitude is clipped to the range {@code [-90, 90]}
|
||||
* <li>Longitude is normalized to be in the range {@code [-180, 180]}
|
||||
* </ul>
|
||||
*
|
||||
* <p>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
|
||||
* <em>valid</em>.
|
||||
*/
|
||||
@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 <em>valid</em>.
|
||||
*/
|
||||
@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 <em>valid</em>.
|
||||
*/
|
||||
@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() + ")";
|
||||
}
|
||||
}
|
||||
@@ -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()}.
|
||||
*
|
||||
* <p>Note that the latitude-longitude space is considered to have a <strong>cylindrical</strong>
|
||||
* 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.
|
||||
*
|
||||
* <p>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.
|
||||
*
|
||||
* <p>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):
|
||||
*
|
||||
* <p>center = (80, 170), size = (40, 60) -> lat = [60, 100], lng = [140, -160]
|
||||
*
|
||||
* <p>center = (10, 40), size = (210, 400) -> lat = [-90, 90], lng = [-180, 180]
|
||||
*
|
||||
* <p>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.
|
||||
*
|
||||
* <p>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.
|
||||
*
|
||||
* <p>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.
|
||||
*
|
||||
* <p>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:
|
||||
*
|
||||
* <ul>
|
||||
* <li>The full and empty rectangles have no boundary on the sphere. Any expansion (positive or
|
||||
* negative) of these rectangles leaves them unchanged.
|
||||
* <li>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.
|
||||
* <li>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.
|
||||
* <li>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).
|
||||
* </ul>
|
||||
*
|
||||
* <p>Expansion and contraction are defined such that they are inverses whenever possible, i.e.
|
||||
*
|
||||
* <p>{@code rect.expandedByDistance(x).expandedByDistance(-x) == rect}
|
||||
*
|
||||
* <p>(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.
|
||||
*
|
||||
* <p>Example usage:
|
||||
*
|
||||
* <p>{@code S2LatLngRect union(List<S2LatLng> 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.
|
||||
*
|
||||
* <p>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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 k<super>th</super> 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() + "]";
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
*
|
||||
* <p>This is similar to {@link S2Polygon#shape}, except that this class supports polygons with two
|
||||
* types of degeneracy:
|
||||
*
|
||||
* <ol>
|
||||
* <li>Degenerate edges (from a vertex to itself)
|
||||
* <li>Sibling edge pairs (consisting of two oppositely oriented edges)
|
||||
* </ol>
|
||||
*
|
||||
* <p>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.
|
||||
*
|
||||
* <p>Loops with fewer than three vertices are interpreted as follows:
|
||||
*
|
||||
* <ul>
|
||||
* <li>A loop with two vertices defines two edges (in opposite directions).
|
||||
* <li>A loop with one vertex defines a single degenerate edge.
|
||||
* <li>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.
|
||||
* </ul>
|
||||
*
|
||||
* <p>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:
|
||||
*
|
||||
* <ul>
|
||||
* <li>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).
|
||||
* <li>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:
|
||||
* <ul>
|
||||
* <li>{AA, AA} // degenerate edge coincides with another edge
|
||||
* <li>{AA, AB} // degenerate edge coincides with another edge
|
||||
* <li>{AB, BA, AB} // sibling pair coincides with another edge
|
||||
* </ul>
|
||||
* </ul>
|
||||
*
|
||||
* <p>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:
|
||||
*
|
||||
* <ol>
|
||||
* <li>{@link #create Standard} polygons copy points into one {@link ImmutableList immutable
|
||||
* list}, which requires 48 bytes per vertex on a standard 64-bit JVM.
|
||||
* <li>{@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.
|
||||
* <li>{@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.
|
||||
* <ol>
|
||||
*/
|
||||
@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.
|
||||
*
|
||||
* <p>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.
|
||||
*
|
||||
* <p>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<? extends Iterable<S2Point>> 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<? extends Iterable<S2Point>> 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<? extends Iterable<S2CellId>> 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<S2Point> vertices) {
|
||||
super(vertices);
|
||||
}
|
||||
}
|
||||
|
||||
/** A simple polygon with vertices referenced from a {@link List<S2Point>}. */
|
||||
static class SimpleList extends ChainAspect.Simple implements S2LaxPolygonShape {
|
||||
private final List<S2Point> vertices;
|
||||
|
||||
private SimpleList(List<S2Point> 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<S2Point> 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<S2CellId> vertices) {
|
||||
super(vertices);
|
||||
}
|
||||
}
|
||||
|
||||
/** A multi polygon with points referenced from an array. */
|
||||
static class MultiArray extends ChainAspect.Multi.Array implements S2LaxPolygonShape {
|
||||
MultiArray(Iterable<? extends Iterable<S2Point>> loops) {
|
||||
super(loops);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A multi polygon with vertices referenced from a {@link List<S2Point>}, and cumulative edges
|
||||
* referenced from an {@link Longs}.
|
||||
*/
|
||||
static class MultiList extends ChainAspect.Multi implements S2LaxPolygonShape {
|
||||
private final List<S2Point> vertices;
|
||||
|
||||
private MultiList(List<S2Point> 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<? extends Iterable<S2Point>> 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<? extends Iterable<S2CellId>> loops) {
|
||||
super(loops);
|
||||
}
|
||||
}
|
||||
|
||||
/** An encoder/decoder of {@link S2LaxPolygonShape}s. */
|
||||
@GwtIncompatible("Uses ByteBuffer")
|
||||
class Coder implements S2Coder<S2LaxPolygonShape> {
|
||||
|
||||
/**
|
||||
* 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<List<S2Point>> coder;
|
||||
|
||||
private Coder(S2Coder<List<S2Point>> 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<S2Point> 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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}.
|
||||
*
|
||||
* <p>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<S2Point> 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<S2Point> 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<S2CellId> 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<? extends Iterable<S2Point>> 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<? extends Iterable<S2Point>> 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<? extends Iterable<S2CellId>> 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 <T> Iterable<T> filterLine(Iterable<T> input) {
|
||||
return Iterables.size(input) < 2 ? ImmutableList.of() : input;
|
||||
}
|
||||
|
||||
static <T> Iterable<? extends Iterable<T>> filterLines(Iterable<? extends Iterable<T>> 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<S2Point> vertices) {
|
||||
super(vertices);
|
||||
}
|
||||
}
|
||||
|
||||
/** A polyline storing {@link S2Point}s in a {@link List<S2Point>}. */
|
||||
static class SimpleList extends ChainAspect.Simple implements S2LaxPolylineShape {
|
||||
private final List<S2Point> vertices;
|
||||
|
||||
private SimpleList(List<S2Point> 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<S2Point> 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<S2CellId> 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<? extends Iterable<S2Point>> 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<? extends Iterable<S2Point>> 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<? extends Iterable<S2CellId>> chains) {
|
||||
super(chains);
|
||||
}
|
||||
}
|
||||
|
||||
/** An encoder/decoder of {@link S2LaxPolylineShape}s. */
|
||||
@GwtIncompatible("Uses EncodedS2PointVector")
|
||||
class Coder implements S2Coder<S2LaxPolylineShape> {
|
||||
|
||||
/**
|
||||
* 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<List<S2Point>> coder;
|
||||
|
||||
private Coder(S2Coder<List<S2Point>> 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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.
|
||||
*
|
||||
* <p>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.
|
||||
*
|
||||
* <p>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".
|
||||
*
|
||||
* <p>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));
|
||||
}
|
||||
}
|
||||
@@ -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<S2Point>, 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))}.
|
||||
*
|
||||
* <p>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.
|
||||
*
|
||||
* <p>Equivalent to {@code a.sub(b).norm()}, but significantly faster.
|
||||
*
|
||||
* <p>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.
|
||||
*
|
||||
* <p>Equivalent to {@code getDistance(that)<sup>2</sup>}, but significantly faster.
|
||||
*
|
||||
* <p>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.
|
||||
*
|
||||
* <p>This class is useful for adding a collection of points to an S2ShapeIndex.
|
||||
*/
|
||||
public abstract static class Shape extends AbstractList<S2Point>
|
||||
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<S2Point> 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<Shape> {
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
*
|
||||
* <p>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<S2Point> points, int level, OutputStream output)
|
||||
throws IOException {
|
||||
encodePointsCompressed(points, level, new LittleEndianOutput(output));
|
||||
}
|
||||
|
||||
static void encodePointsCompressed(List<S2Point> 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<Integer> 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}.
|
||||
*
|
||||
* <p>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<S2Point> decodePointsCompressed(int numVertices, int level, InputStream input)
|
||||
throws IOException {
|
||||
return decodePointsCompressed(numVertices, level, new LittleEndianInput(input));
|
||||
}
|
||||
|
||||
static List<S2Point> decodePointsCompressed(int numVertices, int level, LittleEndianInput decoder)
|
||||
throws IOException {
|
||||
List<S2Point> vertices = new ArrayList<>(numVertices);
|
||||
|
||||
FaceRunCoder faces = new FaceRunCoder();
|
||||
faces.decode(numVertices, decoder);
|
||||
Iterator<Integer> 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<FaceRun> 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<Integer> getFaceIterator() {
|
||||
final Iterator<FaceRun> faceRunIterator = faces.iterator();
|
||||
// Special case if there are not faces at all.
|
||||
if (!faceRunIterator.hasNext()) {
|
||||
return Collections.emptyIterator();
|
||||
}
|
||||
return new Iterator<Integer>() {
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
*
|
||||
* <p>The class supports adding or removing points dynamically, and provides a seekable iterator
|
||||
* interface for navigating the index.
|
||||
*
|
||||
* <p>You can use this class in conjunction with {@link S2ClosestPointQuery} to find the closest
|
||||
* index points to a given query point. For example:
|
||||
*
|
||||
* <pre>
|
||||
* void test(List<S2Point> 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<Integer> index = new S2PointIndex();
|
||||
* for (int i = 0; i < points.size(); i++) {
|
||||
* index.add(points.get(i), i);
|
||||
* }
|
||||
* S2ClosestPointQuery<Integer> 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));
|
||||
* }
|
||||
* }
|
||||
* </pre>
|
||||
*
|
||||
* <p>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":
|
||||
*
|
||||
* <pre>
|
||||
* S2Iterator<S2PointIndex.Entry<Integer>> it = index.iterator();
|
||||
* it.seek(targetId.rangeMin());
|
||||
* for (; !it.done() && it.compareTo(targetId.rangeMax()) <= 0; it.next()) {
|
||||
* doSomething(it.entry());
|
||||
* }
|
||||
* </pre>
|
||||
*
|
||||
* <p>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.
|
||||
*
|
||||
* <p>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<Data> {
|
||||
private final List<Entry<Data>> 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<Entry<Data>> 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<Data> 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<Data> 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 <Data> Entry<Data> 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.
|
||||
*
|
||||
* <p>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 <strong>not</strong> consistent with
|
||||
* equals.
|
||||
*/
|
||||
public static class Entry<Data> implements S2Iterator.Entry, Comparable<Entry<Data>> {
|
||||
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<Data> other) {
|
||||
return UnsignedLongs.compare(id, other.id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return new S2LatLng(point) + ": " + data;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<S2PointRegion>, 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));
|
||||
}
|
||||
}
|
||||
@@ -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<S2Point>}s.
|
||||
*
|
||||
* <p>Values from the {@link List<S2Point>} 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<List<S2Point>> {
|
||||
/**
|
||||
* 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<S2Point> values, OutputStream output) throws IOException {
|
||||
switch (type) {
|
||||
case FAST:
|
||||
encodeFast(values, output);
|
||||
break;
|
||||
case COMPACT:
|
||||
encodeCompact(values, output);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<S2Point> 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<S2Point> 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<S2Point> 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<S2Point>() {
|
||||
@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.
|
||||
*
|
||||
* <p><strong>OVERVIEW</strong>
|
||||
*
|
||||
* <p>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.
|
||||
*
|
||||
* <p>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).
|
||||
*
|
||||
* <p>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.
|
||||
*
|
||||
* <p>Each encodable point is first converted to the (sj, tj) representation defined below:
|
||||
*
|
||||
* <pre>{@code
|
||||
* sj = (((face & 3) << 30) | (si >> 1)) >> (30 - level);
|
||||
* tj = (((face & 4) << 29) | ti) >> (31 - level);
|
||||
* }</pre>
|
||||
*
|
||||
* 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".
|
||||
*
|
||||
* <p>We then combine (sj, tj) into one 64-bit value by interleaving bit pairs:
|
||||
*
|
||||
* <pre>{@code
|
||||
* v = interleaveBitPairs(sj, tj);
|
||||
* }</pre>
|
||||
*
|
||||
* (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.
|
||||
*
|
||||
* <p>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:
|
||||
*
|
||||
* <pre>{@code
|
||||
* v[i,j] = base + offset[i] + delta[i, j]
|
||||
* }</pre>
|
||||
*
|
||||
* <p>where "i" represents a block and "j" represents an entry in that block.
|
||||
*
|
||||
* <p>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.
|
||||
*
|
||||
* <p>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).
|
||||
*
|
||||
* <p>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.
|
||||
*
|
||||
* <p>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:
|
||||
*
|
||||
* <pre>{@code
|
||||
* Base: 1111111100000000-----------------
|
||||
* Offset: -------------1111111100000000----
|
||||
* Delta: -------------------------00000000
|
||||
* Overlap: ^^^^
|
||||
* }</pre>
|
||||
*
|
||||
* <p>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.
|
||||
*
|
||||
* <p>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.
|
||||
*
|
||||
* <p><strong>ENCODING DETAILS</strong></br>
|
||||
*
|
||||
* <p>Now we can move on to the actual encodings. First, there is a 2 byte header encoded as
|
||||
* follows:
|
||||
*
|
||||
* <pre>{@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)
|
||||
* }</pre>
|
||||
*
|
||||
* <p>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
|
||||
*
|
||||
* <pre>
|
||||
* num_values == BLOCK_SIZE * (numBlocks - 1) + lastBlockSize
|
||||
* </pre>
|
||||
*
|
||||
* <p>(An empty vector has numBlocks == 0 and lastBlockSize == BLOCK_SIZE.)
|
||||
*
|
||||
* <p>Each block starts with a 1 byte header containing the following:
|
||||
*
|
||||
* <pre>{@code
|
||||
* Byte 0, bits 0-2: (offsetBytes - overlapNibbles)
|
||||
* Byte 0, bit 3: overlapNibbles
|
||||
* Byte 0, bits 4-7: (deltaNibbles - 1)
|
||||
* }</pre>
|
||||
*
|
||||
* <p>"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).
|
||||
*
|
||||
* <p>The header is followed by "offsetBytes" bytes for the offset, and then (4 * deltaNibbles)
|
||||
* bytes for the deltas.
|
||||
*
|
||||
* <p>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.
|
||||
*
|
||||
* <p>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.
|
||||
*
|
||||
* <p>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<S2Point> 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<CellPoint> 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<byte[]> blocks = new ArrayList<>();
|
||||
List<S2Point> 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<S2Point> 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<S2Point>() {
|
||||
@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.
|
||||
*
|
||||
* <p>(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.
|
||||
*
|
||||
* <p>Adds the {@code S2CellId} representation of each point (if any) to {@code cellPoints}.
|
||||
*/
|
||||
private static int chooseBestLevel(List<S2Point> points, List<CellPoint> 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<CellPoint> 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);
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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.
|
||||
*
|
||||
* <p>Some of the situations this class was designed to handle:
|
||||
*
|
||||
* <ol>
|
||||
* <li>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.
|
||||
* <li>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.
|
||||
* <li>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.
|
||||
* <li>As a tool for implementing other polygon operations by generating a collection of directed
|
||||
* edges and then assembling them into loops.
|
||||
* </ol>
|
||||
*
|
||||
* 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<S2Point, Multiset<S2Point>> 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<S2Point> 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.
|
||||
*
|
||||
* <p>Examples:
|
||||
*
|
||||
* <pre>{@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);
|
||||
* }</pre>
|
||||
*/
|
||||
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).
|
||||
*
|
||||
* <p>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.
|
||||
*
|
||||
* <p>There are only two reasons to turn off {@code xorEdges} (via {@link Builder#setXorEdges}):
|
||||
*
|
||||
* <ol>
|
||||
* <li>{@link #assemblePolygon} will be called, and you want to assert that there are no
|
||||
* duplicate edge pairs in the input.
|
||||
* <li>{@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).
|
||||
* </ol>
|
||||
*/
|
||||
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}.
|
||||
*
|
||||
* <p>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.
|
||||
*
|
||||
* <p>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}.
|
||||
*
|
||||
* <p>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++.
|
||||
*
|
||||
* <p>Default value: false
|
||||
*/
|
||||
public boolean getSnapToCellCenters() {
|
||||
return snapToCellCenters;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the edge splice fraction, which defaults to 0.866 (approximately {@code sqrt(3)/2}).
|
||||
*
|
||||
* <p>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.
|
||||
*
|
||||
* <p>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.
|
||||
*
|
||||
* <p>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.
|
||||
*
|
||||
* <p>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}.
|
||||
*
|
||||
* <p>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}.
|
||||
*
|
||||
* <p>Default: false
|
||||
*/
|
||||
public Builder setUndirectedEdges(boolean undirectedEdges) {
|
||||
this.undirectedEdges = undirectedEdges;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets whether duplicated edges will be collapsed. See {@link Options#getXorEdges}.
|
||||
*
|
||||
* <p>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}.
|
||||
*
|
||||
* <p>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}.
|
||||
*
|
||||
* <p>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()}.
|
||||
*
|
||||
* <p>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}.
|
||||
*
|
||||
* <p>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.
|
||||
*
|
||||
* <p>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.
|
||||
*
|
||||
* <p>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.
|
||||
*
|
||||
* <p>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.<S2Point>create());
|
||||
startingVertices.add(v0);
|
||||
}
|
||||
|
||||
edges.get(v0).add(v1);
|
||||
if (options.getUndirectedEdges()) {
|
||||
if (edges.get(v1) == null) {
|
||||
edges.put(v1, HashMultiset.<S2Point>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.
|
||||
*
|
||||
* <p>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<S2Point> 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.
|
||||
*
|
||||
* <p>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.
|
||||
*
|
||||
* <p>This method resets the {@link S2PolygonBuilder} state so that it can be reused.
|
||||
*/
|
||||
public boolean assembleLoops(List<S2Loop> loops, @Nullable List<S2Edge> unusedEdges) {
|
||||
if (options.getMergeDistance().radians() > 0) {
|
||||
S1Angle mergeDistance = options.getMergeDistance();
|
||||
Map<S2Point, S2Point> 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<S2Point> 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<S2Point>, 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.
|
||||
*
|
||||
* <p>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).
|
||||
*
|
||||
* <p>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<S2Edge> unusedEdges) {
|
||||
List<S2Loop> 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<S2Point> 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<S2Point> 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<S2Edge> unusedEdges) {
|
||||
// The path so far.
|
||||
List<S2Point> path = Lists.newArrayList();
|
||||
|
||||
// Maps a vertex to its index in "path".
|
||||
Map<S2Point, Integer> 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<S2Point> 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<S2Edge> 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<S2Point> v, int n, List<S2Edge> 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<S2Point, S2Point> 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<S2Edge> edgesCopy = Lists.newArrayList();
|
||||
for (Map.Entry<S2Point, Multiset<S2Point>> edge : this.edges.entrySet()) {
|
||||
S2Point v0 = edge.getKey();
|
||||
Multiset<S2Point> 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<Void> index() {
|
||||
S2PointIndex<Void> index = new S2PointIndex<>();
|
||||
for (Map.Entry<S2Point, Multiset<S2Point>> 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<S2Point, S2Point> 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<S2Point, S2Point> mergeMap = Maps.newHashMap();
|
||||
Stack<S2Point> frontier = new Stack<S2Point>();
|
||||
List<Result<Void>> mergeable = Lists.newArrayList();
|
||||
S2PointIndex<Void> index = index();
|
||||
S2ClosestPointQuery<Void> query = new S2ClosestPointQuery<>(index);
|
||||
query.setMaxDistance(snapDistance);
|
||||
for (S2Iterator<S2PointIndex.Entry<Void>> 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<Void> 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<S2Point> 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<S2Edge> pendingEdges = Lists.newArrayList();
|
||||
for (Map.Entry<S2Point, Multiset<S2Point>> 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<Void> query = new S2ClosestPointQuery<>(index());
|
||||
query.setMaxDistance(spliceDistance);
|
||||
List<Result<Void>> 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<Void> 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
*
|
||||
* <p>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<S2Point> 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<S2Point> 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<S2Point> 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.
|
||||
*
|
||||
* <p>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).
|
||||
*
|
||||
* <p>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.
|
||||
*
|
||||
* <p>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<S2Point> 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.
|
||||
*
|
||||
* <p>Some useful properties of the algorithm:
|
||||
*
|
||||
* <ul>
|
||||
* <li>It runs in linear time.
|
||||
* <li>The output is always a valid polyline. In particular, adjacent output vertices are never
|
||||
* identical or antipodal.
|
||||
* <li>The method is not optimal, but it tends to produce 2-3% fewer vertices than the
|
||||
* Douglas-Peucker algorithm with the same tolerance.
|
||||
* <li>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.
|
||||
* </ul>
|
||||
*/
|
||||
public S2Polyline subsampleVertices(S1Angle tolerance) {
|
||||
if (vertices.length == 0) {
|
||||
return this;
|
||||
}
|
||||
List<S2Point> 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}.
|
||||
*
|
||||
* <p>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.
|
||||
*
|
||||
* <p>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;
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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}.
|
||||
*
|
||||
* <p>In the process of converting a latitude-longitude pair to a 64-bit cell id, the following
|
||||
* coordinate systems are used:
|
||||
*
|
||||
* <ul>
|
||||
* <li>(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).
|
||||
* <li>(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.
|
||||
* <li>(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.
|
||||
* <li>(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.
|
||||
* <li>(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.
|
||||
* <li>(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).
|
||||
* <li>(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.
|
||||
* <li>(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.
|
||||
* </ul>
|
||||
*
|
||||
* <p>Note that the (i, j), (s, t), (si, ti), and (u, v) coordinate systems are right-handed on all
|
||||
* six faces.
|
||||
*
|
||||
* <p>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.
|
||||
*
|
||||
* <p>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.
|
||||
*
|
||||
* <table>
|
||||
* <tr>
|
||||
* <th>Projection</th>
|
||||
* <th>Area Ratio</th>
|
||||
* <th>Edge Ratio</th>
|
||||
* <th>Diag Ratio</th>
|
||||
* <th>ToPointRaw (microseconds)</th>
|
||||
* <th>ToPoint (microseconds)</th>
|
||||
* <th>FromPoint (microseconds)</th>
|
||||
* </tr>
|
||||
* <tr>
|
||||
* <td>Linear</td>
|
||||
* <td>5.200</td>
|
||||
* <td>2.117</td>
|
||||
* <td>2.959</td>
|
||||
* <td>0.020</td>
|
||||
* <td>0.087</td>
|
||||
* <td>0.085</td>
|
||||
* </tr>
|
||||
* <tr>
|
||||
* <td>Tangent</td>
|
||||
* <td>1.414</td>
|
||||
* <td>1.414</td>
|
||||
* <td>1.704</td>
|
||||
* <td>0.237</td>
|
||||
* <td>0.299</td>
|
||||
* <td>0.258</td>
|
||||
* </tr>
|
||||
* <tr>
|
||||
* <td>Quadratic</td>
|
||||
* <td>2.082</td>
|
||||
* <td>1.802</td>
|
||||
* <td>1.932</td>
|
||||
* <td>0.033</td>
|
||||
* <td>0.096</td>
|
||||
* <td>0.108</td>
|
||||
* </tr>
|
||||
* </table>
|
||||
*
|
||||
* <p>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.
|
||||
*
|
||||
* <p>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.
|
||||
*
|
||||
* <p>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}.)
|
||||
*
|
||||
* <p>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).
|
||||
*
|
||||
* <p>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).
|
||||
*
|
||||
* <p>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]).
|
||||
*
|
||||
* <p>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;
|
||||
}
|
||||
@@ -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.
|
||||
*
|
||||
* <p>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);
|
||||
}
|
||||
@@ -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.
|
||||
*
|
||||
* <p>Typical usage: {@code S2RegionCoverer coverer =
|
||||
* S2RegionCoverer.builder().setMaxCells(5).build(); S2Cap cap = S2Cap.fromAxisAngle(...);
|
||||
* S2CellUnion covering; coverer.getCovering(cap, covering);}
|
||||
*
|
||||
* <p>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).
|
||||
*
|
||||
* <p>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.
|
||||
*
|
||||
* <p>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<S2Cell> FACE_CELLS;
|
||||
|
||||
static {
|
||||
ImmutableList.Builder<S2Cell> 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<QueueEntry> {
|
||||
@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.
|
||||
*
|
||||
* <p>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.
|
||||
*
|
||||
* <p>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.
|
||||
*
|
||||
* <p>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:
|
||||
*
|
||||
* <ul>
|
||||
* <li>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.
|
||||
* <li>For any setting of maxCells(), an arbitrary number of cells may be returned if
|
||||
* minLevel() is too high for the region being approximated.
|
||||
* <li>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).
|
||||
* </ul>
|
||||
*
|
||||
* <p>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):
|
||||
*
|
||||
* <pre>
|
||||
* 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
|
||||
* </pre>
|
||||
*
|
||||
* <p>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<S2CellId> 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<S2CellId> 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<S2CellId> 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.
|
||||
*
|
||||
* <p>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.)
|
||||
*
|
||||
* <p>This function is useful as a starting point for algorithms that recursively subdivide cells.
|
||||
*/
|
||||
public void getFastCovering(S2Cap cap, ArrayList<S2CellId> 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.
|
||||
*
|
||||
* <p>{@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<S2CellId> 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<S2CellId> 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<S2CellId> result = new ArrayList<S2CellId>();
|
||||
|
||||
/** Prioritized candidates to explore next. */
|
||||
final PriorityQueue<QueueEntry> 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<S2CellId> base = new ArrayList<S2CellId>(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<S2CellId> output) {
|
||||
HashSet<S2CellId> all = new HashSet<S2CellId>();
|
||||
ArrayList<S2CellId> frontier = new ArrayList<S2CellId>();
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<S2Region> 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);
|
||||
}
|
||||
}
|
||||
@@ -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<S2Region> 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);
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
*
|
||||
* <p><>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.
|
||||
*
|
||||
* <p>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).
|
||||
*
|
||||
* <p>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<S2Point> chain(int chain) {
|
||||
return new AbstractList<S2Point>() {
|
||||
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<List<S2Point>> chains() {
|
||||
return new AbstractList<List<S2Point>>() {
|
||||
@Override public int size() {
|
||||
return numChains();
|
||||
}
|
||||
@Override public List<S2Point> get(int index) {
|
||||
return chain(index);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the dimension of the geometry represented by this shape.
|
||||
*
|
||||
* <ul>
|
||||
* <li>0 - Point geometry. Each point is represented as a degenerate edge.
|
||||
* <li>1 - Polyline geometry. Polyline edges may be degenerate. A shape may represent any number
|
||||
* of polylines. Polylines edges may intersect.
|
||||
* <li>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.
|
||||
* </ul>
|
||||
*
|
||||
* <p>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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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:
|
||||
*
|
||||
* <ul>
|
||||
* <li>{@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.
|
||||
* <li>{@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.
|
||||
* <li>{@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.
|
||||
* <li>{@link TopoAspect} provides the methods to relate a point in the world to the interior,
|
||||
* exterior, or boundary of the shape.
|
||||
*
|
||||
* <p>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<S2Point> vertices() {
|
||||
return new AbstractList<S2Point>() {
|
||||
@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<S2Point> 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<S2Point> 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<S2Point> 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<S2Point> 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<S2CellId> 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<S2CellId> 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<? extends 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<? extends Iterable<S2Point>> 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<? extends Iterable<S2Point>> 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<? extends Iterable<S2CellId>> 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 {}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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.
|
||||
*
|
||||
* <p>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<S2ShapeIndex> {
|
||||
|
||||
/**
|
||||
* 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<S2Shape> 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<S2Shape> 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<S2CellId> cellIds = new ArrayList<>();
|
||||
List<byte[]> encodedCells = new ArrayList<>();
|
||||
Multimap<S2Shape, Integer> shapeIds = S2ShapeUtil.shapeToShapeId(value);
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
for (S2Iterator<Cell> 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}.
|
||||
*
|
||||
* <p>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<S2ClippedShape[]> encodedCells;
|
||||
|
||||
/** The list of {@link Cell}s. */
|
||||
private final List<Cell> decodedCells;
|
||||
|
||||
/** A coder of {@code S2ClippedShape[]}s. */
|
||||
private final S2Coder<S2ClippedShape[]> clippedShapeArrayCoder =
|
||||
new S2Coder<S2ClippedShape[]>() {
|
||||
@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}.
|
||||
*
|
||||
* <p>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<S2Shape> 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<S2Shape>() {
|
||||
@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<Cell> 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<Cell> 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<S2Shape, Integer> 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<Integer> 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<S2Shape> 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;
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
*
|
||||
* <p>The dimension does <b>not</b> 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.
|
||||
*
|
||||
* <p>The measure of a given shape is defined as follows:
|
||||
*
|
||||
* <ul>
|
||||
* <li>For dimension 0 shapes, the measure is {@link S2Shape#numEdges()}.
|
||||
* <li>For dimension 1 shapes, the measure is {@link #length(S2ShapeIndex)}.
|
||||
* <li>For dimension 2 shapes, the measure is {@link #area(S2ShapeIndex)}.
|
||||
* </ul>
|
||||
*
|
||||
* <p>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.
|
||||
*
|
||||
* <p>The centroid is scaled by the total measure of the shapes for two reasons:
|
||||
*
|
||||
* <ol>
|
||||
* <li>It is cheaper to compute this way.
|
||||
* <li>This makes it easier to compute the centroid of a collection of shapes (since the
|
||||
* individual centroids can simply be summed)
|
||||
* </ol>
|
||||
*/
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
*
|
||||
* <p>Example usage:
|
||||
*
|
||||
* <pre>
|
||||
* S2CellUnion getCovering(S2ShapeIndex index) {
|
||||
* S2RegionCoverer coverer = new S2RegionCoverer();
|
||||
* coverer.setMaxCells(20);
|
||||
* return coverer.getCovering(new S2ShapeIndexRegion(index));
|
||||
* }
|
||||
* </pre>
|
||||
*
|
||||
* <p>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<S2ShapeIndex.Cell> 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<S2CellId> 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<S2CellId> 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.
|
||||
*
|
||||
* <p>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".
|
||||
*
|
||||
* <p>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;
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
*
|
||||
* <p>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<S2Point> 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<S2Point> 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.
|
||||
*
|
||||
* <p>The following cases are handled specially:
|
||||
*
|
||||
* <ul>
|
||||
* <li>Degenerate loops (consisting of an isolated vertex or composed entirely of sibling edge
|
||||
* pairs) have a curvature of 2*Pi exactly.
|
||||
* <li>The full loop (containing all points, and represented as a loop with no vertices) has a
|
||||
* curvature of -2*Pi exactly.
|
||||
* <li>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.
|
||||
* </ul>
|
||||
*/
|
||||
static double turningAngle(List<S2Point> 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<S2Point> 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<Integer> 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<LoopOrder> 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<S2Point> pruneDegeneracies(List<S2Point> input) {
|
||||
List<S2Point> 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.
|
||||
*
|
||||
* <p>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.
|
||||
*
|
||||
* <p>Scaling by the polyline length makes it easy to compute the centroid of several polylines
|
||||
* (by simply adding up their centroids).
|
||||
*
|
||||
* <p>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.
|
||||
*
|
||||
* <p>See {@link S2ShapeIndexMeasures#centroid(S2ShapeIndex)} for more info.
|
||||
*/
|
||||
@VisibleForTesting
|
||||
static S2Point loopCentroid(S2Shape shape, int chainId) {
|
||||
CentroidMeasure centroidMeasure = new CentroidMeasure();
|
||||
S2ShapeUtil.visitSurfaceIntegral(
|
||||
new AbstractList<S2Point>() {
|
||||
@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<S2Point> vertices(S2Shape shape, int chainId) {
|
||||
return new AbstractList<S2Point>() {
|
||||
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<S2Point, S2Point> 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<LoopOrder> {
|
||||
private final List<S2Point> loop;
|
||||
private final IntFunction<S2Point> vertex;
|
||||
|
||||
LoopOrderComparator(List<S2Point> 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<T, U> {
|
||||
void accept(T t, U u);
|
||||
}
|
||||
|
||||
/** A function which accepts an int. */
|
||||
private interface IntFunction<T> {
|
||||
T apply(int i);
|
||||
}
|
||||
|
||||
/** Wraps a mutable primitive double. */
|
||||
private static class MutableDouble {
|
||||
double d = 0;
|
||||
}
|
||||
}
|
||||
@@ -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<S2Edge>, only the {@link #add(S2Point, S2Point)}
|
||||
* method can mutate the list of edges.
|
||||
*
|
||||
* <p>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<S2Edge> implements S2Shape {
|
||||
|
||||
private final List<S2Edge> 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<S2ShapeIndex.Cell> 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<S2Loop> loops, S2Error error) {
|
||||
for (S2Iterator<S2ShapeIndex.Cell> 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<? extends S2Shape> 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<S2Loop> 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<S2Loop> 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<S2Loop> 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<S2Shape> a, List<S2Shape> 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.
|
||||
*
|
||||
* <p>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<S2Shape,S2Shape> where each S2Shape is distinguished by its
|
||||
// identity hash code.
|
||||
Multimap<S2Shape, S2Shape> 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<S2ShapeIndex.Cell> aIt = a.iterator();
|
||||
S2Iterator<S2ShapeIndex.Cell> 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<S2Edge> 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.
|
||||
*
|
||||
* <p>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().
|
||||
*
|
||||
* <p>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.
|
||||
*
|
||||
* <p>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).
|
||||
*
|
||||
* <p>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<S2Edge> fwdEdges = new ArrayList<>(n);
|
||||
List<S2Edge> 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<S2Shape, Integer> shapeToShapeId(S2ShapeIndex index) {
|
||||
Multimap<S2Shape, Integer> 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<Boolean> 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<Boolean> 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.
|
||||
*
|
||||
* <p>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:
|
||||
*
|
||||
* <ol>
|
||||
* <li>Each point in the loop interior has sign +1, and sign 0 otherwise; or
|
||||
* <li>Each point in the loop exterior has sign -1, and sign 0 otherwise.
|
||||
* </ol>
|
||||
*
|
||||
* <p>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).
|
||||
*
|
||||
* <p>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.
|
||||
*
|
||||
* <p>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<S2Point> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<S2Shape> {
|
||||
|
||||
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<S2Polygon.Shape> FAST_POLYGON_SHAPE_CODER =
|
||||
new S2Coder<S2Polygon.Shape>() {
|
||||
@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<S2Polygon.Shape> COMPACT_POLYGON_SHAPE_CODER =
|
||||
new S2Coder<S2Polygon.Shape>() {
|
||||
@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<S2Polyline> FAST_POLYLINE_SHAPE_CODER =
|
||||
new S2Coder<S2Polyline>() {
|
||||
@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<Class<? extends S2Polygon.Shape>> POLYGON_SHAPE_CLASSES =
|
||||
ImmutableList.of(
|
||||
new S2Polygon().binarySearchShape().getClass(),
|
||||
new S2Polygon().linearSearchShape().getClass());
|
||||
private static final ImmutableList<Class<? extends S2Point.Shape>> 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<Class<? extends S2LaxPolylineShape>>
|
||||
LAX_POLYLINE_SHAPE_CLASSES =
|
||||
ImmutableList.of(
|
||||
S2LaxPolylineShape.SimpleArray.class,
|
||||
S2LaxPolylineShape.SimpleList.class,
|
||||
S2LaxPolylineShape.SimplePacked.class,
|
||||
S2LaxPolylineShape.SimpleSnapped.class);
|
||||
private static final ImmutableList<Class<? extends S2LaxPolygonShape>> 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<Class<? extends S2Shape>, Integer> classToTypeTag;
|
||||
private final Map<Integer, S2Coder<? extends S2Shape>> typeTagToCoder;
|
||||
|
||||
private S2TaggedShapeCoder(
|
||||
IdentityHashMap<Class<? extends S2Shape>, Integer> classToTypeTag,
|
||||
Map<Integer, S2Coder<? extends S2Shape>> 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<S2Shape>) 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<? extends S2Shape> 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<Class<? extends S2Shape>, Integer> classToTypeTag;
|
||||
private final Map<Integer, S2Coder<? extends S2Shape>> typeTagToCoder;
|
||||
|
||||
private Builder(boolean allowReservedTags) {
|
||||
this.allowReservedTags = allowReservedTags;
|
||||
classToTypeTag = new IdentityHashMap<>();
|
||||
typeTagToCoder = new HashMap<>();
|
||||
}
|
||||
|
||||
private Builder(
|
||||
IdentityHashMap<Class<? extends S2Shape>, Integer> classToTypeTag,
|
||||
Map<Integer, S2Coder<? extends S2Shape>> typeTagToCoder) {
|
||||
this.allowReservedTags = false;
|
||||
this.classToTypeTag = classToTypeTag;
|
||||
this.typeTagToCoder = typeTagToCoder;
|
||||
}
|
||||
|
||||
/**
|
||||
* Associates {@code clazz} with a unique {@code coder} and {@code typeTag}.
|
||||
*
|
||||
* <p>If {@code clazz} or {@code typeTag} was already added, an {@link IllegalArgumentException}
|
||||
* is thrown.
|
||||
*/
|
||||
<T extends S2Shape> Builder add(Class<? extends T> clazz, S2Coder<T> 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}.
|
||||
*/
|
||||
<T extends S2Shape> Builder add(
|
||||
List<Class<? extends T>> clazzes, S2Coder<T> coder, int typeTag) {
|
||||
validateTypeTag(typeTag);
|
||||
for (Class<? extends T> 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 <T extends S2Shape> void validateClass(Class<? extends T> clazz) {
|
||||
Preconditions.checkArgument(
|
||||
!classToTypeTag.containsKey(clazz), "Duplicate class: %s", clazz.getName());
|
||||
}
|
||||
|
||||
/** Returns a newly-created {@link S2TaggedShapeCoder}. */
|
||||
S2TaggedShapeCoder build() {
|
||||
return new S2TaggedShapeCoder(classToTypeTag, typeTagToCoder);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<S2Point> 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.
|
||||
*
|
||||
* <p>Examples of the input format:
|
||||
*
|
||||
* <pre>
|
||||
* "" // no points
|
||||
* "-20:150" // one point
|
||||
* "-20:150, -20:151, -19:150" // three points
|
||||
* </pre>
|
||||
*/
|
||||
public static List<S2LatLng> parseLatLngsOrDie(String str) {
|
||||
List<S2LatLng> 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<S2LatLng> parseLatLngs(String str) {
|
||||
List<ParseEntry> ps = dictionaryParse(str);
|
||||
if (ps == null) {
|
||||
return null;
|
||||
}
|
||||
List<S2LatLng> 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<S2Point> parsePointsOrDie(String str) {
|
||||
List<S2Point> 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<S2Point> parsePoints(String str) {
|
||||
List<S2LatLng> latlngs = parseLatLngs(str);
|
||||
if (latlngs == null) {
|
||||
return null;
|
||||
}
|
||||
List<S2Point> 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<S2LatLng> 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<S2LatLng> 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.)
|
||||
*
|
||||
* <p>For example "4/" represents S2CellId.fromFace(4), and "3/02" represents
|
||||
* S2CellId.fromFace(3).child(0).child(2).
|
||||
*
|
||||
* <p>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<S2CellId> 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:
|
||||
*
|
||||
* <p>"-20:150, 10:-120, 0.123:-170.652"
|
||||
*
|
||||
* <p>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<S2Point> 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<S2Point> 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<S2Point> 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.)
|
||||
*
|
||||
* <p>Examples of the input format:
|
||||
*
|
||||
* <pre>
|
||||
* "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).
|
||||
* </pre>
|
||||
*/
|
||||
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<String> loopStrs = splitString(str, ";");
|
||||
List<List<S2Point>> loops = new ArrayList<>();
|
||||
for (String loopStr : loopStrs) {
|
||||
if (loopStr.equals("full")) {
|
||||
loops.add(new ArrayList<S2Point>());
|
||||
} else if (!loopStr.equals("empty")) {
|
||||
List<S2Point> 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:
|
||||
*
|
||||
* <p>point1|point2|... # line1|line2|... # polygon1|polygon2|...
|
||||
*
|
||||
* <p>Examples:
|
||||
*
|
||||
* <pre>
|
||||
* 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
|
||||
* </pre>
|
||||
*
|
||||
* <p>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).
|
||||
*
|
||||
* <p>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<S2Point> 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<S2Point> 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<S2LatLng> 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<String> splitString(String str, String regexp) {
|
||||
String[] parts = str.split(regexp);
|
||||
List<String> 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<ParseEntry> dictionaryParse(String str) {
|
||||
List<ParseEntry> 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<S2Loop>()); // Can't be an ImmutableList, it is clear()ed.
|
||||
}
|
||||
List<String> loopStrs = splitString(str, ";");
|
||||
List<S2Loop> 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<S2Point> points, StringBuilder out) {
|
||||
Iterator<S2Point> i = points.iterator();
|
||||
while (i.hasNext()) {
|
||||
appendVertex(i.next(), out);
|
||||
if (i.hasNext()) {
|
||||
out.append(", ");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<Longs> {
|
||||
|
||||
/** 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;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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<T>}s. */
|
||||
@GwtCompatible
|
||||
public class VectorCoder<T> implements S2Coder<List<T>> {
|
||||
|
||||
/** An encoder/decoder of {@code List<byte[]>}. */
|
||||
static final VectorCoder<byte[]> BYTE_ARRAY =
|
||||
new VectorCoder<>(
|
||||
new S2Coder<byte[]>() {
|
||||
@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<String>}. */
|
||||
static final VectorCoder<String> STRING =
|
||||
new VectorCoder<>(
|
||||
new S2Coder<String>() {
|
||||
@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<S2Shape> 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<S2Shape> COMPACT_SHAPE =
|
||||
new VectorCoder<>(S2TaggedShapeCoder.COMPACT);
|
||||
|
||||
private final S2Coder<T> coder;
|
||||
|
||||
/**
|
||||
* Constructs a {@code VectorCoder} which encodes/decodes elements with the given {@code coder}.
|
||||
*/
|
||||
public VectorCoder(S2Coder<T> coder) {
|
||||
this.coder = coder;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void encode(List<T> 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<T> 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<T>() {
|
||||
@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());
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user