Quellcode durchsuchen

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.
Igor Motov vor 6 Jahren
Ursprung
Commit
b2e50dd3d4
21 geänderte Dateien mit 381 neuen und 80 gelöschten Zeilen
  1. 22 3
      libs/geo/src/main/java/org/elasticsearch/geo/geometry/Circle.java
  2. 4 0
      libs/geo/src/main/java/org/elasticsearch/geo/geometry/Geometry.java
  3. 23 0
      libs/geo/src/main/java/org/elasticsearch/geo/geometry/GeometryCollection.java
  4. 32 3
      libs/geo/src/main/java/org/elasticsearch/geo/geometry/Line.java
  5. 12 5
      libs/geo/src/main/java/org/elasticsearch/geo/geometry/LinearRing.java
  6. 1 1
      libs/geo/src/main/java/org/elasticsearch/geo/geometry/MultiPoint.java
  7. 22 3
      libs/geo/src/main/java/org/elasticsearch/geo/geometry/Point.java
  8. 11 0
      libs/geo/src/main/java/org/elasticsearch/geo/geometry/Polygon.java
  9. 49 2
      libs/geo/src/main/java/org/elasticsearch/geo/geometry/Rectangle.java
  10. 67 28
      libs/geo/src/main/java/org/elasticsearch/geo/utils/WellKnownText.java
  11. 65 17
      libs/geo/src/test/java/org/elasticsearch/geo/geometry/BaseGeometryTestCase.java
  12. 10 2
      libs/geo/src/test/java/org/elasticsearch/geo/geometry/CircleTests.java
  13. 8 2
      libs/geo/src/test/java/org/elasticsearch/geo/geometry/GeometryCollectionTests.java
  14. 7 2
      libs/geo/src/test/java/org/elasticsearch/geo/geometry/LineTests.java
  15. 8 1
      libs/geo/src/test/java/org/elasticsearch/geo/geometry/LinearRingTests.java
  16. 2 2
      libs/geo/src/test/java/org/elasticsearch/geo/geometry/MultiLineTests.java
  17. 13 2
      libs/geo/src/test/java/org/elasticsearch/geo/geometry/MultiPointTests.java
  18. 2 2
      libs/geo/src/test/java/org/elasticsearch/geo/geometry/MultiPolygonTests.java
  19. 5 2
      libs/geo/src/test/java/org/elasticsearch/geo/geometry/PointTests.java
  20. 13 2
      libs/geo/src/test/java/org/elasticsearch/geo/geometry/PolygonTests.java
  21. 5 1
      libs/geo/src/test/java/org/elasticsearch/geo/geometry/RectangleTests.java

+ 22 - 3
libs/geo/src/main/java/org/elasticsearch/geo/geometry/Circle.java

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

+ 4 - 0
libs/geo/src/main/java/org/elasticsearch/geo/geometry/Geometry.java

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

+ 23 - 0
libs/geo/src/main/java/org/elasticsearch/geo/geometry/GeometryCollection.java

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

+ 32 - 3
libs/geo/src/main/java/org/elasticsearch/geo/geometry/Line.java

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

+ 12 - 5
libs/geo/src/main/java/org/elasticsearch/geo/geometry/LinearRing.java

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

+ 1 - 1
libs/geo/src/main/java/org/elasticsearch/geo/geometry/MultiPoint.java

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

+ 22 - 3
libs/geo/src/main/java/org/elasticsearch/geo/geometry/Point.java

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

+ 11 - 0
libs/geo/src/main/java/org/elasticsearch/geo/geometry/Polygon.java

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

+ 49 - 2
libs/geo/src/main/java/org/elasticsearch/geo/geometry/Rectangle.java

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

+ 67 - 28
libs/geo/src/main/java/org/elasticsearch/geo/utils/WellKnownText.java

@@ -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));
         }
+        LinearRing shell;
+        if (alts.isEmpty()) {
+            shell = new LinearRing(toArray(lats), toArray(lons));
+        } else {
+            shell = new LinearRing(toArray(lats), toArray(lons), toArray(alts));
+        }
         if (holes.isEmpty()) {
-            return new Polygon(new LinearRing(lats.stream().mapToDouble(i -> i).toArray(), lons.stream().mapToDouble(i -> i).toArray()));
+            return new Polygon(shell);
         } else {
-            return new Polygon(
-                new LinearRing(lats.stream().mapToDouble(i -> i).toArray(), lons.stream().mapToDouble(i -> i).toArray()),
-                Collections.unmodifiableList(holes));
+            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();
+    }
+
 }

+ 65 - 17
libs/geo/src/test/java/org/elasticsearch/geo/geometry/BaseGeometryTestCase.java

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

+ 10 - 2
libs/geo/src/test/java/org/elasticsearch/geo/geometry/CircleTests.java

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

+ 8 - 2
libs/geo/src/test/java/org/elasticsearch/geo/geometry/GeometryCollectionTests.java

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

+ 7 - 2
libs/geo/src/test/java/org/elasticsearch/geo/geometry/LineTests.java

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

+ 8 - 1
libs/geo/src/test/java/org/elasticsearch/geo/geometry/LinearRingTests.java

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

+ 2 - 2
libs/geo/src/test/java/org/elasticsearch/geo/geometry/MultiLineTests.java

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

+ 13 - 2
libs/geo/src/test/java/org/elasticsearch/geo/geometry/MultiPointTests.java

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

+ 2 - 2
libs/geo/src/test/java/org/elasticsearch/geo/geometry/MultiPolygonTests.java

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

+ 5 - 2
libs/geo/src/test/java/org/elasticsearch/geo/geometry/PointTests.java

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

+ 13 - 2
libs/geo/src/test/java/org/elasticsearch/geo/geometry/PolygonTests.java

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

+ 5 - 1
libs/geo/src/test/java/org/elasticsearch/geo/geometry/RectangleTests.java

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