GEO: Add support for z values to libs/geo classes (#38921)

Adds support for z-values to all Geometry objects in the
libs/geo library.
This commit is contained in:
Igor Motov 2019-03-13 15:35:18 -04:00
parent 8a314a36db
commit 4a42e408c5
21 changed files with 382 additions and 81 deletions

View File

@ -20,24 +20,32 @@
package org.elasticsearch.geo.geometry;
/**
* Circle geometry (not part of WKT standard, but used in elasticsearch)
* Circle geometry (not part of WKT standard, but used in elasticsearch) defined by lat/lon coordinates of the center in degrees
* and optional altitude in meters.
*/
public class Circle implements Geometry {
public static final Circle EMPTY = new Circle();
private final double lat;
private final double lon;
private final double alt;
private final double radiusMeters;
private Circle() {
lat = 0;
lon = 0;
alt = Double.NaN;
radiusMeters = -1;
}
public Circle(final double lat, final double lon, final double radiusMeters) {
this(lat, lon, Double.NaN, radiusMeters);
}
public Circle(final double lat, final double lon, final double alt, final double radiusMeters) {
this.lat = lat;
this.lon = lon;
this.radiusMeters = radiusMeters;
this.alt = alt;
if (radiusMeters < 0 ) {
throw new IllegalArgumentException("Circle radius [" + radiusMeters + "] cannot be negative");
}
@ -62,6 +70,10 @@ public class Circle implements Geometry {
return radiusMeters;
}
public double getAlt() {
return alt;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
@ -70,7 +82,8 @@ public class Circle implements Geometry {
Circle circle = (Circle) o;
if (Double.compare(circle.lat, lat) != 0) return false;
if (Double.compare(circle.lon, lon) != 0) return false;
return (Double.compare(circle.radiusMeters, radiusMeters) == 0);
if (Double.compare(circle.radiusMeters, radiusMeters) != 0) return false;
return (Double.compare(circle.alt, alt) == 0);
}
@Override
@ -83,6 +96,8 @@ public class Circle implements Geometry {
result = 31 * result + (int) (temp ^ (temp >>> 32));
temp = Double.doubleToLongBits(radiusMeters);
result = 31 * result + (int) (temp ^ (temp >>> 32));
temp = Double.doubleToLongBits(alt);
result = 31 * result + (int) (temp ^ (temp >>> 32));
return result;
}
@ -98,7 +113,11 @@ public class Circle implements Geometry {
@Override
public String toString() {
return "lat=" + lat + ", lon=" + lon + ", radius=" + radiusMeters;
return "lat=" + lat + ", lon=" + lon + ", radius=" + radiusMeters + (Double.isNaN(alt) ? ", alt=" + alt : "");
}
@Override
public boolean hasAlt() {
return Double.isNaN(alt) == false;
}
}

View File

@ -29,4 +29,8 @@ public interface Geometry {
<T> T visit(GeometryVisitor<T> visitor);
boolean isEmpty();
default boolean hasAlt() {
return false;
}
}

View File

@ -21,6 +21,7 @@ package org.elasticsearch.geo.geometry;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
/**
@ -31,6 +32,8 @@ public class GeometryCollection<G extends Geometry> implements Geometry, Iterabl
private final List<G> shapes;
private boolean hasAlt;
public GeometryCollection() {
shapes = Collections.emptyList();
}
@ -39,6 +42,12 @@ public class GeometryCollection<G extends Geometry> implements Geometry, Iterabl
if (shapes == null || shapes.isEmpty()) {
throw new IllegalArgumentException("the list of shapes cannot be null or empty");
}
hasAlt = shapes.get(0).hasAlt();
for (G shape : shapes) {
if (shape.hasAlt() != hasAlt) {
throw new IllegalArgumentException("all elements of the collection should have the same number of dimension");
}
}
this.shapes = shapes;
}
@ -82,4 +91,18 @@ public class GeometryCollection<G extends Geometry> implements Geometry, Iterabl
public Iterator<G> iterator() {
return shapes.iterator();
}
@Override
public boolean hasAlt() {
return hasAlt;
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append(type().name().toLowerCase(Locale.ROOT)).append("(shapes=");
sb.append(shapes);
sb.append(")");
return sb.toString();
}
}

View File

@ -22,21 +22,28 @@ package org.elasticsearch.geo.geometry;
import java.util.Arrays;
/**
* Represents a Line on the earth's surface in lat/lon decimal degrees.
* Represents a Line on the earth's surface in lat/lon decimal degrees and optional altitude in meters.
*/
public class Line implements Geometry {
public static final Line EMPTY = new Line();
private final double[] lats;
private final double[] lons;
private final double[] alts;
protected Line() {
lats = new double[0];
lons = new double[0];
alts = null;
}
public Line(double[] lats, double[] lons) {
this(lats, lons, null);
}
public Line(double[] lats, double[] lons, double[] alts) {
this.lats = lats;
this.lons = lons;
this.alts = alts;
if (lats == null) {
throw new IllegalArgumentException("lats must not be null");
}
@ -49,6 +56,9 @@ public class Line implements Geometry {
if (lats.length < 2) {
throw new IllegalArgumentException("at least two points in the line is required");
}
if (alts != null && alts.length != lats.length) {
throw new IllegalArgumentException("alts and lats must be equal length");
}
for (int i = 0; i < lats.length; i++) {
GeometryUtils.checkLatitude(lats[i]);
GeometryUtils.checkLongitude(lons[i]);
@ -67,6 +77,14 @@ public class Line implements Geometry {
return lons[i];
}
public double getAlt(int i) {
if (alts != null) {
return alts[i];
} else {
return Double.NaN;
}
}
public double[] getLats() {
return lats.clone();
}
@ -75,6 +93,10 @@ public class Line implements Geometry {
return lons.clone();
}
public double[] getAlts() {
return alts == null ? null : alts.clone();
}
@Override
public ShapeType type() {
return ShapeType.LINESTRING;
@ -96,19 +118,26 @@ public class Line implements Geometry {
if (o == null || getClass() != o.getClass()) return false;
Line line = (Line) o;
return Arrays.equals(lats, line.lats) &&
Arrays.equals(lons, line.lons);
Arrays.equals(lons, line.lons) && Arrays.equals(alts, line.alts);
}
@Override
public int hashCode() {
int result = Arrays.hashCode(lats);
result = 31 * result + Arrays.hashCode(lons);
result = 31 * result + Arrays.hashCode(alts);
return result;
}
@Override
public boolean hasAlt() {
return alts != null;
}
@Override
public String toString() {
return "lats=" + Arrays.toString(lats) +
", lons=" + Arrays.toString(lons);
", lons=" + Arrays.toString(lons) +
(hasAlt() ? ", alts=" + Arrays.toString(alts) : "");
}
}

View File

@ -20,7 +20,7 @@
package org.elasticsearch.geo.geometry;
/**
* Represents a closed line on the earth's surface in lat/lon decimal degrees.
* Represents a closed line on the earth's surface in lat/lon decimal degrees and optional altitude in meters.
* <p>
* Cannot be serialized by WKT directly but used as a part of polygon
*/
@ -31,13 +31,20 @@ public class LinearRing extends Line {
}
public LinearRing(double[] lats, double[] lons) {
super(lats, lons);
this(lats, lons, null);
}
public LinearRing(double[] lats, double[] lons, double[] alts) {
super(lats, lons, alts);
if (lats.length < 2) {
throw new IllegalArgumentException("linear ring cannot contain less than 2 points, found " + lats.length);
}
if (lats[0] != lats[lats.length - 1] || lons[0] != lons[lons.length - 1]) {
throw new IllegalArgumentException("first and last points of the linear ring must be the same (it must close itself): lats[0]="
+ lats[0] + " lats[" + (lats.length - 1) + "]=" + lats[lats.length - 1]);
int last = lats.length - 1;
if (lats[0] != lats[last] || lons[0] != lons[last] || (alts != null && alts[0] != alts[last])) {
throw new IllegalArgumentException("first and last points of the linear ring must be the same (it must close itself):" +
" lats[0]=" + lats[0] + " lats[" + last + "]=" + lats[last] +
" lons[0]=" + lons[0] + " lons[" + last + "]=" + lons[last] +
(alts == null ? "" : " alts[0]=" + alts[0] + " alts[" + last + "]=" + alts[last] ));
}
}

View File

@ -22,7 +22,7 @@ package org.elasticsearch.geo.geometry;
import java.util.List;
/**
* Represents a MultiPoint object on the earth's surface in decimal degrees.
* Represents a MultiPoint object on the earth's surface in decimal degrees and optional altitude in meters.
*/
public class MultiPoint extends GeometryCollection<Point> {
public static final MultiPoint EMPTY = new MultiPoint();

View File

@ -20,26 +20,33 @@
package org.elasticsearch.geo.geometry;
/**
* Represents a Point on the earth's surface in decimal degrees.
* Represents a Point on the earth's surface in decimal degrees and optional altitude in meters.
*/
public class Point implements Geometry {
public static final Point EMPTY = new Point();
private final double lat;
private final double lon;
private final double alt;
private final boolean empty;
private Point() {
lat = 0;
lon = 0;
alt = Double.NaN;
empty = true;
}
public Point(double lat, double lon) {
this(lat, lon, Double.NaN);
}
public Point(double lat, double lon, double alt) {
GeometryUtils.checkLatitude(lat);
GeometryUtils.checkLongitude(lon);
this.lat = lat;
this.lon = lon;
this.alt = alt;
this.empty = false;
}
@ -56,6 +63,10 @@ public class Point implements Geometry {
return lon;
}
public double getAlt() {
return alt;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
@ -64,7 +75,8 @@ public class Point implements Geometry {
Point point = (Point) o;
if (point.empty != empty) return false;
if (Double.compare(point.lat, lat) != 0) return false;
return Double.compare(point.lon, lon) == 0;
if (Double.compare(point.lon, lon) != 0) return false;
return Double.compare(point.alt, alt) == 0;
}
@Override
@ -75,6 +87,8 @@ public class Point implements Geometry {
result = (int) (temp ^ (temp >>> 32));
temp = Double.doubleToLongBits(lon);
result = 31 * result + (int) (temp ^ (temp >>> 32));
temp = Double.doubleToLongBits(alt);
result = 31 * result + (int) (temp ^ (temp >>> 32));
return result;
}
@ -88,8 +102,13 @@ public class Point implements Geometry {
return empty;
}
@Override
public boolean hasAlt() {
return Double.isNaN(alt) == false;
}
@Override
public String toString() {
return "lat=" + lat + ", lon=" + lon;
return "lat=" + lat + ", lon=" + lon + (hasAlt() ? ", alt=" + alt : "");
}
}

View File

@ -30,10 +30,12 @@ public final class Polygon implements Geometry {
public static final Polygon EMPTY = new Polygon();
private final LinearRing polygon;
private final List<LinearRing> holes;
private final boolean hasAlt;
private Polygon() {
polygon = LinearRing.EMPTY;
holes = Collections.emptyList();
hasAlt = false;
}
/**
@ -45,10 +47,15 @@ public final class Polygon implements Geometry {
if (holes == null) {
throw new IllegalArgumentException("holes must not be null");
}
boolean hasAlt = polygon.hasAlt();
checkRing(polygon);
for (LinearRing hole : holes) {
if (hole.hasAlt() != hasAlt) {
throw new IllegalArgumentException("holes must have the same number of dimensions as the polygon");
}
checkRing(hole);
}
this.hasAlt = hasAlt;
}
/**
@ -94,6 +101,10 @@ public final class Polygon implements Geometry {
return polygon.isEmpty();
}
@Override
public boolean hasAlt() {
return hasAlt;
}
@Override
public String toString() {

View File

@ -20,7 +20,7 @@
package org.elasticsearch.geo.geometry;
/**
* Represents a lat/lon rectangle in decimal degrees.
* Represents a lat/lon rectangle in decimal degrees and optional altitude in meters.
*/
public class Rectangle implements Geometry {
public static final Rectangle EMPTY = new Rectangle();
@ -32,6 +32,10 @@ public class Rectangle implements Geometry {
* minimum longitude value (in degrees)
*/
private final double minLon;
/**
* maximum altitude value (in meters)
*/
private final double minAlt;
/**
* maximum latitude value (in degrees)
*/
@ -40,6 +44,10 @@ public class Rectangle implements Geometry {
* minimum latitude value (in degrees)
*/
private final double maxLon;
/**
* minimum altitude value (in meters)
*/
private final double maxAlt;
private final boolean empty;
@ -48,6 +56,8 @@ public class Rectangle implements Geometry {
minLon = 0;
maxLat = 0;
maxLon = 0;
minAlt = Double.NaN;
maxAlt = Double.NaN;
empty = true;
}
@ -55,6 +65,12 @@ public class Rectangle implements Geometry {
* Constructs a bounding box by first validating the provided latitude and longitude coordinates
*/
public Rectangle(double minLat, double maxLat, double minLon, double maxLon) {
this(minLat, maxLat, minLon, maxLon, Double.NaN, Double.NaN);
}
/**
* Constructs a bounding box by first validating the provided latitude and longitude coordinates
*/
public Rectangle(double minLat, double maxLat, double minLon, double maxLon, double minAlt, double maxAlt) {
GeometryUtils.checkLatitude(minLat);
GeometryUtils.checkLatitude(maxLat);
GeometryUtils.checkLongitude(minLon);
@ -63,10 +79,15 @@ public class Rectangle implements Geometry {
this.maxLon = maxLon;
this.minLat = minLat;
this.maxLat = maxLat;
this.minAlt = minAlt;
this.maxAlt = maxAlt;
empty = false;
if (maxLat < minLat) {
throw new IllegalArgumentException("max lat cannot be less than min lat");
}
if (Double.isNaN(minAlt) != Double.isNaN(maxAlt)) {
throw new IllegalArgumentException("only one altitude value is specified");
}
}
public double getWidth() {
@ -88,6 +109,11 @@ public class Rectangle implements Geometry {
return minLon;
}
public double getMinAlt() {
return minAlt;
}
public double getMaxLat() {
return maxLat;
}
@ -96,6 +122,10 @@ public class Rectangle implements Geometry {
return maxLon;
}
public double getMaxAlt() {
return maxAlt;
}
@Override
public ShapeType type() {
return ShapeType.ENVELOPE;
@ -115,6 +145,12 @@ public class Rectangle implements Geometry {
if (maxLon < minLon) {
b.append(" [crosses dateline!]");
}
if (hasAlt()) {
b.append(" alt=");
b.append(minAlt);
b.append(" TO ");
b.append(maxAlt);
}
b.append(")");
return b.toString();
@ -137,7 +173,9 @@ public class Rectangle implements Geometry {
if (Double.compare(rectangle.minLat, minLat) != 0) return false;
if (Double.compare(rectangle.minLon, minLon) != 0) return false;
if (Double.compare(rectangle.maxLat, maxLat) != 0) return false;
return Double.compare(rectangle.maxLon, maxLon) == 0;
if (Double.compare(rectangle.maxLon, maxLon) != 0) return false;
if (Double.compare(rectangle.minAlt, minAlt) != 0) return false;
return Double.compare(rectangle.maxAlt, maxAlt) == 0;
}
@ -153,6 +191,10 @@ public class Rectangle implements Geometry {
result = 31 * result + (int) (temp ^ (temp >>> 32));
temp = Double.doubleToLongBits(maxLon);
result = 31 * result + (int) (temp ^ (temp >>> 32));
temp = Double.doubleToLongBits(minAlt);
result = 31 * result + (int) (temp ^ (temp >>> 32));
temp = Double.doubleToLongBits(maxAlt);
result = 31 * result + (int) (temp ^ (temp >>> 32));
return result;
}
@ -165,4 +207,9 @@ public class Rectangle implements Geometry {
public boolean isEmpty() {
return empty;
}
@Override
public boolean hasAlt() {
return Double.isNaN(maxAlt) == false;
}
}

View File

@ -72,9 +72,13 @@ public class WellKnownText {
@Override
public Void visit(Circle circle) {
sb.append(LPAREN);
visitPoint(circle.getLon(), circle.getLat());
visitPoint(circle.getLon(), circle.getLat(), Double.NaN);
sb.append(SPACE);
sb.append(circle.getRadiusMeters());
if (circle.hasAlt()) {
sb.append(SPACE);
sb.append(circle.getAlt());
}
sb.append(RPAREN);
return null;
}
@ -98,11 +102,11 @@ public class WellKnownText {
@Override
public Void visit(Line line) {
sb.append(LPAREN);
visitPoint(line.getLon(0), line.getLat(0));
visitPoint(line.getLon(0), line.getLat(0), line.getAlt(0));
for (int i = 1; i < line.length(); ++i) {
sb.append(COMMA);
sb.append(SPACE);
visitPoint(line.getLon(i), line.getLat(i));
visitPoint(line.getLon(i), line.getLat(i), line.getAlt(i));
}
sb.append(RPAREN);
return null;
@ -127,12 +131,12 @@ public class WellKnownText {
}
// walk through coordinates:
sb.append(LPAREN);
visitPoint(multiPoint.get(0).getLon(), multiPoint.get(0).getLat());
visitPoint(multiPoint.get(0).getLon(), multiPoint.get(0).getLat(), multiPoint.get(0).getAlt());
for (int i = 1; i < multiPoint.size(); ++i) {
sb.append(COMMA);
sb.append(SPACE);
Point point = multiPoint.get(i);
visitPoint(point.getLon(), point.getLat());
visitPoint(point.getLon(), point.getLat(), point.getAlt());
}
sb.append(RPAREN);
return null;
@ -150,14 +154,17 @@ public class WellKnownText {
sb.append(EMPTY);
} else {
sb.append(LPAREN);
visitPoint(point.getLon(), point.getLat());
visitPoint(point.getLon(), point.getLat(), point.getAlt());
sb.append(RPAREN);
}
return null;
}
private void visitPoint(double lon, double lat) {
private void visitPoint(double lon, double lat, double alt) {
sb.append(lon).append(SPACE).append(lat);
if (Double.isNaN(alt) == false) {
sb.append(SPACE).append(alt);
}
}
private void visitCollection(GeometryCollection<?> collection) {
@ -191,6 +198,7 @@ public class WellKnownText {
public Void visit(Rectangle rectangle) {
sb.append(LPAREN);
// minX, maxX, maxY, minY
// TODO: Add 3D support
sb.append(rectangle.getMinLon());
sb.append(COMMA);
sb.append(SPACE);
@ -278,28 +286,33 @@ public class WellKnownText {
}
double lon = nextNumber(stream);
double lat = nextNumber(stream);
Point pt = new Point(lat, lon);
if (isNumberNext(stream) == true) {
nextNumber(stream);
Point pt;
if (isNumberNext(stream)) {
pt = new Point(lat, lon, nextNumber(stream));
} else {
pt = new Point(lat, lon);
}
nextCloser(stream);
return pt;
}
private static void parseCoordinates(StreamTokenizer stream, ArrayList<Double> lats, ArrayList<Double> lons)
private static void parseCoordinates(StreamTokenizer stream, ArrayList<Double> lats, ArrayList<Double> lons, ArrayList<Double> alts)
throws IOException, ParseException {
parseCoordinate(stream, lats, lons);
parseCoordinate(stream, lats, lons, alts);
while (nextCloserOrComma(stream).equals(COMMA)) {
parseCoordinate(stream, lats, lons);
parseCoordinate(stream, lats, lons, alts);
}
}
private static void parseCoordinate(StreamTokenizer stream, ArrayList<Double> lats, ArrayList<Double> lons)
private static void parseCoordinate(StreamTokenizer stream, ArrayList<Double> lats, ArrayList<Double> lons, ArrayList<Double> alts)
throws IOException, ParseException {
lons.add(nextNumber(stream));
lats.add(nextNumber(stream));
if (isNumberNext(stream)) {
nextNumber(stream);
alts.add(nextNumber(stream));
}
if (alts.isEmpty() == false && alts.size() != lons.size()) {
throw new ParseException("coordinate dimensions do not match: " + tokenString(stream), stream.lineno());
}
}
@ -310,10 +323,15 @@ public class WellKnownText {
}
ArrayList<Double> lats = new ArrayList<>();
ArrayList<Double> lons = new ArrayList<>();
ArrayList<Double> alts = new ArrayList<>();
ArrayList<Point> points = new ArrayList<>();
parseCoordinates(stream, lats, lons);
parseCoordinates(stream, lats, lons, alts);
for (int i = 0; i < lats.size(); i++) {
points.add(new Point(lats.get(i), lons.get(i)));
if (alts.isEmpty()) {
points.add(new Point(lats.get(i), lons.get(i)));
} else {
points.add(new Point(lats.get(i), lons.get(i), alts.get(i)));
}
}
return new MultiPoint(Collections.unmodifiableList(points));
}
@ -325,8 +343,13 @@ public class WellKnownText {
}
ArrayList<Double> lats = new ArrayList<>();
ArrayList<Double> lons = new ArrayList<>();
parseCoordinates(stream, lats, lons);
return new Line(lats.stream().mapToDouble(i -> i).toArray(), lons.stream().mapToDouble(i -> i).toArray());
ArrayList<Double> alts = new ArrayList<>();
parseCoordinates(stream, lats, lons, alts);
if (alts.isEmpty()) {
return new Line(toArray(lats), toArray(lons));
} else {
return new Line(toArray(lats), toArray(lons), toArray(alts));
}
}
private static MultiLine parseMultiLine(StreamTokenizer stream) throws IOException, ParseException {
@ -346,8 +369,13 @@ public class WellKnownText {
nextOpener(stream);
ArrayList<Double> lats = new ArrayList<>();
ArrayList<Double> lons = new ArrayList<>();
parseCoordinates(stream, lats, lons);
return new LinearRing(lats.stream().mapToDouble(i -> i).toArray(), lons.stream().mapToDouble(i -> i).toArray());
ArrayList<Double> alts = new ArrayList<>();
parseCoordinates(stream, lats, lons, alts);
if (alts.isEmpty()) {
return new LinearRing(toArray(lats), toArray(lons));
} else {
return new LinearRing(toArray(lats), toArray(lons), toArray(alts));
}
}
private static Polygon parsePolygon(StreamTokenizer stream) throws IOException, ParseException {
@ -357,17 +385,22 @@ public class WellKnownText {
nextOpener(stream);
ArrayList<Double> lats = new ArrayList<>();
ArrayList<Double> lons = new ArrayList<>();
parseCoordinates(stream, lats, lons);
ArrayList<Double> alts = new ArrayList<>();
parseCoordinates(stream, lats, lons, alts);
ArrayList<LinearRing> holes = new ArrayList<>();
while (nextCloserOrComma(stream).equals(COMMA)) {
holes.add(parsePolygonHole(stream));
}
if (holes.isEmpty()) {
return new Polygon(new LinearRing(lats.stream().mapToDouble(i -> i).toArray(), lons.stream().mapToDouble(i -> i).toArray()));
LinearRing shell;
if (alts.isEmpty()) {
shell = new LinearRing(toArray(lats), toArray(lons));
} else {
return new Polygon(
new LinearRing(lats.stream().mapToDouble(i -> i).toArray(), lons.stream().mapToDouble(i -> i).toArray()),
Collections.unmodifiableList(holes));
shell = new LinearRing(toArray(lats), toArray(lons), toArray(alts));
}
if (holes.isEmpty()) {
return new Polygon(shell);
} else {
return new Polygon(shell, Collections.unmodifiableList(holes));
}
}
@ -388,6 +421,7 @@ public class WellKnownText {
if (nextEmptyOrOpen(stream).equals(EMPTY)) {
return Rectangle.EMPTY;
}
// TODO: Add 3D support
double minLon = nextNumber(stream);
nextComma(stream);
double maxLon = nextNumber(stream);
@ -407,10 +441,11 @@ public class WellKnownText {
double lon = nextNumber(stream);
double lat = nextNumber(stream);
double radius = nextNumber(stream);
Circle circle = new Circle(lat, lon, radius);
double alt = Double.NaN;
if (isNumberNext(stream) == true) {
nextNumber(stream);
alt = nextNumber(stream);
}
Circle circle = new Circle(lat, lon, alt, radius);
nextCloser(stream);
return circle;
}
@ -561,4 +596,8 @@ public class WellKnownText {
});
}
private static double[] toArray(ArrayList<Double> doubles) {
return doubles.stream().mapToDouble(i -> i).toArray();
}
}

View File

@ -30,10 +30,20 @@ import java.text.ParseException;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Supplier;
import java.util.function.Function;
abstract class BaseGeometryTestCase<T extends Geometry> extends AbstractWireTestCase<T> {
@Override
protected final T createTestInstance() {
boolean hasAlt = randomBoolean();
T obj = createTestInstance(hasAlt);
assertEquals(hasAlt, obj.hasAlt());
return obj;
}
protected abstract T createTestInstance(boolean hasAlt);
@Override
protected Writeable.Reader<T> instanceReader() {
throw new IllegalStateException("shouldn't be called in this test");
@ -127,48 +137,86 @@ abstract class BaseGeometryTestCase<T extends Geometry> extends AbstractWireTest
return randomDoubleBetween(-180, 180, true);
}
public static Circle randomCircle() {
return new Circle(randomDoubleBetween(-90, 90, true), randomDoubleBetween(-180, 180, true), randomDoubleBetween(0, 100, false));
public static Circle randomCircle(boolean hasAlt) {
if (hasAlt) {
return new Circle(randomDoubleBetween(-90, 90, true), randomDoubleBetween(-180, 180, true), randomDouble(),
randomDoubleBetween(0, 100, false));
} else {
return new Circle(randomDoubleBetween(-90, 90, true), randomDoubleBetween(-180, 180, true), randomDoubleBetween(0, 100, false));
}
}
public static Line randomLine() {
return randomLine(randomBoolean());
}
public static Line randomLine(boolean hasAlts) {
int size = randomIntBetween(2, 10);
double[] lats = new double[size];
double[] lons = new double[size];
double[] alts = hasAlts ? new double[size] : null;
for (int i = 0; i < size; i++) {
lats[i] = randomLat();
lons[i] = randomLon();
if (hasAlts) {
alts[i] = randomDouble();
}
}
if (hasAlts) {
return new Line(lats, lons, alts);
}
return new Line(lats, lons);
}
public static Point randomPoint() {
return new Point(randomLat(), randomLon());
return randomPoint(randomBoolean());
}
public static LinearRing randomLinearRing() {
public static Point randomPoint(boolean hasAlt) {
if (hasAlt) {
return new Point(randomLat(), randomLon(), randomDouble());
} else {
return new Point(randomLat(), randomLon());
}
}
public static LinearRing randomLinearRing(boolean hasAlt) {
int size = randomIntBetween(3, 10);
double[] lats = new double[size + 1];
double[] lons = new double[size + 1];
double[] alts;
if (hasAlt) {
alts = new double[size + 1];
} else {
alts = null;
}
for (int i = 0; i < size; i++) {
lats[i] = randomLat();
lons[i] = randomLon();
if (hasAlt) {
alts[i] = randomDouble();
}
}
lats[size] = lats[0];
lons[size] = lons[0];
return new LinearRing(lats, lons);
if (hasAlt) {
alts[size] = alts[0];
return new LinearRing(lats, lons, alts);
} else {
return new LinearRing(lats, lons);
}
}
public static Polygon randomPolygon() {
public static Polygon randomPolygon(boolean hasAlt) {
int size = randomIntBetween(0, 10);
List<LinearRing> holes = new ArrayList<>();
for (int i = 0; i < size; i++) {
holes.add(randomLinearRing());
holes.add(randomLinearRing(hasAlt));
}
if (holes.size() > 0) {
return new Polygon(randomLinearRing(), holes);
return new Polygon(randomLinearRing(hasAlt), holes);
} else {
return new Polygon(randomLinearRing());
return new Polygon(randomLinearRing(hasAlt));
}
}
@ -180,23 +228,23 @@ abstract class BaseGeometryTestCase<T extends Geometry> extends AbstractWireTest
return new Rectangle(Math.min(lat1, lat2), Math.max(lat1, lat2), minLon, maxLon);
}
public static GeometryCollection<Geometry> randomGeometryCollection() {
return randomGeometryCollection(0);
public static GeometryCollection<Geometry> randomGeometryCollection(boolean hasAlt) {
return randomGeometryCollection(0, hasAlt);
}
private static GeometryCollection<Geometry> randomGeometryCollection(int level) {
private static GeometryCollection<Geometry> randomGeometryCollection(int level, boolean hasAlt) {
int size = randomIntBetween(1, 10);
List<Geometry> shapes = new ArrayList<>();
for (int i = 0; i < size; i++) {
@SuppressWarnings("unchecked") Supplier<Geometry> geometry = randomFrom(
@SuppressWarnings("unchecked") Function<Boolean, Geometry> geometry = randomFrom(
BaseGeometryTestCase::randomCircle,
BaseGeometryTestCase::randomLine,
BaseGeometryTestCase::randomPoint,
BaseGeometryTestCase::randomPolygon,
BaseGeometryTestCase::randomRectangle,
level < 3 ? () -> randomGeometryCollection(level + 1) : BaseGeometryTestCase::randomPoint // don't build too deep
hasAlt ? BaseGeometryTestCase::randomPoint : (b) -> randomRectangle(),
level < 3 ? (b) -> randomGeometryCollection(level + 1, b) : BaseGeometryTestCase::randomPoint // don't build too deep
);
shapes.add(geometry.get());
shapes.add(geometry.apply(hasAlt));
}
return new GeometryCollection<>(shapes);
}

View File

@ -26,14 +26,22 @@ import java.text.ParseException;
public class CircleTests extends BaseGeometryTestCase<Circle> {
@Override
protected Circle createTestInstance() {
return new Circle(randomDoubleBetween(-90, 90, true), randomDoubleBetween(-180, 180, true), randomDoubleBetween(0, 100, false));
protected Circle createTestInstance(boolean hasAlt) {
if (hasAlt) {
return new Circle(randomDoubleBetween(-90, 90, true), randomDoubleBetween(-180, 180, true), randomDouble(),
randomDoubleBetween(0, 100, false));
} else {
return new Circle(randomDoubleBetween(-90, 90, true), randomDoubleBetween(-180, 180, true), randomDoubleBetween(0, 100, false));
}
}
public void testBasicSerialization() throws IOException, ParseException {
assertEquals("circle (20.0 10.0 15.0)", WellKnownText.toWKT(new Circle(10, 20, 15)));
assertEquals(new Circle(10, 20, 15), WellKnownText.fromWKT("circle (20.0 10.0 15.0)"));
assertEquals("circle (20.0 10.0 15.0 25.0)", WellKnownText.toWKT(new Circle(10, 20, 25, 15)));
assertEquals(new Circle(10, 20, 25, 15), WellKnownText.fromWKT("circle (20.0 10.0 15.0 25.0)"));
assertEquals("circle EMPTY", WellKnownText.toWKT(Circle.EMPTY));
assertEquals(Circle.EMPTY, WellKnownText.fromWKT("circle EMPTY)"));
}

View File

@ -28,10 +28,12 @@ import java.util.Collections;
public class GeometryCollectionTests extends BaseGeometryTestCase<GeometryCollection<Geometry>> {
@Override
protected GeometryCollection<Geometry> createTestInstance() {
return randomGeometryCollection();
protected GeometryCollection<Geometry> createTestInstance(boolean hasAlt) {
return randomGeometryCollection(hasAlt);
}
public void testBasicSerialization() throws IOException, ParseException {
assertEquals("geometrycollection (point (20.0 10.0),point EMPTY)",
WellKnownText.toWKT(new GeometryCollection<Geometry>(Arrays.asList(new Point(10, 20), Point.EMPTY))));
@ -50,5 +52,9 @@ public class GeometryCollectionTests extends BaseGeometryTestCase<GeometryCollec
ex = expectThrows(IllegalArgumentException.class, () -> new GeometryCollection<>(null));
assertEquals("the list of shapes cannot be null or empty", ex.getMessage());
ex = expectThrows(IllegalArgumentException.class, () -> new GeometryCollection<>(
Arrays.asList(new Point(10, 20), new Point(10, 20, 30))));
assertEquals("all elements of the collection should have the same number of dimension", ex.getMessage());
}
}

View File

@ -26,14 +26,19 @@ import java.text.ParseException;
public class LineTests extends BaseGeometryTestCase<Line> {
@Override
protected Line createTestInstance() {
return randomLine();
protected Line createTestInstance(boolean hasAlt) {
return randomLine(hasAlt);
}
public void testBasicSerialization() throws IOException, ParseException {
assertEquals("linestring (3.0 1.0, 4.0 2.0)", WellKnownText.toWKT(new Line(new double[]{1, 2}, new double[]{3, 4})));
assertEquals(new Line(new double[]{1, 2}, new double[]{3, 4}), WellKnownText.fromWKT("linestring (3 1, 4 2)"));
assertEquals("linestring (3.0 1.0 5.0, 4.0 2.0 6.0)", WellKnownText.toWKT(new Line(new double[]{1, 2}, new double[]{3, 4},
new double[]{5, 6})));
assertEquals(new Line(new double[]{1, 2}, new double[]{3, 4}, new double[]{6, 5}),
WellKnownText.fromWKT("linestring (3 1 6, 4 2 5)"));
assertEquals("linestring EMPTY", WellKnownText.toWKT(Line.EMPTY));
assertEquals(Line.EMPTY, WellKnownText.fromWKT("linestring EMPTY)"));
}

View File

@ -33,7 +33,14 @@ public class LinearRingTests extends ESTestCase {
public void testInitValidation() {
IllegalArgumentException ex = expectThrows(IllegalArgumentException.class,
() -> new LinearRing(new double[]{1, 2, 3}, new double[]{3, 4, 5}));
assertEquals("first and last points of the linear ring must be the same (it must close itself): lats[0]=1.0 lats[2]=3.0",
assertEquals("first and last points of the linear ring must be the same (it must close itself): lats[0]=1.0 lats[2]=3.0 " +
"lons[0]=3.0 lons[2]=5.0",
ex.getMessage());
ex = expectThrows(IllegalArgumentException.class,
() -> new LinearRing(new double[]{1, 2, 1}, new double[]{3, 4, 3}, new double[]{1, 2, 3}));
assertEquals("first and last points of the linear ring must be the same (it must close itself): lats[0]=1.0 lats[2]=1.0 " +
"lons[0]=3.0 lons[2]=3.0 alts[0]=1.0 alts[2]=3.0",
ex.getMessage());
ex = expectThrows(IllegalArgumentException.class, () -> new LinearRing(new double[]{1}, new double[]{3}));

View File

@ -30,11 +30,11 @@ import java.util.List;
public class MultiLineTests extends BaseGeometryTestCase<MultiLine> {
@Override
protected MultiLine createTestInstance() {
protected MultiLine createTestInstance(boolean hasAlt) {
int size = randomIntBetween(1, 10);
List<Line> arr = new ArrayList<Line>();
for (int i = 0; i < size; i++) {
arr.add(randomLine());
arr.add(randomLine(hasAlt));
}
return new MultiLine(arr);
}

View File

@ -24,17 +24,18 @@ import org.elasticsearch.geo.utils.WellKnownText;
import java.io.IOException;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
public class MultiPointTests extends BaseGeometryTestCase<MultiPoint> {
@Override
protected MultiPoint createTestInstance() {
protected MultiPoint createTestInstance(boolean hasAlt) {
int size = randomIntBetween(1, 10);
List<Point> arr = new ArrayList<>();
for (int i = 0; i < size; i++) {
arr.add(randomPoint());
arr.add(randomPoint(hasAlt));
}
return new MultiPoint(arr);
}
@ -45,6 +46,16 @@ public class MultiPointTests extends BaseGeometryTestCase<MultiPoint> {
assertEquals(new MultiPoint(Collections.singletonList(new Point(1 ,2))),
WellKnownText.fromWKT("multipoint (2 1)"));
assertEquals("multipoint (2.0 1.0, 3.0 4.0)",
WellKnownText.toWKT(new MultiPoint(Arrays.asList(new Point(1, 2), new Point(4, 3)))));
assertEquals(new MultiPoint(Arrays.asList(new Point(1, 2), new Point(4, 3))),
WellKnownText.fromWKT("multipoint (2 1, 3 4)"));
assertEquals("multipoint (2.0 1.0 10.0, 3.0 4.0 20.0)",
WellKnownText.toWKT(new MultiPoint(Arrays.asList(new Point(1, 2, 10), new Point(4, 3, 20)))));
assertEquals(new MultiPoint(Arrays.asList(new Point(1, 2, 10), new Point(4, 3, 20))),
WellKnownText.fromWKT("multipoint (2 1 10, 3 4 20)"));
assertEquals("multipoint EMPTY", WellKnownText.toWKT(MultiPoint.EMPTY));
assertEquals(MultiPoint.EMPTY, WellKnownText.fromWKT("multipoint EMPTY)"));
}

View File

@ -30,11 +30,11 @@ import java.util.List;
public class MultiPolygonTests extends BaseGeometryTestCase<MultiPolygon> {
@Override
protected MultiPolygon createTestInstance() {
protected MultiPolygon createTestInstance(boolean hasAlt) {
int size = randomIntBetween(1, 10);
List<Polygon> arr = new ArrayList<>();
for (int i = 0; i < size; i++) {
arr.add(randomPolygon());
arr.add(randomPolygon(hasAlt));
}
return new MultiPolygon(arr);
}

View File

@ -26,14 +26,17 @@ import java.text.ParseException;
public class PointTests extends BaseGeometryTestCase<Point> {
@Override
protected Point createTestInstance() {
return randomPoint();
protected Point createTestInstance(boolean hasAlt) {
return randomPoint(hasAlt);
}
public void testBasicSerialization() throws IOException, ParseException {
assertEquals("point (20.0 10.0)", WellKnownText.toWKT(new Point(10, 20)));
assertEquals(new Point(10, 20), WellKnownText.fromWKT("point (20.0 10.0)"));
assertEquals("point (20.0 10.0 100.0)", WellKnownText.toWKT(new Point(10, 20, 100)));
assertEquals(new Point(10, 20, 100), WellKnownText.fromWKT("point (20.0 10.0 100.0)"));
assertEquals("point EMPTY", WellKnownText.toWKT(Point.EMPTY));
assertEquals(Point.EMPTY, WellKnownText.fromWKT("point EMPTY)"));
}

View File

@ -23,11 +23,12 @@ import org.elasticsearch.geo.utils.WellKnownText;
import java.io.IOException;
import java.text.ParseException;
import java.util.Collections;
public class PolygonTests extends BaseGeometryTestCase<Polygon> {
@Override
protected Polygon createTestInstance() {
return randomPolygon();
protected Polygon createTestInstance(boolean hasAlt) {
return randomPolygon(hasAlt);
}
public void testBasicSerialization() throws IOException, ParseException {
@ -36,6 +37,11 @@ public class PolygonTests extends BaseGeometryTestCase<Polygon> {
assertEquals(new Polygon(new LinearRing(new double[]{1, 2, 3, 1}, new double[]{3, 4, 5, 3})),
WellKnownText.fromWKT("polygon ((3 1, 4 2, 5 3, 3 1))"));
assertEquals("polygon ((3.0 1.0 5.0, 4.0 2.0 4.0, 5.0 3.0 3.0, 3.0 1.0 5.0))",
WellKnownText.toWKT(new Polygon(new LinearRing(new double[]{1, 2, 3, 1}, new double[]{3, 4, 5, 3}, new double[]{5, 4, 3, 5}))));
assertEquals(new Polygon(new LinearRing(new double[]{1, 2, 3, 1}, new double[]{3, 4, 5, 3}, new double[]{5, 4, 3, 5})),
WellKnownText.fromWKT("polygon ((3 1 5, 4 2 4, 5 3 3, 3 1 5))"));
assertEquals("polygon EMPTY", WellKnownText.toWKT(Polygon.EMPTY));
assertEquals(Polygon.EMPTY, WellKnownText.fromWKT("polygon EMPTY)"));
}
@ -48,5 +54,10 @@ public class PolygonTests extends BaseGeometryTestCase<Polygon> {
ex = expectThrows(IllegalArgumentException.class,
() -> new Polygon(new LinearRing(new double[]{1, 2, 3, 1}, new double[]{3, 4, 5, 3}), null));
assertEquals("holes must not be null", ex.getMessage());
ex = expectThrows(IllegalArgumentException.class,
() -> new Polygon(new LinearRing(new double[]{1, 2, 3, 1}, new double[]{3, 4, 5, 3}, new double[]{5, 4, 3, 5}),
Collections.singletonList(new LinearRing(new double[]{1, 2, 3, 1}, new double[]{3, 4, 5, 3}))));
assertEquals("holes must have the same number of dimensions as the polygon", ex.getMessage());
}
}

View File

@ -26,7 +26,8 @@ import java.text.ParseException;
public class RectangleTests extends BaseGeometryTestCase<Rectangle> {
@Override
protected Rectangle createTestInstance() {
protected Rectangle createTestInstance(boolean hasAlt) {
assumeFalse("3rd dimension is not supported yet", hasAlt);
return randomRectangle();
}
@ -47,5 +48,8 @@ public class RectangleTests extends BaseGeometryTestCase<Rectangle> {
ex = expectThrows(IllegalArgumentException.class, () -> new Rectangle(2, 1, 2, 3));
assertEquals("max lat cannot be less than min lat", ex.getMessage());
ex = expectThrows(IllegalArgumentException.class, () -> new Rectangle(1, 2, 2, 3, 5, Double.NaN));
assertEquals("only one altitude value is specified", ex.getMessage());
}
}