diff --git a/libs/geo/build.gradle b/libs/geo/build.gradle new file mode 100644 index 00000000000..ab3419b93b9 --- /dev/null +++ b/libs/geo/build.gradle @@ -0,0 +1,49 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you 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. + */ + +apply plugin: 'elasticsearch.build' +apply plugin: 'nebula.maven-base-publish' +apply plugin: 'nebula.maven-scm' + +dependencies { + if (isEclipse == false || project.path == ":libs:geo-tests") { + testCompile("org.elasticsearch.test:framework:${version}") { + exclude group: 'org.elasticsearch', module: 'elasticsearch-geo' + } + } +} + +forbiddenApisMain { + // geo does not depend on server + // TODO: Need to decide how we want to handle for forbidden signatures with the changes to core + replaceSignatureFiles 'jdk-signatures' +} + +if (isEclipse) { + // in eclipse the project is under a fake root, we need to change around the source sets + sourceSets { + if (project.path == ":libs:geo") { + main.java.srcDirs = ['java'] + main.resources.srcDirs = ['resources'] + } else { + test.java.srcDirs = ['java'] + test.resources.srcDirs = ['resources'] + } + } +} diff --git a/libs/geo/src/main/java/org/elasticsearch/geo/geometry/Circle.java b/libs/geo/src/main/java/org/elasticsearch/geo/geometry/Circle.java new file mode 100644 index 00000000000..fea582e07b3 --- /dev/null +++ b/libs/geo/src/main/java/org/elasticsearch/geo/geometry/Circle.java @@ -0,0 +1,104 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you 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 org.elasticsearch.geo.geometry; + +/** + * Circle geometry (not part of WKT standard, but used in elasticsearch) + */ +public class Circle implements Geometry { + public static final Circle EMPTY = new Circle(); + private final double lat; + private final double lon; + private final double radiusMeters; + + private Circle() { + lat = 0; + lon = 0; + radiusMeters = -1; + } + + public Circle(final double lat, final double lon, final double radiusMeters) { + this.lat = lat; + this.lon = lon; + this.radiusMeters = radiusMeters; + if (radiusMeters < 0 ) { + throw new IllegalArgumentException("Circle radius [" + radiusMeters + "] cannot be negative"); + } + GeometryUtils.checkLatitude(lat); + GeometryUtils.checkLongitude(lon); + } + + @Override + public ShapeType type() { + return ShapeType.CIRCLE; + } + + public double getLat() { + return lat; + } + + public double getLon() { + return lon; + } + + public double getRadiusMeters() { + return radiusMeters; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + 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); + } + + @Override + public int hashCode() { + int result; + long temp; + temp = Double.doubleToLongBits(lat); + result = (int) (temp ^ (temp >>> 32)); + temp = Double.doubleToLongBits(lon); + result = 31 * result + (int) (temp ^ (temp >>> 32)); + temp = Double.doubleToLongBits(radiusMeters); + result = 31 * result + (int) (temp ^ (temp >>> 32)); + return result; + } + + @Override + public T visit(GeometryVisitor visitor) { + return visitor.visit(this); + } + + @Override + public boolean isEmpty() { + return radiusMeters < 0; + } + + @Override + public String toString() { + return "lat=" + lat + ", lon=" + lon + ", radius=" + radiusMeters; + } + +} diff --git a/libs/geo/src/main/java/org/elasticsearch/geo/geometry/Geometry.java b/libs/geo/src/main/java/org/elasticsearch/geo/geometry/Geometry.java new file mode 100644 index 00000000000..4557780effc --- /dev/null +++ b/libs/geo/src/main/java/org/elasticsearch/geo/geometry/Geometry.java @@ -0,0 +1,32 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you 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 org.elasticsearch.geo.geometry; + +/** + * Base class for all Geometry objects supported by elasticsearch + */ +public interface Geometry { + + ShapeType type(); + + T visit(GeometryVisitor visitor); + + boolean isEmpty(); +} diff --git a/libs/geo/src/main/java/org/elasticsearch/geo/geometry/GeometryCollection.java b/libs/geo/src/main/java/org/elasticsearch/geo/geometry/GeometryCollection.java new file mode 100644 index 00000000000..a6bad62efad --- /dev/null +++ b/libs/geo/src/main/java/org/elasticsearch/geo/geometry/GeometryCollection.java @@ -0,0 +1,85 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you 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 org.elasticsearch.geo.geometry; + +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.Objects; + +/** + * Collection of arbitrary geometry classes + */ +public class GeometryCollection implements Geometry, Iterable { + public static final GeometryCollection EMPTY = new GeometryCollection<>(); + + private final List shapes; + + public GeometryCollection() { + shapes = Collections.emptyList(); + } + + public GeometryCollection(List shapes) { + if (shapes == null || shapes.isEmpty()) { + throw new IllegalArgumentException("the list of shapes cannot be null or empty"); + } + this.shapes = shapes; + } + + @Override + public ShapeType type() { + return ShapeType.GEOMETRYCOLLECTION; + } + + @Override + public T visit(GeometryVisitor visitor) { + return visitor.visit(this); + } + + @Override + public boolean isEmpty() { + return shapes.isEmpty(); + } + + public int size() { + return shapes.size(); + } + + public G get(int i) { + return shapes.get(i); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + GeometryCollection that = (GeometryCollection) o; + return Objects.equals(shapes, that.shapes); + } + + @Override + public int hashCode() { + return Objects.hash(shapes); + } + + @Override + public Iterator iterator() { + return shapes.iterator(); + } +} diff --git a/libs/geo/src/main/java/org/elasticsearch/geo/geometry/GeometryUtils.java b/libs/geo/src/main/java/org/elasticsearch/geo/geometry/GeometryUtils.java new file mode 100644 index 00000000000..9a7d4b99d3e --- /dev/null +++ b/libs/geo/src/main/java/org/elasticsearch/geo/geometry/GeometryUtils.java @@ -0,0 +1,70 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you 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 org.elasticsearch.geo.geometry; + +/** + * Geometry-related utility methods + */ +final class GeometryUtils { + /** + * Minimum longitude value. + */ + static final double MIN_LON_INCL = -180.0D; + + /** + * Maximum longitude value. + */ + static final double MAX_LON_INCL = 180.0D; + + /** + * Minimum latitude value. + */ + static final double MIN_LAT_INCL = -90.0D; + + /** + * Maximum latitude value. + */ + static final double MAX_LAT_INCL = 90.0D; + + // No instance: + private GeometryUtils() { + } + + /** + * validates latitude value is within standard +/-90 coordinate bounds + */ + static void checkLatitude(double latitude) { + if (Double.isNaN(latitude) || latitude < MIN_LAT_INCL || latitude > MAX_LAT_INCL) { + throw new IllegalArgumentException( + "invalid latitude " + latitude + "; must be between " + MIN_LAT_INCL + " and " + MAX_LAT_INCL); + } + } + + /** + * validates longitude value is within standard +/-180 coordinate bounds + */ + static void checkLongitude(double longitude) { + if (Double.isNaN(longitude) || longitude < MIN_LON_INCL || longitude > MAX_LON_INCL) { + throw new IllegalArgumentException( + "invalid longitude " + longitude + "; must be between " + MIN_LON_INCL + " and " + MAX_LON_INCL); + } + } + +} diff --git a/libs/geo/src/main/java/org/elasticsearch/geo/geometry/GeometryVisitor.java b/libs/geo/src/main/java/org/elasticsearch/geo/geometry/GeometryVisitor.java new file mode 100644 index 00000000000..8317b23d1fe --- /dev/null +++ b/libs/geo/src/main/java/org/elasticsearch/geo/geometry/GeometryVisitor.java @@ -0,0 +1,69 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you 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 org.elasticsearch.geo.geometry; + +/** + * Support class for creating Geometry Visitors. + *

+ * This is an implementation of the Visitor pattern. The basic idea is to simplify adding new operations on Geometries, without + * constantly modifying and adding new functionality to the Geometry hierarchy and keeping it as lightweight as possible. + *

+ * It is a more object-oriented alternative to structures like this: + *

+ * if (obj instanceof This) {
+ *   doThis((This) obj);
+ * } elseif (obj instanceof That) {
+ *   doThat((That) obj);
+ * ...
+ * } else {
+ *   throw new IllegalArgumentException("Unknown object " + obj);
+ * }
+ * 
+ *

+ * The Visitor Pattern replaces this structure with Interface inheritance making it easier to identify all places that are using this + * structure, and making a shape a compile-time failure instead of runtime. + *

+ * See {@link org.elasticsearch.geo.utils.WellKnownText#toWKT(Geometry, StringBuilder)} for an example of how this interface is used. + * + * @see Visitor Pattern + */ +public interface GeometryVisitor { + + T visit(Circle circle); + + T visit(GeometryCollection collection); + + T visit(Line line); + + T visit(LinearRing ring); + + T visit(MultiLine multiLine); + + T visit(MultiPoint multiPoint); + + T visit(MultiPolygon multiPolygon); + + T visit(Point point); + + T visit(Polygon polygon); + + T visit(Rectangle rectangle); + +} diff --git a/libs/geo/src/main/java/org/elasticsearch/geo/geometry/Line.java b/libs/geo/src/main/java/org/elasticsearch/geo/geometry/Line.java new file mode 100644 index 00000000000..415dacfce9b --- /dev/null +++ b/libs/geo/src/main/java/org/elasticsearch/geo/geometry/Line.java @@ -0,0 +1,106 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you 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 org.elasticsearch.geo.geometry; + +import java.util.Arrays; + +/** + * Represents a Line on the earth's surface in lat/lon decimal degrees. + */ +public class Line implements Geometry { + public static final Line EMPTY = new Line(); + private final double[] lats; + private final double[] lons; + + protected Line() { + lats = new double[0]; + lons = new double[0]; + } + + public Line(double[] lats, double[] lons) { + this.lats = lats; + this.lons = lons; + if (lats == null) { + throw new IllegalArgumentException("lats must not be null"); + } + if (lons == null) { + throw new IllegalArgumentException("lons must not be null"); + } + if (lats.length != lons.length) { + throw new IllegalArgumentException("lats and lons must be equal length"); + } + if (lats.length < 2) { + throw new IllegalArgumentException("at least two points in the line is required"); + } + for (int i = 0; i < lats.length; i++) { + GeometryUtils.checkLatitude(lats[i]); + GeometryUtils.checkLongitude(lons[i]); + } + } + + public int length() { + return lats.length; + } + + public double getLat(int i) { + return lats[i]; + } + + public double getLon(int i) { + return lons[i]; + } + + @Override + public ShapeType type() { + return ShapeType.LINESTRING; + } + + @Override + public T visit(GeometryVisitor visitor) { + return visitor.visit(this); + } + + @Override + public boolean isEmpty() { + return lats.length == 0; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Line line = (Line) o; + return Arrays.equals(lats, line.lats) && + Arrays.equals(lons, line.lons); + } + + @Override + public int hashCode() { + int result = Arrays.hashCode(lats); + result = 31 * result + Arrays.hashCode(lons); + return result; + } + + @Override + public String toString() { + return "lats=" + Arrays.toString(lats) + + ", lons=" + Arrays.toString(lons); + } +} diff --git a/libs/geo/src/main/java/org/elasticsearch/geo/geometry/LinearRing.java b/libs/geo/src/main/java/org/elasticsearch/geo/geometry/LinearRing.java new file mode 100644 index 00000000000..20b1a46dd9d --- /dev/null +++ b/libs/geo/src/main/java/org/elasticsearch/geo/geometry/LinearRing.java @@ -0,0 +1,53 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you 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 org.elasticsearch.geo.geometry; + +/** + * Represents a closed line on the earth's surface in lat/lon decimal degrees. + *

+ * Cannot be serialized by WKT directly but used as a part of polygon + */ +public class LinearRing extends Line { + public static final LinearRing EMPTY = new LinearRing(); + + private LinearRing() { + } + + public LinearRing(double[] lats, double[] lons) { + super(lats, lons); + 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]); + } + } + + @Override + public ShapeType type() { + return ShapeType.LINEARRING; + } + + @Override + public T visit(GeometryVisitor visitor) { + return visitor.visit(this); + } +} diff --git a/libs/geo/src/main/java/org/elasticsearch/geo/geometry/MultiLine.java b/libs/geo/src/main/java/org/elasticsearch/geo/geometry/MultiLine.java new file mode 100644 index 00000000000..995c43d0c1c --- /dev/null +++ b/libs/geo/src/main/java/org/elasticsearch/geo/geometry/MultiLine.java @@ -0,0 +1,46 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you 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 org.elasticsearch.geo.geometry; + +import java.util.List; + +/** + * Represents a MultiLine geometry object on the earth's surface. + */ +public class MultiLine extends GeometryCollection { + public static final MultiLine EMPTY = new MultiLine(); + + private MultiLine() { + } + + public MultiLine(List lines) { + super(lines); + } + + @Override + public ShapeType type() { + return ShapeType.MULTILINESTRING; + } + + @Override + public T visit(GeometryVisitor visitor) { + return visitor.visit(this); + } +} diff --git a/libs/geo/src/main/java/org/elasticsearch/geo/geometry/MultiPoint.java b/libs/geo/src/main/java/org/elasticsearch/geo/geometry/MultiPoint.java new file mode 100644 index 00000000000..383fef81219 --- /dev/null +++ b/libs/geo/src/main/java/org/elasticsearch/geo/geometry/MultiPoint.java @@ -0,0 +1,47 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you 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 org.elasticsearch.geo.geometry; + +import java.util.List; + +/** + * Represents a MultiPoint object on the earth's surface in decimal degrees. + */ +public class MultiPoint extends GeometryCollection { + public static final MultiPoint EMPTY = new MultiPoint(); + + private MultiPoint() { + } + + public MultiPoint(List points) { + super(points); + } + + @Override + public ShapeType type() { + return ShapeType.MULTIPOINT; + } + + @Override + public T visit(GeometryVisitor visitor) { + return visitor.visit(this); + } + +} diff --git a/libs/geo/src/main/java/org/elasticsearch/geo/geometry/MultiPolygon.java b/libs/geo/src/main/java/org/elasticsearch/geo/geometry/MultiPolygon.java new file mode 100644 index 00000000000..01c68d6dd0b --- /dev/null +++ b/libs/geo/src/main/java/org/elasticsearch/geo/geometry/MultiPolygon.java @@ -0,0 +1,46 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you 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 org.elasticsearch.geo.geometry; + +import java.util.List; + +/** + * Collection of polygons + */ +public class MultiPolygon extends GeometryCollection { + public static final MultiPolygon EMPTY = new MultiPolygon(); + + private MultiPolygon() { + } + + public MultiPolygon(List polygons) { + super(polygons); + } + + @Override + public ShapeType type() { + return ShapeType.MULTIPOLYGON; + } + + @Override + public T visit(GeometryVisitor visitor) { + return visitor.visit(this); + } +} diff --git a/libs/geo/src/main/java/org/elasticsearch/geo/geometry/Point.java b/libs/geo/src/main/java/org/elasticsearch/geo/geometry/Point.java new file mode 100644 index 00000000000..d85d40c8dc7 --- /dev/null +++ b/libs/geo/src/main/java/org/elasticsearch/geo/geometry/Point.java @@ -0,0 +1,95 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you 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 org.elasticsearch.geo.geometry; + +/** + * Represents a Point on the earth's surface in decimal degrees. + */ +public class Point implements Geometry { + public static final Point EMPTY = new Point(); + + private final double lat; + private final double lon; + private final boolean empty; + + private Point() { + lat = 0; + lon = 0; + empty = true; + } + + public Point(double lat, double lon) { + GeometryUtils.checkLatitude(lat); + GeometryUtils.checkLongitude(lon); + this.lat = lat; + this.lon = lon; + this.empty = false; + } + + @Override + public ShapeType type() { + return ShapeType.POINT; + } + + public double lat() { + return lat; + } + + public double lon() { + return lon; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + 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; + } + + @Override + public int hashCode() { + int result; + long temp; + temp = Double.doubleToLongBits(lat); + result = (int) (temp ^ (temp >>> 32)); + temp = Double.doubleToLongBits(lon); + result = 31 * result + (int) (temp ^ (temp >>> 32)); + return result; + } + + @Override + public T visit(GeometryVisitor visitor) { + return visitor.visit(this); + } + + @Override + public boolean isEmpty() { + return empty; + } + + @Override + public String toString() { + return "lat=" + lat + ", lon=" + lon; + } +} diff --git a/libs/geo/src/main/java/org/elasticsearch/geo/geometry/Polygon.java b/libs/geo/src/main/java/org/elasticsearch/geo/geometry/Polygon.java new file mode 100644 index 00000000000..9f28c4b81b6 --- /dev/null +++ b/libs/geo/src/main/java/org/elasticsearch/geo/geometry/Polygon.java @@ -0,0 +1,122 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you 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 org.elasticsearch.geo.geometry; + +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +/** + * Represents a closed polygon on the earth's surface with optional holes + */ +public final class Polygon implements Geometry { + public static final Polygon EMPTY = new Polygon(); + private final LinearRing polygon; + private final List holes; + + private Polygon() { + polygon = LinearRing.EMPTY; + holes = Collections.emptyList(); + } + + /** + * Creates a new Polygon from the supplied latitude/longitude array, and optionally any holes. + */ + public Polygon(LinearRing polygon, List holes) { + this.polygon = polygon; + this.holes = holes; + if (holes == null) { + throw new IllegalArgumentException("holes must not be null"); + } + checkRing(polygon); + for (LinearRing hole : holes) { + checkRing(hole); + } + } + + /** + * Creates a new Polygon from the supplied latitude/longitude array, and optionally any holes. + */ + public Polygon(LinearRing polygon) { + this(polygon, Collections.emptyList()); + } + + @Override + public ShapeType type() { + return ShapeType.POLYGON; + } + + private void checkRing(LinearRing ring) { + if (ring.length() < 4) { + throw new IllegalArgumentException("at least 4 polygon points required"); + } + } + + public int getNumberOfHoles() { + return holes.size(); + } + + public LinearRing getPolygon() { + return polygon; + } + + public LinearRing getHole(int i) { + if (i >= holes.size()) { + throw new IllegalArgumentException("Index " + i + " is outside the bounds of the " + holes.size() + " polygon holes"); + } + return holes.get(i); + } + + @Override + public T visit(GeometryVisitor visitor) { + return visitor.visit(this); + } + + @Override + public boolean isEmpty() { + return polygon.isEmpty(); + } + + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("polygon=").append(polygon); + if (holes.size() > 0) { + sb.append(", holes="); + sb.append(holes); + } + return sb.toString(); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Polygon polygon1 = (Polygon) o; + return Objects.equals(polygon, polygon1.polygon) && + Objects.equals(holes, polygon1.holes); + } + + @Override + public int hashCode() { + return Objects.hash(polygon, holes); + } +} diff --git a/libs/geo/src/main/java/org/elasticsearch/geo/geometry/Rectangle.java b/libs/geo/src/main/java/org/elasticsearch/geo/geometry/Rectangle.java new file mode 100644 index 00000000000..8170bf95a6e --- /dev/null +++ b/libs/geo/src/main/java/org/elasticsearch/geo/geometry/Rectangle.java @@ -0,0 +1,168 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you 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 org.elasticsearch.geo.geometry; + +/** + * Represents a lat/lon rectangle in decimal degrees. + */ +public class Rectangle implements Geometry { + public static final Rectangle EMPTY = new Rectangle(); + /** + * maximum longitude value (in degrees) + */ + private final double minLat; + /** + * minimum longitude value (in degrees) + */ + private final double minLon; + /** + * maximum latitude value (in degrees) + */ + private final double maxLat; + /** + * minimum latitude value (in degrees) + */ + private final double maxLon; + + private final boolean empty; + + private Rectangle() { + minLat = 0; + minLon = 0; + maxLat = 0; + maxLon = 0; + empty = true; + } + + /** + * Constructs a bounding box by first validating the provided latitude and longitude coordinates + */ + public Rectangle(double minLat, double maxLat, double minLon, double maxLon) { + GeometryUtils.checkLatitude(minLat); + GeometryUtils.checkLatitude(maxLat); + GeometryUtils.checkLongitude(minLon); + GeometryUtils.checkLongitude(maxLon); + this.minLon = minLon; + this.maxLon = maxLon; + this.minLat = minLat; + this.maxLat = maxLat; + empty = false; + if (maxLat < minLat) { + throw new IllegalArgumentException("max lat cannot be less than min lat"); + } + } + + public double getWidth() { + if (crossesDateline()) { + return GeometryUtils.MAX_LON_INCL - minLon + maxLon - GeometryUtils.MIN_LON_INCL; + } + return maxLon - minLon; + } + + public double getHeight() { + return maxLat - minLat; + } + + public double getMinLat() { + return minLat; + } + + public double getMinLon() { + return minLon; + } + + public double getMaxLat() { + return maxLat; + } + + public double getMaxLon() { + return maxLon; + } + + @Override + public ShapeType type() { + return ShapeType.ENVELOPE; + } + + @Override + public String toString() { + StringBuilder b = new StringBuilder(); + b.append("Rectangle(lat="); + b.append(minLat); + b.append(" TO "); + b.append(maxLat); + b.append(" lon="); + b.append(minLon); + b.append(" TO "); + b.append(maxLon); + if (maxLon < minLon) { + b.append(" [crosses dateline!]"); + } + b.append(")"); + + return b.toString(); + } + + /** + * Returns true if this bounding box crosses the dateline + */ + public boolean crossesDateline() { + return maxLon < minLon; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + Rectangle rectangle = (Rectangle) o; + + 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; + + } + + @Override + public int hashCode() { + int result; + long temp; + temp = Double.doubleToLongBits(minLat); + result = (int) (temp ^ (temp >>> 32)); + temp = Double.doubleToLongBits(minLon); + result = 31 * result + (int) (temp ^ (temp >>> 32)); + temp = Double.doubleToLongBits(maxLat); + result = 31 * result + (int) (temp ^ (temp >>> 32)); + temp = Double.doubleToLongBits(maxLon); + result = 31 * result + (int) (temp ^ (temp >>> 32)); + return result; + } + + @Override + public T visit(GeometryVisitor visitor) { + return visitor.visit(this); + } + + @Override + public boolean isEmpty() { + return empty; + } +} diff --git a/libs/geo/src/main/java/org/elasticsearch/geo/geometry/ShapeType.java b/libs/geo/src/main/java/org/elasticsearch/geo/geometry/ShapeType.java new file mode 100644 index 00000000000..2272f1ad894 --- /dev/null +++ b/libs/geo/src/main/java/org/elasticsearch/geo/geometry/ShapeType.java @@ -0,0 +1,36 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you 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 org.elasticsearch.geo.geometry; + +/** + * Shape types supported by elasticsearch + */ +public enum ShapeType { + POINT, + MULTIPOINT, + LINESTRING, + MULTILINESTRING, + POLYGON, + MULTIPOLYGON, + GEOMETRYCOLLECTION, + LINEARRING, // not serialized by itself in WKT or WKB + ENVELOPE, // not part of the actual WKB spec + CIRCLE; // not part of the actual WKB spec +} diff --git a/libs/geo/src/main/java/org/elasticsearch/geo/package-info.java b/libs/geo/src/main/java/org/elasticsearch/geo/package-info.java new file mode 100644 index 00000000000..3b59d36b46a --- /dev/null +++ b/libs/geo/src/main/java/org/elasticsearch/geo/package-info.java @@ -0,0 +1,24 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you 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. + */ + + +/** + * Common Geo classes + */ +package org.elasticsearch.geo; diff --git a/libs/geo/src/main/java/org/elasticsearch/geo/utils/WellKnownText.java b/libs/geo/src/main/java/org/elasticsearch/geo/utils/WellKnownText.java new file mode 100644 index 00000000000..5cf29065b00 --- /dev/null +++ b/libs/geo/src/main/java/org/elasticsearch/geo/utils/WellKnownText.java @@ -0,0 +1,560 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you 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 org.elasticsearch.geo.utils; + +import org.elasticsearch.geo.geometry.Circle; +import org.elasticsearch.geo.geometry.Geometry; +import org.elasticsearch.geo.geometry.GeometryCollection; +import org.elasticsearch.geo.geometry.GeometryVisitor; +import org.elasticsearch.geo.geometry.Line; +import org.elasticsearch.geo.geometry.LinearRing; +import org.elasticsearch.geo.geometry.MultiLine; +import org.elasticsearch.geo.geometry.MultiPoint; +import org.elasticsearch.geo.geometry.MultiPolygon; +import org.elasticsearch.geo.geometry.Point; +import org.elasticsearch.geo.geometry.Polygon; +import org.elasticsearch.geo.geometry.Rectangle; + +import java.io.IOException; +import java.io.StreamTokenizer; +import java.io.StringReader; +import java.text.ParseException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Locale; + +/** + * Utility class for converting to and from WKT + */ +public class WellKnownText { + public static final String EMPTY = "EMPTY"; + public static final String SPACE = " "; + public static final String LPAREN = "("; + public static final String RPAREN = ")"; + public static final String COMMA = ","; + public static final String NAN = "NaN"; + + private static final String NUMBER = ""; + private static final String EOF = "END-OF-STREAM"; + private static final String EOL = "END-OF-LINE"; + + public static String toWKT(Geometry geometry) { + StringBuilder builder = new StringBuilder(); + toWKT(geometry, builder); + return builder.toString(); + } + + public static void toWKT(Geometry geometry, StringBuilder sb) { + sb.append(getWKTName(geometry)); + sb.append(SPACE); + if (geometry.isEmpty()) { + sb.append(EMPTY); + } else { + geometry.visit(new GeometryVisitor() { + @Override + public Void visit(Circle circle) { + sb.append(LPAREN); + visitPoint(circle.getLon(), circle.getLat()); + sb.append(SPACE); + sb.append(circle.getRadiusMeters()); + sb.append(RPAREN); + return null; + } + + @Override + public Void visit(GeometryCollection collection) { + if (collection.size() == 0) { + sb.append(EMPTY); + } else { + sb.append(LPAREN); + toWKT(collection.get(0), sb); + for (int i = 1; i < collection.size(); ++i) { + sb.append(COMMA); + toWKT(collection.get(i), sb); + } + sb.append(RPAREN); + } + return null; + } + + @Override + public Void visit(Line line) { + sb.append(LPAREN); + visitPoint(line.getLon(0), line.getLat(0)); + for (int i = 1; i < line.length(); ++i) { + sb.append(COMMA); + sb.append(SPACE); + visitPoint(line.getLon(i), line.getLat(i)); + } + sb.append(RPAREN); + return null; + } + + @Override + public Void visit(LinearRing ring) { + throw new IllegalArgumentException("Linear ring is not supported by WKT"); + } + + @Override + public Void visit(MultiLine multiLine) { + visitCollection(multiLine); + return null; + } + + @Override + public Void visit(MultiPoint multiPoint) { + // walk through coordinates: + sb.append(LPAREN); + visitPoint(multiPoint.get(0).lon(), multiPoint.get(0).lat()); + for (int i = 1; i < multiPoint.size(); ++i) { + sb.append(COMMA); + sb.append(SPACE); + Point point = multiPoint.get(i); + visitPoint(point.lon(), point.lat()); + } + sb.append(RPAREN); + return null; + } + + @Override + public Void visit(MultiPolygon multiPolygon) { + visitCollection(multiPolygon); + return null; + } + + @Override + public Void visit(Point point) { + if (point.isEmpty()) { + sb.append(EMPTY); + } else { + sb.append(LPAREN); + visitPoint(point.lon(), point.lat()); + sb.append(RPAREN); + } + return null; + } + + private void visitPoint(double lon, double lat) { + sb.append(lon).append(SPACE).append(lat); + } + + private void visitCollection(GeometryCollection collection) { + if (collection.size() == 0) { + sb.append(EMPTY); + } else { + sb.append(LPAREN); + collection.get(0).visit(this); + for (int i = 1; i < collection.size(); ++i) { + sb.append(COMMA); + collection.get(i).visit(this); + } + sb.append(RPAREN); + } + } + + @Override + public Void visit(Polygon polygon) { + sb.append(LPAREN); + visit((Line) polygon.getPolygon()); + int numberOfHoles = polygon.getNumberOfHoles(); + for (int i = 0; i < numberOfHoles; ++i) { + sb.append(", "); + visit((Line) polygon.getHole(i)); + } + sb.append(RPAREN); + return null; + } + + @Override + public Void visit(Rectangle rectangle) { + sb.append(LPAREN); + // minX, maxX, maxY, minY + sb.append(rectangle.getMinLon()); + sb.append(COMMA); + sb.append(SPACE); + sb.append(rectangle.getMaxLon()); + sb.append(COMMA); + sb.append(SPACE); + sb.append(rectangle.getMaxLat()); + sb.append(COMMA); + sb.append(SPACE); + sb.append(rectangle.getMinLat()); + sb.append(RPAREN); + return null; + } + }); + } + } + + public static Geometry fromWKT(String wkt) throws IOException, ParseException { + StringReader reader = new StringReader(wkt); + try { + // setup the tokenizer; configured to read words w/o numbers + StreamTokenizer tokenizer = new StreamTokenizer(reader); + tokenizer.resetSyntax(); + tokenizer.wordChars('a', 'z'); + tokenizer.wordChars('A', 'Z'); + tokenizer.wordChars(128 + 32, 255); + tokenizer.wordChars('0', '9'); + tokenizer.wordChars('-', '-'); + tokenizer.wordChars('+', '+'); + tokenizer.wordChars('.', '.'); + tokenizer.whitespaceChars(' ', ' '); + tokenizer.whitespaceChars('\t', '\t'); + tokenizer.whitespaceChars('\r', '\r'); + tokenizer.whitespaceChars('\n', '\n'); + tokenizer.commentChar('#'); + return parseGeometry(tokenizer); + } finally { + reader.close(); + } + } + + /** + * parse geometry from the stream tokenizer + */ + private static Geometry parseGeometry(StreamTokenizer stream) throws IOException, ParseException { + final String type = nextWord(stream).toLowerCase(Locale.ROOT); + switch (type) { + case "point": + return parsePoint(stream); + case "multipoint": + return parseMultiPoint(stream); + case "linestring": + return parseLine(stream); + case "multilinestring": + return parseMultiLine(stream); + case "polygon": + return parsePolygon(stream); + case "multipolygon": + return parseMultiPolygon(stream); + case "bbox": + return parseBBox(stream); + case "geometrycollection": + return parseGeometryCollection(stream); + case "circle": // Not part of the standard, but we need it for internal serialization + return parseCircle(stream); + } + throw new IllegalArgumentException("Unknown geometry type: " + type); + } + + private static GeometryCollection parseGeometryCollection(StreamTokenizer stream) throws IOException, ParseException { + if (nextEmptyOrOpen(stream).equals(EMPTY)) { + return GeometryCollection.EMPTY; + } + List shapes = new ArrayList<>(); + shapes.add(parseGeometry(stream)); + while (nextCloserOrComma(stream).equals(COMMA)) { + shapes.add(parseGeometry(stream)); + } + return new GeometryCollection<>(shapes); + } + + private static Point parsePoint(StreamTokenizer stream) throws IOException, ParseException { + if (nextEmptyOrOpen(stream).equals(EMPTY)) { + return Point.EMPTY; + } + double lon = nextNumber(stream); + double lat = nextNumber(stream); + Point pt = new Point(lat, lon); + if (isNumberNext(stream) == true) { + nextNumber(stream); + } + nextCloser(stream); + return pt; + } + + private static void parseCoordinates(StreamTokenizer stream, ArrayList lats, ArrayList lons) + throws IOException, ParseException { + parseCoordinate(stream, lats, lons); + while (nextCloserOrComma(stream).equals(COMMA)) { + parseCoordinate(stream, lats, lons); + } + } + + private static void parseCoordinate(StreamTokenizer stream, ArrayList lats, ArrayList lons) + throws IOException, ParseException { + lons.add(nextNumber(stream)); + lats.add(nextNumber(stream)); + if (isNumberNext(stream)) { + nextNumber(stream); + } + } + + private static MultiPoint parseMultiPoint(StreamTokenizer stream) throws IOException, ParseException { + String token = nextEmptyOrOpen(stream); + if (token.equals(EMPTY)) { + return MultiPoint.EMPTY; + } + ArrayList lats = new ArrayList<>(); + ArrayList lons = new ArrayList<>(); + ArrayList points = new ArrayList<>(); + parseCoordinates(stream, lats, lons); + for (int i = 0; i < lats.size(); i++) { + points.add(new Point(lats.get(i), lons.get(i))); + } + return new MultiPoint(Collections.unmodifiableList(points)); + } + + private static Line parseLine(StreamTokenizer stream) throws IOException, ParseException { + String token = nextEmptyOrOpen(stream); + if (token.equals(EMPTY)) { + return Line.EMPTY; + } + ArrayList lats = new ArrayList<>(); + ArrayList lons = new ArrayList<>(); + parseCoordinates(stream, lats, lons); + return new Line(lats.stream().mapToDouble(i -> i).toArray(), lons.stream().mapToDouble(i -> i).toArray()); + } + + private static MultiLine parseMultiLine(StreamTokenizer stream) throws IOException, ParseException { + String token = nextEmptyOrOpen(stream); + if (token.equals(EMPTY)) { + return MultiLine.EMPTY; + } + ArrayList lines = new ArrayList<>(); + lines.add(parseLine(stream)); + while (nextCloserOrComma(stream).equals(COMMA)) { + lines.add(parseLine(stream)); + } + return new MultiLine(Collections.unmodifiableList(lines)); + } + + private static LinearRing parsePolygonHole(StreamTokenizer stream) throws IOException, ParseException { + nextOpener(stream); + ArrayList lats = new ArrayList<>(); + ArrayList lons = new ArrayList<>(); + parseCoordinates(stream, lats, lons); + return new LinearRing(lats.stream().mapToDouble(i -> i).toArray(), lons.stream().mapToDouble(i -> i).toArray()); + } + + private static Polygon parsePolygon(StreamTokenizer stream) throws IOException, ParseException { + if (nextEmptyOrOpen(stream).equals(EMPTY)) { + return Polygon.EMPTY; + } + nextOpener(stream); + ArrayList lats = new ArrayList<>(); + ArrayList lons = new ArrayList<>(); + parseCoordinates(stream, lats, lons); + ArrayList 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())); + } else { + return new Polygon( + new LinearRing(lats.stream().mapToDouble(i -> i).toArray(), lons.stream().mapToDouble(i -> i).toArray()), + Collections.unmodifiableList(holes)); + } + } + + private static MultiPolygon parseMultiPolygon(StreamTokenizer stream) throws IOException, ParseException { + String token = nextEmptyOrOpen(stream); + if (token.equals(EMPTY)) { + return MultiPolygon.EMPTY; + } + ArrayList polygons = new ArrayList<>(); + polygons.add(parsePolygon(stream)); + while (nextCloserOrComma(stream).equals(COMMA)) { + polygons.add(parsePolygon(stream)); + } + return new MultiPolygon(Collections.unmodifiableList(polygons)); + } + + private static Rectangle parseBBox(StreamTokenizer stream) throws IOException, ParseException { + if (nextEmptyOrOpen(stream).equals(EMPTY)) { + return Rectangle.EMPTY; + } + double minLon = nextNumber(stream); + nextComma(stream); + double maxLon = nextNumber(stream); + nextComma(stream); + double maxLat = nextNumber(stream); + nextComma(stream); + double minLat = nextNumber(stream); + nextCloser(stream); + return new Rectangle(minLat, maxLat, minLon, maxLon); + } + + + private static Circle parseCircle(StreamTokenizer stream) throws IOException, ParseException { + if (nextEmptyOrOpen(stream).equals(EMPTY)) { + return Circle.EMPTY; + } + double lon = nextNumber(stream); + double lat = nextNumber(stream); + double radius = nextNumber(stream); + Circle circle = new Circle(lat, lon, radius); + if (isNumberNext(stream) == true) { + nextNumber(stream); + } + nextCloser(stream); + return circle; + } + + /** + * next word in the stream + */ + private static String nextWord(StreamTokenizer stream) throws ParseException, IOException { + switch (stream.nextToken()) { + case StreamTokenizer.TT_WORD: + final String word = stream.sval; + return word.equalsIgnoreCase(EMPTY) ? EMPTY : word; + case '(': + return LPAREN; + case ')': + return RPAREN; + case ',': + return COMMA; + } + throw new ParseException("expected word but found: " + tokenString(stream), stream.lineno()); + } + + private static double nextNumber(StreamTokenizer stream) throws IOException, ParseException { + if (stream.nextToken() == StreamTokenizer.TT_WORD) { + if (stream.sval.equalsIgnoreCase(NAN)) { + return Double.NaN; + } else { + try { + return Double.parseDouble(stream.sval); + } catch (NumberFormatException e) { + throw new ParseException("invalid number found: " + stream.sval, stream.lineno()); + } + } + } + throw new ParseException("expected number but found: " + tokenString(stream), stream.lineno()); + } + + private static String tokenString(StreamTokenizer stream) { + switch (stream.ttype) { + case StreamTokenizer.TT_WORD: + return stream.sval; + case StreamTokenizer.TT_EOF: + return EOF; + case StreamTokenizer.TT_EOL: + return EOL; + case StreamTokenizer.TT_NUMBER: + return NUMBER; + } + return "'" + (char) stream.ttype + "'"; + } + + private static boolean isNumberNext(StreamTokenizer stream) throws IOException { + final int type = stream.nextToken(); + stream.pushBack(); + return type == StreamTokenizer.TT_WORD; + } + + private static String nextEmptyOrOpen(StreamTokenizer stream) throws IOException, ParseException { + final String next = nextWord(stream); + if (next.equals(EMPTY) || next.equals(LPAREN)) { + return next; + } + throw new ParseException("expected " + EMPTY + " or " + LPAREN + + " but found: " + tokenString(stream), stream.lineno()); + } + + private static String nextCloser(StreamTokenizer stream) throws IOException, ParseException { + if (nextWord(stream).equals(RPAREN)) { + return RPAREN; + } + throw new ParseException("expected " + RPAREN + " but found: " + tokenString(stream), stream.lineno()); + } + + private static String nextComma(StreamTokenizer stream) throws IOException, ParseException { + if (nextWord(stream).equals(COMMA) == true) { + return COMMA; + } + throw new ParseException("expected " + COMMA + " but found: " + tokenString(stream), stream.lineno()); + } + + private static String nextOpener(StreamTokenizer stream) throws IOException, ParseException { + if (nextWord(stream).equals(LPAREN)) { + return LPAREN; + } + throw new ParseException("expected " + LPAREN + " but found: " + tokenString(stream), stream.lineno()); + } + + private static String nextCloserOrComma(StreamTokenizer stream) throws IOException, ParseException { + String token = nextWord(stream); + if (token.equals(COMMA) || token.equals(RPAREN)) { + return token; + } + throw new ParseException("expected " + COMMA + " or " + RPAREN + + " but found: " + tokenString(stream), stream.lineno()); + } + + public static String getWKTName(Geometry geometry) { + return geometry.visit(new GeometryVisitor() { + @Override + public String visit(Circle circle) { + return "circle"; + } + + @Override + public String visit(GeometryCollection collection) { + return "geometrycollection"; + } + + @Override + public String visit(Line line) { + return "linestring"; + } + + @Override + public String visit(LinearRing ring) { + throw new UnsupportedOperationException("line ring cannot be serialized using WKT"); + } + + @Override + public String visit(MultiLine multiLine) { + return "multilinestring"; + } + + @Override + public String visit(MultiPoint multiPoint) { + return "multipoint"; + } + + @Override + public String visit(MultiPolygon multiPolygon) { + return "multipolygon"; + } + + @Override + public String visit(Point point) { + return "point"; + } + + @Override + public String visit(Polygon polygon) { + return "polygon"; + } + + @Override + public String visit(Rectangle rectangle) { + return "bbox"; + } + }); + } + +} diff --git a/libs/geo/src/test/java/org/elasticsearch/geo/geometry/BaseGeometryTestCase.java b/libs/geo/src/test/java/org/elasticsearch/geo/geometry/BaseGeometryTestCase.java new file mode 100644 index 00000000000..3ffabd21343 --- /dev/null +++ b/libs/geo/src/test/java/org/elasticsearch/geo/geometry/BaseGeometryTestCase.java @@ -0,0 +1,203 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you 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 org.elasticsearch.geo.geometry; + +import org.elasticsearch.ElasticsearchException; +import org.elasticsearch.Version; +import org.elasticsearch.common.io.stream.Writeable; +import org.elasticsearch.geo.utils.WellKnownText; +import org.elasticsearch.test.AbstractWireTestCase; + +import java.io.IOException; +import java.text.ParseException; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Supplier; + +abstract class BaseGeometryTestCase extends AbstractWireTestCase { + + @Override + protected Writeable.Reader instanceReader() { + throw new IllegalStateException("shouldn't be called in this test"); + } + + + @SuppressWarnings("unchecked") + @Override + protected T copyInstance(T instance, Version version) throws IOException { + String text = WellKnownText.toWKT(instance); + try { + return (T) WellKnownText.fromWKT(text); + } catch (ParseException e) { + throw new ElasticsearchException(e); + } + } + + public void testVisitor() { + testVisitor(createTestInstance()); + } + + public static void testVisitor(Geometry geom) { + AtomicBoolean called = new AtomicBoolean(false); + Object result = geom.visit(new GeometryVisitor() { + private Object verify(Geometry geometry, String expectedClass) { + assertFalse("Visitor should be called only once", called.getAndSet(true)); + assertSame(geom, geometry); + assertEquals(geometry.getClass().getName(), "org.elasticsearch.geo.geometry." + expectedClass); + return "result"; + } + + @Override + public Object visit(Circle circle) { + return verify(circle, "Circle"); + } + + @Override + public Object visit(GeometryCollection collection) { + return verify(collection, "GeometryCollection"); } + + @Override + public Object visit(Line line) { + return verify(line, "Line"); + } + + @Override + public Object visit(LinearRing ring) { + return verify(ring, "LinearRing"); + } + + @Override + public Object visit(MultiLine multiLine) { + return verify(multiLine, "MultiLine"); + } + + @Override + public Object visit(MultiPoint multiPoint) { + return verify(multiPoint, "MultiPoint"); + } + + @Override + public Object visit(MultiPolygon multiPolygon) { + return verify(multiPolygon, "MultiPolygon"); + } + + @Override + public Object visit(Point point) { + return verify(point, "Point"); + } + + @Override + public Object visit(Polygon polygon) { + return verify(polygon, "Polygon"); + } + + @Override + public Object visit(Rectangle rectangle) { + return verify(rectangle, "Rectangle"); + } + }); + + assertTrue("visitor wasn't called", called.get()); + assertEquals("result", result); + } + + public static double randomLat() { + return randomDoubleBetween(-90, 90, true); + } + + public static double randomLon() { + 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 Line randomLine() { + int size = randomIntBetween(2, 10); + double[] lats = new double[size]; + double[] lons = new double[size]; + for (int i = 0; i < size; i++) { + lats[i] = randomLat(); + lons[i] = randomLon(); + } + return new Line(lats, lons); + } + + public static Point randomPoint() { + return new Point(randomLat(), randomLon()); + } + + public static LinearRing randomLinearRing() { + int size = randomIntBetween(3, 10); + double[] lats = new double[size + 1]; + double[] lons = new double[size + 1]; + for (int i = 0; i < size; i++) { + lats[i] = randomLat(); + lons[i] = randomLon(); + } + lats[size] = lats[0]; + lons[size] = lons[0]; + return new LinearRing(lats, lons); + } + + public static Polygon randomPolygon() { + int size = randomIntBetween(0, 10); + List holes = new ArrayList<>(); + for (int i = 0; i < size; i++) { + holes.add(randomLinearRing()); + } + if (holes.size() > 0) { + return new Polygon(randomLinearRing(), holes); + } else { + return new Polygon(randomLinearRing()); + } + } + + public static Rectangle randomRectangle() { + double lat1 = randomLat(); + double lat2 = randomLat(); + double minLon = randomLon(); + double maxLon = randomLon(); + return new Rectangle(Math.min(lat1, lat2), Math.max(lat1, lat2), minLon, maxLon); + } + + public static GeometryCollection randomGeometryCollection() { + return randomGeometryCollection(0); + } + + private static GeometryCollection randomGeometryCollection(int level) { + int size = randomIntBetween(1, 10); + List shapes = new ArrayList<>(); + for (int i = 0; i < size; i++) { + @SuppressWarnings("unchecked") Supplier geometry = randomFrom( + BaseGeometryTestCase::randomCircle, + BaseGeometryTestCase::randomLine, + BaseGeometryTestCase::randomPoint, + BaseGeometryTestCase::randomPolygon, + BaseGeometryTestCase::randomRectangle, + level < 3 ? () -> randomGeometryCollection(level + 1) : BaseGeometryTestCase::randomPoint // don't build too deep + ); + shapes.add(geometry.get()); + } + return new GeometryCollection<>(shapes); + } +} diff --git a/libs/geo/src/test/java/org/elasticsearch/geo/geometry/CircleTests.java b/libs/geo/src/test/java/org/elasticsearch/geo/geometry/CircleTests.java new file mode 100644 index 00000000000..0f2292792a7 --- /dev/null +++ b/libs/geo/src/test/java/org/elasticsearch/geo/geometry/CircleTests.java @@ -0,0 +1,51 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you 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 org.elasticsearch.geo.geometry; + +import org.elasticsearch.geo.utils.WellKnownText; + +import java.io.IOException; +import java.text.ParseException; + +public class CircleTests extends BaseGeometryTestCase { + @Override + protected Circle createTestInstance() { + 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 EMPTY", WellKnownText.toWKT(Circle.EMPTY)); + assertEquals(Circle.EMPTY, WellKnownText.fromWKT("circle EMPTY)")); + } + + public void testInitValidation() { + IllegalArgumentException ex = expectThrows(IllegalArgumentException.class, () -> new Circle(10, 20, -1)); + assertEquals("Circle radius [-1.0] cannot be negative", ex.getMessage()); + + ex = expectThrows(IllegalArgumentException.class, () -> new Circle(100, 20, 1)); + assertEquals("invalid latitude 100.0; must be between -90.0 and 90.0", ex.getMessage()); + + ex = expectThrows(IllegalArgumentException.class, () -> new Circle(10, 200, 1)); + assertEquals("invalid longitude 200.0; must be between -180.0 and 180.0", ex.getMessage()); + } +} diff --git a/libs/geo/src/test/java/org/elasticsearch/geo/geometry/GeometryCollectionTests.java b/libs/geo/src/test/java/org/elasticsearch/geo/geometry/GeometryCollectionTests.java new file mode 100644 index 00000000000..21b3c654103 --- /dev/null +++ b/libs/geo/src/test/java/org/elasticsearch/geo/geometry/GeometryCollectionTests.java @@ -0,0 +1,54 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you 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 org.elasticsearch.geo.geometry; + +import org.elasticsearch.geo.utils.WellKnownText; + +import java.io.IOException; +import java.text.ParseException; +import java.util.Arrays; +import java.util.Collections; + +public class GeometryCollectionTests extends BaseGeometryTestCase> { + @Override + protected GeometryCollection createTestInstance() { + return randomGeometryCollection(); + } + + public void testBasicSerialization() throws IOException, ParseException { + assertEquals("geometrycollection (point (20.0 10.0),point EMPTY)", + WellKnownText.toWKT(new GeometryCollection(Arrays.asList(new Point(10, 20), Point.EMPTY)))); + + assertEquals(new GeometryCollection(Arrays.asList(new Point(10, 20), Point.EMPTY)), + WellKnownText.fromWKT("geometrycollection (point (20.0 10.0),point EMPTY)")); + + assertEquals("geometrycollection EMPTY", WellKnownText.toWKT(GeometryCollection.EMPTY)); + assertEquals(GeometryCollection.EMPTY, WellKnownText.fromWKT("geometrycollection EMPTY)")); + } + + @SuppressWarnings("ConstantConditions") + public void testInitValidation() { + IllegalArgumentException ex = expectThrows(IllegalArgumentException.class, () -> new GeometryCollection<>(Collections.emptyList())); + assertEquals("the list of shapes cannot be null or empty", ex.getMessage()); + + ex = expectThrows(IllegalArgumentException.class, () -> new GeometryCollection<>(null)); + assertEquals("the list of shapes cannot be null or empty", ex.getMessage()); + } +} diff --git a/libs/geo/src/test/java/org/elasticsearch/geo/geometry/LineTests.java b/libs/geo/src/test/java/org/elasticsearch/geo/geometry/LineTests.java new file mode 100644 index 00000000000..9914481df44 --- /dev/null +++ b/libs/geo/src/test/java/org/elasticsearch/geo/geometry/LineTests.java @@ -0,0 +1,51 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you 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 org.elasticsearch.geo.geometry; + +import org.elasticsearch.geo.utils.WellKnownText; + +import java.io.IOException; +import java.text.ParseException; + +public class LineTests extends BaseGeometryTestCase { + @Override + protected Line createTestInstance() { + return randomLine(); + } + + 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 EMPTY", WellKnownText.toWKT(Line.EMPTY)); + assertEquals(Line.EMPTY, WellKnownText.fromWKT("linestring EMPTY)")); + } + + public void testInitValidation() { + IllegalArgumentException ex = expectThrows(IllegalArgumentException.class, () -> new Line(new double[]{1}, new double[]{3})); + assertEquals("at least two points in the line is required", ex.getMessage()); + + ex = expectThrows(IllegalArgumentException.class, () -> new Line(new double[]{1, 2, 3, 1}, new double[]{3, 4, 500, 3})); + assertEquals("invalid longitude 500.0; must be between -180.0 and 180.0", ex.getMessage()); + + ex = expectThrows(IllegalArgumentException.class, () -> new Line(new double[]{1, 100, 3, 1}, new double[]{3, 4, 5, 3})); + assertEquals("invalid latitude 100.0; must be between -90.0 and 90.0", ex.getMessage()); + } +} diff --git a/libs/geo/src/test/java/org/elasticsearch/geo/geometry/LinearRingTests.java b/libs/geo/src/test/java/org/elasticsearch/geo/geometry/LinearRingTests.java new file mode 100644 index 00000000000..73f6ac9a2f9 --- /dev/null +++ b/libs/geo/src/test/java/org/elasticsearch/geo/geometry/LinearRingTests.java @@ -0,0 +1,52 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you 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 org.elasticsearch.geo.geometry; + +import org.elasticsearch.geo.utils.WellKnownText; +import org.elasticsearch.test.ESTestCase; + +public class LinearRingTests extends ESTestCase { + + public void testBasicSerialization() { + UnsupportedOperationException ex = expectThrows(UnsupportedOperationException.class, + () -> WellKnownText.toWKT(new LinearRing(new double[]{1, 2, 3, 1}, new double[]{3, 4, 5, 3}))); + assertEquals("line ring cannot be serialized using WKT", ex.getMessage()); + } + + 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", + ex.getMessage()); + + ex = expectThrows(IllegalArgumentException.class, () -> new LinearRing(new double[]{1}, new double[]{3})); + assertEquals("at least two points in the line is required", ex.getMessage()); + + ex = expectThrows(IllegalArgumentException.class, () -> new LinearRing(new double[]{1, 2, 3, 1}, new double[]{3, 4, 500, 3})); + assertEquals("invalid longitude 500.0; must be between -180.0 and 180.0", ex.getMessage()); + + ex = expectThrows(IllegalArgumentException.class, () -> new LinearRing(new double[]{1, 100, 3, 1}, new double[]{3, 4, 5, 3})); + assertEquals("invalid latitude 100.0; must be between -90.0 and 90.0", ex.getMessage()); + } + + public void testVisitor() { + BaseGeometryTestCase.testVisitor(new LinearRing(new double[]{1, 2, 3, 1}, new double[]{3, 4, 5, 3})); + } +} diff --git a/libs/geo/src/test/java/org/elasticsearch/geo/geometry/MultiLineTests.java b/libs/geo/src/test/java/org/elasticsearch/geo/geometry/MultiLineTests.java new file mode 100644 index 00000000000..ec5a8c2dd38 --- /dev/null +++ b/libs/geo/src/test/java/org/elasticsearch/geo/geometry/MultiLineTests.java @@ -0,0 +1,51 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you 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 org.elasticsearch.geo.geometry; + +import org.elasticsearch.geo.utils.WellKnownText; + +import java.io.IOException; +import java.text.ParseException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public class MultiLineTests extends BaseGeometryTestCase { + + @Override + protected MultiLine createTestInstance() { + int size = randomIntBetween(1, 10); + List arr = new ArrayList(); + for (int i = 0; i < size; i++) { + arr.add(randomLine()); + } + return new MultiLine(arr); + } + + public void testBasicSerialization() throws IOException, ParseException { + assertEquals("multilinestring ((3.0 1.0, 4.0 2.0))", WellKnownText.toWKT( + new MultiLine(Collections.singletonList(new Line(new double[]{1, 2}, new double[]{3, 4}))))); + assertEquals(new MultiLine(Collections.singletonList(new Line(new double[]{1, 2}, new double[]{3, 4}))), + WellKnownText.fromWKT("multilinestring ((3 1, 4 2))")); + + assertEquals("multilinestring EMPTY", WellKnownText.toWKT(MultiLine.EMPTY)); + assertEquals(MultiLine.EMPTY, WellKnownText.fromWKT("multilinestring EMPTY)")); + } +} diff --git a/libs/geo/src/test/java/org/elasticsearch/geo/geometry/MultiPointTests.java b/libs/geo/src/test/java/org/elasticsearch/geo/geometry/MultiPointTests.java new file mode 100644 index 00000000000..81c8c6f3fac --- /dev/null +++ b/libs/geo/src/test/java/org/elasticsearch/geo/geometry/MultiPointTests.java @@ -0,0 +1,51 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you 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 org.elasticsearch.geo.geometry; + +import org.elasticsearch.geo.utils.WellKnownText; + +import java.io.IOException; +import java.text.ParseException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public class MultiPointTests extends BaseGeometryTestCase { + + @Override + protected MultiPoint createTestInstance() { + int size = randomIntBetween(1, 10); + List arr = new ArrayList<>(); + for (int i = 0; i < size; i++) { + arr.add(randomPoint()); + } + return new MultiPoint(arr); + } + + public void testBasicSerialization() throws IOException, ParseException { + assertEquals("multipoint (2.0 1.0)", WellKnownText.toWKT( + new MultiPoint(Collections.singletonList(new Point(1, 2))))); + assertEquals(new MultiPoint(Collections.singletonList(new Point(1 ,2))), + WellKnownText.fromWKT("multipoint (2 1)")); + + assertEquals("multipoint EMPTY", WellKnownText.toWKT(MultiPoint.EMPTY)); + assertEquals(MultiPoint.EMPTY, WellKnownText.fromWKT("multipoint EMPTY)")); + } +} diff --git a/libs/geo/src/test/java/org/elasticsearch/geo/geometry/MultiPolygonTests.java b/libs/geo/src/test/java/org/elasticsearch/geo/geometry/MultiPolygonTests.java new file mode 100644 index 00000000000..38257245603 --- /dev/null +++ b/libs/geo/src/test/java/org/elasticsearch/geo/geometry/MultiPolygonTests.java @@ -0,0 +1,53 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you 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 org.elasticsearch.geo.geometry; + +import org.elasticsearch.geo.utils.WellKnownText; + +import java.io.IOException; +import java.text.ParseException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public class MultiPolygonTests extends BaseGeometryTestCase { + + @Override + protected MultiPolygon createTestInstance() { + int size = randomIntBetween(1, 10); + List arr = new ArrayList<>(); + for (int i = 0; i < size; i++) { + arr.add(randomPolygon()); + } + return new MultiPolygon(arr); + } + + public void testBasicSerialization() throws IOException, ParseException { + assertEquals("multipolygon (((3.0 1.0, 4.0 2.0, 5.0 3.0, 3.0 1.0)))", + WellKnownText.toWKT(new MultiPolygon(Collections.singletonList( + new Polygon(new LinearRing(new double[]{1, 2, 3, 1}, new double[]{3, 4, 5, 3})))))); + assertEquals(new MultiPolygon(Collections.singletonList( + new Polygon(new LinearRing(new double[]{1, 2, 3, 1}, new double[]{3, 4, 5, 3})))), + WellKnownText.fromWKT("multipolygon (((3.0 1.0, 4.0 2.0, 5.0 3.0, 3.0 1.0)))")); + + assertEquals("multipolygon EMPTY", WellKnownText.toWKT(MultiPolygon.EMPTY)); + assertEquals(MultiPolygon.EMPTY, WellKnownText.fromWKT("multipolygon EMPTY)")); + } +} diff --git a/libs/geo/src/test/java/org/elasticsearch/geo/geometry/PointTests.java b/libs/geo/src/test/java/org/elasticsearch/geo/geometry/PointTests.java new file mode 100644 index 00000000000..bfdb369d7a8 --- /dev/null +++ b/libs/geo/src/test/java/org/elasticsearch/geo/geometry/PointTests.java @@ -0,0 +1,48 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you 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 org.elasticsearch.geo.geometry; + +import org.elasticsearch.geo.utils.WellKnownText; + +import java.io.IOException; +import java.text.ParseException; + +public class PointTests extends BaseGeometryTestCase { + @Override + protected Point createTestInstance() { + return randomPoint(); + } + + 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 EMPTY", WellKnownText.toWKT(Point.EMPTY)); + assertEquals(Point.EMPTY, WellKnownText.fromWKT("point EMPTY)")); + } + + public void testInitValidation() { + IllegalArgumentException ex = expectThrows(IllegalArgumentException.class, () -> new Point(100, 10)); + assertEquals("invalid latitude 100.0; must be between -90.0 and 90.0", ex.getMessage()); + + ex = expectThrows(IllegalArgumentException.class, () -> new Point(10, 500)); + assertEquals("invalid longitude 500.0; must be between -180.0 and 180.0", ex.getMessage()); + } +} diff --git a/libs/geo/src/test/java/org/elasticsearch/geo/geometry/PolygonTests.java b/libs/geo/src/test/java/org/elasticsearch/geo/geometry/PolygonTests.java new file mode 100644 index 00000000000..69a6d232083 --- /dev/null +++ b/libs/geo/src/test/java/org/elasticsearch/geo/geometry/PolygonTests.java @@ -0,0 +1,52 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you 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 org.elasticsearch.geo.geometry; + +import org.elasticsearch.geo.utils.WellKnownText; + +import java.io.IOException; +import java.text.ParseException; + +public class PolygonTests extends BaseGeometryTestCase { + @Override + protected Polygon createTestInstance() { + return randomPolygon(); + } + + public void testBasicSerialization() throws IOException, ParseException { + assertEquals("polygon ((3.0 1.0, 4.0 2.0, 5.0 3.0, 3.0 1.0))", + WellKnownText.toWKT(new Polygon(new LinearRing(new double[]{1, 2, 3, 1}, new double[]{3, 4, 5, 3})))); + 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 EMPTY", WellKnownText.toWKT(Polygon.EMPTY)); + assertEquals(Polygon.EMPTY, WellKnownText.fromWKT("polygon EMPTY)")); + } + + public void testInitValidation() { + IllegalArgumentException ex = expectThrows(IllegalArgumentException.class, + () -> new Polygon(new LinearRing(new double[]{1, 2, 1}, new double[]{3, 4, 3}))); + assertEquals("at least 4 polygon points required", ex.getMessage()); + + 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()); + } +} diff --git a/libs/geo/src/test/java/org/elasticsearch/geo/geometry/RectangleTests.java b/libs/geo/src/test/java/org/elasticsearch/geo/geometry/RectangleTests.java new file mode 100644 index 00000000000..fa5cbcd0a8f --- /dev/null +++ b/libs/geo/src/test/java/org/elasticsearch/geo/geometry/RectangleTests.java @@ -0,0 +1,51 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you 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 org.elasticsearch.geo.geometry; + +import org.elasticsearch.geo.utils.WellKnownText; + +import java.io.IOException; +import java.text.ParseException; + +public class RectangleTests extends BaseGeometryTestCase { + @Override + protected Rectangle createTestInstance() { + return randomRectangle(); + } + + public void testBasicSerialization() throws IOException, ParseException { + assertEquals("bbox (10.0, 20.0, 40.0, 30.0)", WellKnownText.toWKT(new Rectangle(30, 40, 10, 20))); + assertEquals(new Rectangle(30, 40, 10, 20), WellKnownText.fromWKT("bbox (10.0, 20.0, 40.0, 30.0)")); + + assertEquals("bbox EMPTY", WellKnownText.toWKT(Rectangle.EMPTY)); + assertEquals(Rectangle.EMPTY, WellKnownText.fromWKT("bbox EMPTY)")); + } + + public void testInitValidation() { + IllegalArgumentException ex = expectThrows(IllegalArgumentException.class, () -> new Rectangle(100, 1, 2, 3)); + assertEquals("invalid latitude 100.0; must be between -90.0 and 90.0", ex.getMessage()); + + ex = expectThrows(IllegalArgumentException.class, () -> new Rectangle(1, 2, 200, 3)); + assertEquals("invalid longitude 200.0; must be between -180.0 and 180.0", ex.getMessage()); + + ex = expectThrows(IllegalArgumentException.class, () -> new Rectangle(2, 1, 2, 3)); + assertEquals("max lat cannot be less than min lat", ex.getMessage()); + } +} diff --git a/settings.gradle b/settings.gradle index 43313f7236c..40fb419b0bf 100644 --- a/settings.gradle +++ b/settings.gradle @@ -95,6 +95,7 @@ if (isEclipse) { projects << 'libs:x-content-tests' projects << 'libs:secure-sm-tests' projects << 'libs:grok-tests' + projects << 'libs:geo-tests' } include projects.toArray(new String[0]) @@ -130,7 +131,10 @@ if (isEclipse) { project(":libs:grok").buildFileName = 'eclipse-build.gradle' project(":libs:grok-tests").projectDir = new File(rootProject.projectDir, 'libs/grok/src/test') project(":libs:grok-tests").buildFileName = 'eclipse-build.gradle' -} + project(":libs:geo").projectDir = new File(rootProject.projectDir, 'libs/geo/src/main') + project(":libs:geo").buildFileName = 'eclipse-build.gradle' + project(":libs:geo-tests").projectDir = new File(rootProject.projectDir, 'libs/geo/src/test') + project(":libs:geo-tests").buildFileName = 'eclipse-build.gradle'} // look for extra plugins for elasticsearch File extraProjects = new File(rootProject.projectDir.parentFile, "${dirName}-extra") @@ -141,3 +145,4 @@ if (extraProjects.exists()) { } project(":libs:cli").name = 'elasticsearch-cli' +project(":libs:geo").name = 'elasticsearch-geo'