wait to finish

This commit is contained in:
zhongchao
2022-06-07 14:32:12 +08:00
committed by liujing
parent 8481e08e91
commit ffc352a605
75 changed files with 36072 additions and 11 deletions

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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)
// }
}
/**

View File

@@ -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

View File

@@ -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);
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}

View File

@@ -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();
}
}

View File

@@ -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();
}
}

View File

@@ -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;
}
}

View File

@@ -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();
}
}

View File

@@ -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);
}
}

View File

@@ -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;
}
}
}

View File

@@ -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 + "]";
}
}

View File

@@ -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() + "]";
}
}

View File

@@ -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 + ")";
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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() {}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}

View File

@@ -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;
}
};
}
}

View File

@@ -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;
}
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}
}

View File

@@ -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);
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}
}
}
}

View File

@@ -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());
}
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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() + ")";
}
}

View File

@@ -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();
}
}
}

View File

@@ -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() + "]";
}
}

View File

@@ -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));
}
}
}

View File

@@ -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));
}
}
}

View File

@@ -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));
}
}

View File

@@ -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);
}
}
}

View File

@@ -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;
}
}
}

View File

@@ -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;
}
}
}

View File

@@ -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));
}
}

View File

@@ -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);
}
}

View File

@@ -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;
}
}
}
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}

View File

@@ -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);
}

View File

@@ -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);
}
}
}
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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();
}
}
}

View File

@@ -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 {}
}

View File

@@ -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;
}
}

View File

@@ -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();
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}
}

View File

@@ -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);
}
}
}

View File

@@ -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(", ");
}
}
}
}

View File

@@ -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;
}
};
}
}

View File

@@ -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());
}
};
}
}