diff --git a/core/src/main/java/org/elasticsearch/common/geo/builders/BaseLineStringBuilder.java b/core/src/main/java/org/elasticsearch/common/geo/builders/BaseLineStringBuilder.java deleted file mode 100644 index c6536077e5f..00000000000 --- a/core/src/main/java/org/elasticsearch/common/geo/builders/BaseLineStringBuilder.java +++ /dev/null @@ -1,123 +0,0 @@ -/* - * 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.common.geo.builders; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.Arrays; - -import org.elasticsearch.common.xcontent.XContentBuilder; - -import com.spatial4j.core.shape.Shape; -import com.vividsolutions.jts.geom.Coordinate; -import com.vividsolutions.jts.geom.Geometry; -import com.vividsolutions.jts.geom.GeometryFactory; -import com.vividsolutions.jts.geom.LineString; - -public abstract class BaseLineStringBuilder> extends PointCollection { - - public BaseLineStringBuilder(ArrayList points) { - super(points); - } - - @Override - public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { - return coordinatesToXcontent(builder, false); - } - - @Override - public Shape build() { - Coordinate[] coordinates = points.toArray(new Coordinate[points.size()]); - Geometry geometry; - if(wrapdateline) { - ArrayList strings = decompose(FACTORY, coordinates, new ArrayList()); - - if(strings.size() == 1) { - geometry = strings.get(0); - } else { - LineString[] linestrings = strings.toArray(new LineString[strings.size()]); - geometry = FACTORY.createMultiLineString(linestrings); - } - - } else { - geometry = FACTORY.createLineString(coordinates); - } - return jtsGeometry(geometry); - } - - protected static ArrayList decompose(GeometryFactory factory, Coordinate[] coordinates, ArrayList strings) { - for(Coordinate[] part : decompose(+DATELINE, coordinates)) { - for(Coordinate[] line : decompose(-DATELINE, part)) { - strings.add(factory.createLineString(line)); - } - } - return strings; - } - - /** - * Decompose a linestring given as array of coordinates at a vertical line. - * - * @param dateline x-axis intercept of the vertical line - * @param coordinates coordinates forming the linestring - * @return array of linestrings given as coordinate arrays - */ - protected static Coordinate[][] decompose(double dateline, Coordinate[] coordinates) { - int offset = 0; - ArrayList parts = new ArrayList<>(); - - double shift = coordinates[0].x > DATELINE ? DATELINE : (coordinates[0].x < -DATELINE ? -DATELINE : 0); - - for (int i = 1; i < coordinates.length; i++) { - double t = intersection(coordinates[i-1], coordinates[i], dateline); - if(!Double.isNaN(t)) { - Coordinate[] part; - if(t<1) { - part = Arrays.copyOfRange(coordinates, offset, i+1); - part[part.length-1] = Edge.position(coordinates[i-1], coordinates[i], t); - coordinates[offset+i-1] = Edge.position(coordinates[i-1], coordinates[i], t); - shift(shift, part); - offset = i-1; - shift = coordinates[i].x > DATELINE ? DATELINE : (coordinates[i].x < -DATELINE ? -DATELINE : 0); - } else { - part = shift(shift, Arrays.copyOfRange(coordinates, offset, i+1)); - offset = i; - } - parts.add(part); - } - } - - if(offset == 0) { - parts.add(shift(shift, coordinates)); - } else if(offset < coordinates.length-1) { - Coordinate[] part = Arrays.copyOfRange(coordinates, offset, coordinates.length); - parts.add(shift(shift, part)); - } - return parts.toArray(new Coordinate[parts.size()][]); - } - - private static Coordinate[] shift(double shift, Coordinate...coordinates) { - if(shift != 0) { - for (int j = 0; j < coordinates.length; j++) { - coordinates[j] = new Coordinate(coordinates[j].x - 2 * shift, coordinates[j].y); - } - } - return coordinates; - } -} diff --git a/core/src/main/java/org/elasticsearch/common/geo/builders/BasePolygonBuilder.java b/core/src/main/java/org/elasticsearch/common/geo/builders/BasePolygonBuilder.java deleted file mode 100644 index cdb91b572c6..00000000000 --- a/core/src/main/java/org/elasticsearch/common/geo/builders/BasePolygonBuilder.java +++ /dev/null @@ -1,520 +0,0 @@ -/* - * 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.common.geo.builders; - -import com.spatial4j.core.exception.InvalidShapeException; -import com.spatial4j.core.shape.Shape; -import com.vividsolutions.jts.geom.Coordinate; -import com.vividsolutions.jts.geom.Geometry; -import com.vividsolutions.jts.geom.GeometryFactory; -import com.vividsolutions.jts.geom.LinearRing; -import com.vividsolutions.jts.geom.MultiPolygon; -import com.vividsolutions.jts.geom.Polygon; -import org.elasticsearch.common.collect.Tuple; -import org.elasticsearch.common.util.set.Sets; -import org.elasticsearch.common.xcontent.XContentBuilder; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Iterator; - -/** - * The {@link BasePolygonBuilder} implements the groundwork to create polygons. This contains - * Methods to wrap polygons at the dateline and building shapes from the data held by the - * builder. - * Since this Builder can be embedded to other builders (i.e. {@link MultiPolygonBuilder}) - * the class of the embedding builder is given by the generic argument E - - * @param type of the embedding class - */ -public abstract class BasePolygonBuilder> extends ShapeBuilder { - - public static final GeoShapeType TYPE = GeoShapeType.POLYGON; - - // line string defining the shell of the polygon - protected LineStringBuilder shell; - - // List of line strings defining the holes of the polygon - protected final ArrayList holes = new ArrayList<>(); - - public BasePolygonBuilder(Orientation orientation) { - super(orientation); - } - - @SuppressWarnings("unchecked") - private E thisRef() { - return (E)this; - } - - public E point(double longitude, double latitude) { - shell.point(longitude, latitude); - return thisRef(); - } - - /** - * Add a point to the shell of the polygon - * @param coordinate coordinate of the new point - * @return this - */ - public E point(Coordinate coordinate) { - shell.point(coordinate); - return thisRef(); - } - - /** - * Add a array of points to the shell of the polygon - * @param coordinates coordinates of the new points to add - * @return this - */ - public E points(Coordinate...coordinates) { - shell.points(coordinates); - return thisRef(); - } - - /** - * Add a new hole to the polygon - * @param hole linear ring defining the hole - * @return this - */ - public E hole(LineStringBuilder hole) { - holes.add(hole); - return thisRef(); - } - - /** - * Close the shell of the polygon - */ - public BasePolygonBuilder close() { - shell.close(); - return this; - } - - /** - * Validates only 1 vertex is tangential (shared) between the interior and exterior of a polygon - */ - protected void validateHole(BaseLineStringBuilder shell, BaseLineStringBuilder hole) { - HashSet exterior = Sets.newHashSet(shell.points); - HashSet interior = Sets.newHashSet(hole.points); - exterior.retainAll(interior); - if (exterior.size() >= 2) { - throw new InvalidShapeException("Invalid polygon, interior cannot share more than one point with the exterior"); - } - } - - /** - * The coordinates setup by the builder will be assembled to a polygon. The result will consist of - * a set of polygons. Each of these components holds a list of linestrings defining the polygon: the - * first set of coordinates will be used as the shell of the polygon. The others are defined to holes - * within the polygon. - * This Method also wraps the polygons at the dateline. In order to this fact the result may - * contains more polygons and less holes than defined in the builder it self. - * - * @return coordinates of the polygon - */ - public Coordinate[][][] coordinates() { - int numEdges = shell.points.size()-1; // Last point is repeated - for (int i = 0; i < holes.size(); i++) { - numEdges += holes.get(i).points.size()-1; - validateHole(shell, this.holes.get(i)); - } - - Edge[] edges = new Edge[numEdges]; - Edge[] holeComponents = new Edge[holes.size()]; - int offset = createEdges(0, orientation, shell, null, edges, 0); - for (int i = 0; i < holes.size(); i++) { - int length = createEdges(i+1, orientation, shell, this.holes.get(i), edges, offset); - holeComponents[i] = edges[offset]; - offset += length; - } - - int numHoles = holeComponents.length; - - numHoles = merge(edges, 0, intersections(+DATELINE, edges), holeComponents, numHoles); - numHoles = merge(edges, 0, intersections(-DATELINE, edges), holeComponents, numHoles); - - return compose(edges, holeComponents, numHoles); - } - - @Override - public Shape build() { - return jtsGeometry(buildGeometry(FACTORY, wrapdateline)); - } - - protected XContentBuilder coordinatesArray(XContentBuilder builder, Params params) throws IOException { - shell.coordinatesToXcontent(builder, true); - for(BaseLineStringBuilder hole : holes) { - hole.coordinatesToXcontent(builder, true); - } - return builder; - } - - @Override - public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { - builder.startObject(); - builder.field(FIELD_TYPE, TYPE.shapename); - builder.startArray(FIELD_COORDINATES); - coordinatesArray(builder, params); - builder.endArray(); - builder.endObject(); - return builder; - } - - public Geometry buildGeometry(GeometryFactory factory, boolean fixDateline) { - if(fixDateline) { - Coordinate[][][] polygons = coordinates(); - return polygons.length == 1 - ? polygon(factory, polygons[0]) - : multipolygon(factory, polygons); - } else { - return toPolygon(factory); - } - } - - public Polygon toPolygon() { - return toPolygon(FACTORY); - } - - protected Polygon toPolygon(GeometryFactory factory) { - final LinearRing shell = linearRing(factory, this.shell.points); - final LinearRing[] holes = new LinearRing[this.holes.size()]; - Iterator iterator = this.holes.iterator(); - for (int i = 0; iterator.hasNext(); i++) { - holes[i] = linearRing(factory, iterator.next().points); - } - return factory.createPolygon(shell, holes); - } - - protected static LinearRing linearRing(GeometryFactory factory, ArrayList coordinates) { - return factory.createLinearRing(coordinates.toArray(new Coordinate[coordinates.size()])); - } - - @Override - public GeoShapeType type() { - return TYPE; - } - - protected static Polygon polygon(GeometryFactory factory, Coordinate[][] polygon) { - LinearRing shell = factory.createLinearRing(polygon[0]); - LinearRing[] holes; - - if(polygon.length > 1) { - holes = new LinearRing[polygon.length-1]; - for (int i = 0; i < holes.length; i++) { - holes[i] = factory.createLinearRing(polygon[i+1]); - } - } else { - holes = null; - } - return factory.createPolygon(shell, holes); - } - - /** - * Create a Multipolygon from a set of coordinates. Each primary array contains a polygon which - * in turn contains an array of linestrings. These line Strings are represented as an array of - * coordinates. The first linestring will be the shell of the polygon the others define holes - * within the polygon. - * - * @param factory {@link GeometryFactory} to use - * @param polygons definition of polygons - * @return a new Multipolygon - */ - protected static MultiPolygon multipolygon(GeometryFactory factory, Coordinate[][][] polygons) { - Polygon[] polygonSet = new Polygon[polygons.length]; - for (int i = 0; i < polygonSet.length; i++) { - polygonSet[i] = polygon(factory, polygons[i]); - } - return factory.createMultiPolygon(polygonSet); - } - - /** - * This method sets the component id of all edges in a ring to a given id and shifts the - * coordinates of this component according to the dateline - * - * @param edge An arbitrary edge of the component - * @param id id to apply to the component - * @param edges a list of edges to which all edges of the component will be added (could be null) - * @return number of edges that belong to this component - */ - private static int component(final Edge edge, final int id, final ArrayList edges) { - // find a coordinate that is not part of the dateline - Edge any = edge; - while(any.coordinate.x == +DATELINE || any.coordinate.x == -DATELINE) { - if((any = any.next) == edge) { - break; - } - } - - double shiftOffset = any.coordinate.x > DATELINE ? DATELINE : (any.coordinate.x < -DATELINE ? -DATELINE : 0); - if (debugEnabled()) { - LOGGER.debug("shift: {[]}", shiftOffset); - } - - // run along the border of the component, collect the - // edges, shift them according to the dateline and - // update the component id - int length = 0, connectedComponents = 0; - // if there are two connected components, splitIndex keeps track of where to split the edge array - // start at 1 since the source coordinate is shared - int splitIndex = 1; - Edge current = edge; - Edge prev = edge; - // bookkeep the source and sink of each visited coordinate - HashMap> visitedEdge = new HashMap<>(); - do { - current.coordinate = shift(current.coordinate, shiftOffset); - current.component = id; - - if (edges != null) { - // found a closed loop - we have two connected components so we need to slice into two distinct components - if (visitedEdge.containsKey(current.coordinate)) { - if (connectedComponents > 0 && current.next != edge) { - throw new InvalidShapeException("Shape contains more than one shared point"); - } - - // a negative id flags the edge as visited for the edges(...) method. - // since we're splitting connected components, we want the edges method to visit - // the newly separated component - final int visitID = -id; - Edge firstAppearance = visitedEdge.get(current.coordinate).v2(); - // correct the graph pointers by correcting the 'next' pointer for both the - // first appearance and this appearance of the edge - Edge temp = firstAppearance.next; - firstAppearance.next = current.next; - current.next = temp; - current.component = visitID; - // backtrack until we get back to this coordinate, setting the visit id to - // a non-visited value (anything positive) - do { - prev.component = visitID; - prev = visitedEdge.get(prev.coordinate).v1(); - ++splitIndex; - } while (!current.coordinate.equals(prev.coordinate)); - ++connectedComponents; - } else { - visitedEdge.put(current.coordinate, new Tuple(prev, current)); - } - edges.add(current); - prev = current; - } - length++; - } while(connectedComponents == 0 && (current = current.next) != edge); - - return (splitIndex != 1) ? length-splitIndex: length; - } - - /** - * Compute all coordinates of a component - * @param component an arbitrary edge of the component - * @param coordinates Array of coordinates to write the result to - * @return the coordinates parameter - */ - private static Coordinate[] coordinates(Edge component, Coordinate[] coordinates) { - for (int i = 0; i < coordinates.length; i++) { - coordinates[i] = (component = component.next).coordinate; - } - return coordinates; - } - - private static Coordinate[][][] buildCoordinates(ArrayList> components) { - Coordinate[][][] result = new Coordinate[components.size()][][]; - for (int i = 0; i < result.length; i++) { - ArrayList component = components.get(i); - result[i] = component.toArray(new Coordinate[component.size()][]); - } - - if(debugEnabled()) { - for (int i = 0; i < result.length; i++) { - LOGGER.debug("Component {[]}:", i); - for (int j = 0; j < result[i].length; j++) { - LOGGER.debug("\t" + Arrays.toString(result[i][j])); - } - } - } - - return result; - } - - private static final Coordinate[][] EMPTY = new Coordinate[0][]; - - private static Coordinate[][] holes(Edge[] holes, int numHoles) { - if (numHoles == 0) { - return EMPTY; - } - final Coordinate[][] points = new Coordinate[numHoles][]; - - for (int i = 0; i < numHoles; i++) { - int length = component(holes[i], -(i+1), null); // mark as visited by inverting the sign - points[i] = coordinates(holes[i], new Coordinate[length+1]); - } - - return points; - } - - private static Edge[] edges(Edge[] edges, int numHoles, ArrayList> components) { - ArrayList mainEdges = new ArrayList<>(edges.length); - - for (int i = 0; i < edges.length; i++) { - if (edges[i].component >= 0) { - int length = component(edges[i], -(components.size()+numHoles+1), mainEdges); - ArrayList component = new ArrayList<>(); - component.add(coordinates(edges[i], new Coordinate[length+1])); - components.add(component); - } - } - - return mainEdges.toArray(new Edge[mainEdges.size()]); - } - - private static Coordinate[][][] compose(Edge[] edges, Edge[] holes, int numHoles) { - final ArrayList> components = new ArrayList<>(); - assign(holes, holes(holes, numHoles), numHoles, edges(edges, numHoles, components), components); - return buildCoordinates(components); - } - - private static void assign(Edge[] holes, Coordinate[][] points, int numHoles, Edge[] edges, ArrayList> components) { - // Assign Hole to related components - // To find the new component the hole belongs to all intersections of the - // polygon edges with a vertical line are calculated. This vertical line - // is an arbitrary point of the hole. The polygon edge next to this point - // is part of the polygon the hole belongs to. - if (debugEnabled()) { - LOGGER.debug("Holes: " + Arrays.toString(holes)); - } - for (int i = 0; i < numHoles; i++) { - final Edge current = new Edge(holes[i].coordinate, holes[i].next); - // the edge intersects with itself at its own coordinate. We need intersect to be set this way so the binary search - // will get the correct position in the edge list and therefore the correct component to add the hole - current.intersect = current.coordinate; - final int intersections = intersections(current.coordinate.x, edges); - // if no intersection is found then the hole is not within the polygon, so - // don't waste time calling a binary search - final int pos; - boolean sharedVertex = false; - if (intersections == 0 || ((pos = Arrays.binarySearch(edges, 0, intersections, current, INTERSECTION_ORDER)) >= 0) - && !(sharedVertex = (edges[pos].intersect.compareTo(current.coordinate) == 0)) ) { - throw new InvalidShapeException("Invalid shape: Hole is not within polygon"); - } - final int index = -((sharedVertex) ? 0 : pos+2); - final int component = -edges[index].component - numHoles - 1; - - if(debugEnabled()) { - LOGGER.debug("\tposition ("+index+") of edge "+current+": " + edges[index]); - LOGGER.debug("\tComponent: " + component); - LOGGER.debug("\tHole intersections ("+current.coordinate.x+"): " + Arrays.toString(edges)); - } - - components.get(component).add(points[i]); - } - } - - private static int merge(Edge[] intersections, int offset, int length, Edge[] holes, int numHoles) { - // Intersections appear pairwise. On the first edge the inner of - // of the polygon is entered. On the second edge the outer face - // is entered. Other kinds of intersections are discard by the - // intersection function - - for (int i = 0; i < length; i += 2) { - Edge e1 = intersections[offset + i + 0]; - Edge e2 = intersections[offset + i + 1]; - - // If two segments are connected maybe a hole must be deleted - // Since Edges of components appear pairwise we need to check - // the second edge only (the first edge is either polygon or - // already handled) - if (e2.component > 0) { - //TODO: Check if we could save the set null step - numHoles--; - holes[e2.component-1] = holes[numHoles]; - holes[numHoles] = null; - } - // only connect edges if intersections are pairwise - // 1. per the comment above, the edge array is sorted by y-value of the intersection - // with the dateline. Two edges have the same y intercept when they cross the - // dateline thus they appear sequentially (pairwise) in the edge array. Two edges - // do not have the same y intercept when we're forming a multi-poly from a poly - // that wraps the dateline (but there are 2 ordered intercepts). - // The connect method creates a new edge for these paired edges in the linked list. - // For boundary conditions (e.g., intersect but not crossing) there is no sibling edge - // to connect. Thus the first logic check enforces the pairwise rule - // 2. the second logic check ensures the two candidate edges aren't already connected by an - // existing edge along the dateline - this is necessary due to a logic change in - // ShapeBuilder.intersection that computes dateline edges as valid intersect points - // in support of OGC standards - if (e1.intersect != Edge.MAX_COORDINATE && e2.intersect != Edge.MAX_COORDINATE - && !(e1.next.next.coordinate.equals3D(e2.coordinate) && Math.abs(e1.next.coordinate.x) == DATELINE - && Math.abs(e2.coordinate.x) == DATELINE) ) { - connect(e1, e2); - } - } - return numHoles; - } - - private static void connect(Edge in, Edge out) { - assert in != null && out != null; - assert in != out; - // Connecting two Edges by inserting the point at - // dateline intersection and connect these by adding - // two edges between this points. One per direction - if(in.intersect != in.next.coordinate) { - // NOTE: the order of the object creation is crucial here! Don't change it! - // first edge has no point on dateline - Edge e1 = new Edge(in.intersect, in.next); - - if(out.intersect != out.next.coordinate) { - // second edge has no point on dateline - Edge e2 = new Edge(out.intersect, out.next); - in.next = new Edge(in.intersect, e2, in.intersect); - } else { - // second edge intersects with dateline - in.next = new Edge(in.intersect, out.next, in.intersect); - } - out.next = new Edge(out.intersect, e1, out.intersect); - } else if (in.next != out && in.coordinate != out.intersect) { - // first edge intersects with dateline - Edge e2 = new Edge(out.intersect, in.next, out.intersect); - - if(out.intersect != out.next.coordinate) { - // second edge has no point on dateline - Edge e1 = new Edge(out.intersect, out.next); - in.next = new Edge(in.intersect, e1, in.intersect); - - } else { - // second edge intersects with dateline - in.next = new Edge(in.intersect, out.next, in.intersect); - } - out.next = e2; - } - } - - private static int createEdges(int component, Orientation orientation, BaseLineStringBuilder shell, - BaseLineStringBuilder hole, - Edge[] edges, int offset) { - // inner rings (holes) have an opposite direction than the outer rings - // XOR will invert the orientation for outer ring cases (Truth Table:, T/T = F, T/F = T, F/T = T, F/F = F) - boolean direction = (component == 0 ^ orientation == Orientation.RIGHT); - // set the points array accordingly (shell or hole) - Coordinate[] points = (hole != null) ? hole.coordinates(false) : shell.coordinates(false); - Edge.ring(component, direction, orientation == Orientation.LEFT, shell, points, 0, edges, offset, points.length-1); - return points.length-1; - } -} diff --git a/core/src/main/java/org/elasticsearch/common/geo/builders/GeometryCollectionBuilder.java b/core/src/main/java/org/elasticsearch/common/geo/builders/GeometryCollectionBuilder.java index 28018431cb2..57f3fc67b64 100644 --- a/core/src/main/java/org/elasticsearch/common/geo/builders/GeometryCollectionBuilder.java +++ b/core/src/main/java/org/elasticsearch/common/geo/builders/GeometryCollectionBuilder.java @@ -57,7 +57,7 @@ public class GeometryCollectionBuilder extends ShapeBuilder { return this; } - public GeometryCollectionBuilder line(BaseLineStringBuilder line) { + public GeometryCollectionBuilder line(LineStringBuilder line) { this.shapes.add(line); return this; } @@ -67,7 +67,7 @@ public class GeometryCollectionBuilder extends ShapeBuilder { return this; } - public GeometryCollectionBuilder polygon(BasePolygonBuilder polygon) { + public GeometryCollectionBuilder polygon(PolygonBuilder polygon) { this.shapes.add(polygon); return this; } diff --git a/core/src/main/java/org/elasticsearch/common/geo/builders/LineStringBuilder.java b/core/src/main/java/org/elasticsearch/common/geo/builders/LineStringBuilder.java index 6d40ffb4610..265efe11621 100644 --- a/core/src/main/java/org/elasticsearch/common/geo/builders/LineStringBuilder.java +++ b/core/src/main/java/org/elasticsearch/common/geo/builders/LineStringBuilder.java @@ -19,25 +19,23 @@ package org.elasticsearch.common.geo.builders; -import com.vividsolutions.jts.geom.Coordinate; - -import org.elasticsearch.common.xcontent.XContentBuilder; - import java.io.IOException; import java.util.ArrayList; +import java.util.Arrays; -public class LineStringBuilder extends BaseLineStringBuilder { +import org.elasticsearch.common.xcontent.XContentBuilder; +import com.spatial4j.core.shape.Shape; +import com.vividsolutions.jts.geom.Coordinate; +import com.vividsolutions.jts.geom.Geometry; +import com.vividsolutions.jts.geom.GeometryFactory; +import com.vividsolutions.jts.geom.LineString; - public LineStringBuilder() { - this(new ArrayList()); - } - - public LineStringBuilder(ArrayList points) { - super(points); - } +public class LineStringBuilder extends PointCollection { public static final GeoShapeType TYPE = GeoShapeType.LINESTRING; + protected boolean translated = false; + @Override public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { builder.startObject(); @@ -48,11 +46,6 @@ public class LineStringBuilder extends BaseLineStringBuilder return builder; } - @Override - public GeoShapeType type() { - return TYPE; - } - /** * Closes the current lineString by adding the starting point as the end point */ @@ -65,4 +58,87 @@ public class LineStringBuilder extends BaseLineStringBuilder return this; } + @Override + public GeoShapeType type() { + return TYPE; + } + + @Override + public Shape build() { + Coordinate[] coordinates = points.toArray(new Coordinate[points.size()]); + Geometry geometry; + if(wrapdateline) { + ArrayList strings = decompose(FACTORY, coordinates, new ArrayList()); + + if(strings.size() == 1) { + geometry = strings.get(0); + } else { + LineString[] linestrings = strings.toArray(new LineString[strings.size()]); + geometry = FACTORY.createMultiLineString(linestrings); + } + + } else { + geometry = FACTORY.createLineString(coordinates); + } + return jtsGeometry(geometry); + } + + static ArrayList decompose(GeometryFactory factory, Coordinate[] coordinates, ArrayList strings) { + for(Coordinate[] part : decompose(+DATELINE, coordinates)) { + for(Coordinate[] line : decompose(-DATELINE, part)) { + strings.add(factory.createLineString(line)); + } + } + return strings; + } + + /** + * Decompose a linestring given as array of coordinates at a vertical line. + * + * @param dateline x-axis intercept of the vertical line + * @param coordinates coordinates forming the linestring + * @return array of linestrings given as coordinate arrays + */ + private static Coordinate[][] decompose(double dateline, Coordinate[] coordinates) { + int offset = 0; + ArrayList parts = new ArrayList<>(); + + double shift = coordinates[0].x > DATELINE ? DATELINE : (coordinates[0].x < -DATELINE ? -DATELINE : 0); + + for (int i = 1; i < coordinates.length; i++) { + double t = intersection(coordinates[i-1], coordinates[i], dateline); + if(!Double.isNaN(t)) { + Coordinate[] part; + if(t<1) { + part = Arrays.copyOfRange(coordinates, offset, i+1); + part[part.length-1] = Edge.position(coordinates[i-1], coordinates[i], t); + coordinates[offset+i-1] = Edge.position(coordinates[i-1], coordinates[i], t); + shift(shift, part); + offset = i-1; + shift = coordinates[i].x > DATELINE ? DATELINE : (coordinates[i].x < -DATELINE ? -DATELINE : 0); + } else { + part = shift(shift, Arrays.copyOfRange(coordinates, offset, i+1)); + offset = i; + } + parts.add(part); + } + } + + if(offset == 0) { + parts.add(shift(shift, coordinates)); + } else if(offset < coordinates.length-1) { + Coordinate[] part = Arrays.copyOfRange(coordinates, offset, coordinates.length); + parts.add(shift(shift, part)); + } + return parts.toArray(new Coordinate[parts.size()][]); + } + + private static Coordinate[] shift(double shift, Coordinate...coordinates) { + if(shift != 0) { + for (int j = 0; j < coordinates.length; j++) { + coordinates[j] = new Coordinate(coordinates[j].x - 2 * shift, coordinates[j].y); + } + } + return coordinates; + } } diff --git a/core/src/main/java/org/elasticsearch/common/geo/builders/MultiLineStringBuilder.java b/core/src/main/java/org/elasticsearch/common/geo/builders/MultiLineStringBuilder.java index 7dea65ad825..10ad25c89e1 100644 --- a/core/src/main/java/org/elasticsearch/common/geo/builders/MultiLineStringBuilder.java +++ b/core/src/main/java/org/elasticsearch/common/geo/builders/MultiLineStringBuilder.java @@ -60,7 +60,7 @@ public class MultiLineStringBuilder extends ShapeBuilder { builder.field(FIELD_TYPE, TYPE.shapename); builder.field(FIELD_COORDINATES); builder.startArray(); - for(BaseLineStringBuilder line : lines) { + for(LineStringBuilder line : lines) { line.coordinatesToXcontent(builder, false); } builder.endArray(); @@ -73,8 +73,8 @@ public class MultiLineStringBuilder extends ShapeBuilder { final Geometry geometry; if(wrapdateline) { ArrayList parts = new ArrayList<>(); - for (BaseLineStringBuilder line : lines) { - BaseLineStringBuilder.decompose(FACTORY, line.coordinates(false), parts); + for (LineStringBuilder line : lines) { + LineStringBuilder.decompose(FACTORY, line.coordinates(false), parts); } if(parts.size() == 1) { geometry = parts.get(0); diff --git a/core/src/main/java/org/elasticsearch/common/geo/builders/MultiPointBuilder.java b/core/src/main/java/org/elasticsearch/common/geo/builders/MultiPointBuilder.java index 5a9aaa90927..d12baad70d9 100644 --- a/core/src/main/java/org/elasticsearch/common/geo/builders/MultiPointBuilder.java +++ b/core/src/main/java/org/elasticsearch/common/geo/builders/MultiPointBuilder.java @@ -31,6 +31,7 @@ import java.util.List; public class MultiPointBuilder extends PointCollection { + public static final GeoShapeType TYPE = GeoShapeType.MULTIPOINT; @Override diff --git a/core/src/main/java/org/elasticsearch/common/geo/builders/MultiPolygonBuilder.java b/core/src/main/java/org/elasticsearch/common/geo/builders/MultiPolygonBuilder.java index b2d5fd85a93..0998cd2944b 100644 --- a/core/src/main/java/org/elasticsearch/common/geo/builders/MultiPolygonBuilder.java +++ b/core/src/main/java/org/elasticsearch/common/geo/builders/MultiPolygonBuilder.java @@ -33,7 +33,7 @@ public class MultiPolygonBuilder extends ShapeBuilder { public static final GeoShapeType TYPE = GeoShapeType.MULTIPOLYGON; - protected final ArrayList> polygons = new ArrayList<>(); + protected final ArrayList polygons = new ArrayList<>(); public MultiPolygonBuilder() { this(Orientation.RIGHT); @@ -43,7 +43,7 @@ public class MultiPolygonBuilder extends ShapeBuilder { super(orientation); } - public MultiPolygonBuilder polygon(BasePolygonBuilder polygon) { + public MultiPolygonBuilder polygon(PolygonBuilder polygon) { this.polygons.add(polygon); return this; } @@ -53,7 +53,7 @@ public class MultiPolygonBuilder extends ShapeBuilder { builder.startObject(); builder.field(FIELD_TYPE, TYPE.shapename); builder.startArray(FIELD_COORDINATES); - for(BasePolygonBuilder polygon : polygons) { + for(PolygonBuilder polygon : polygons) { builder.startArray(); polygon.coordinatesArray(builder, params); builder.endArray(); @@ -73,13 +73,13 @@ public class MultiPolygonBuilder extends ShapeBuilder { List shapes = new ArrayList<>(this.polygons.size()); if(wrapdateline) { - for (BasePolygonBuilder polygon : this.polygons) { + for (PolygonBuilder polygon : this.polygons) { for(Coordinate[][] part : polygon.coordinates()) { shapes.add(jtsGeometry(PolygonBuilder.polygon(FACTORY, part))); } } } else { - for (BasePolygonBuilder polygon : this.polygons) { + for (PolygonBuilder polygon : this.polygons) { shapes.add(jtsGeometry(polygon.toPolygon(FACTORY))); } } diff --git a/core/src/main/java/org/elasticsearch/common/geo/builders/PointCollection.java b/core/src/main/java/org/elasticsearch/common/geo/builders/PointCollection.java index 1bd9174db39..45ce5adb595 100644 --- a/core/src/main/java/org/elasticsearch/common/geo/builders/PointCollection.java +++ b/core/src/main/java/org/elasticsearch/common/geo/builders/PointCollection.java @@ -34,7 +34,6 @@ import com.vividsolutions.jts.geom.Coordinate; public abstract class PointCollection> extends ShapeBuilder { protected final ArrayList points; - protected boolean translated = false; protected PointCollection() { this(new ArrayList()); diff --git a/core/src/main/java/org/elasticsearch/common/geo/builders/PolygonBuilder.java b/core/src/main/java/org/elasticsearch/common/geo/builders/PolygonBuilder.java index ad41309c1f4..4a406eb22b8 100644 --- a/core/src/main/java/org/elasticsearch/common/geo/builders/PolygonBuilder.java +++ b/core/src/main/java/org/elasticsearch/common/geo/builders/PolygonBuilder.java @@ -19,11 +19,40 @@ package org.elasticsearch.common.geo.builders; -import java.util.ArrayList; - +import com.spatial4j.core.exception.InvalidShapeException; +import com.spatial4j.core.shape.Shape; import com.vividsolutions.jts.geom.Coordinate; +import com.vividsolutions.jts.geom.Geometry; +import com.vividsolutions.jts.geom.GeometryFactory; +import com.vividsolutions.jts.geom.LinearRing; +import com.vividsolutions.jts.geom.MultiPolygon; +import com.vividsolutions.jts.geom.Polygon; -public class PolygonBuilder extends BasePolygonBuilder { +import org.elasticsearch.common.collect.Tuple; +import org.elasticsearch.common.util.set.Sets; +import org.elasticsearch.common.xcontent.XContentBuilder; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; + +/** + * The {@link PolygonBuilder} implements the groundwork to create polygons. This contains + * Methods to wrap polygons at the dateline and building shapes from the data held by the + * builder. + */ +public class PolygonBuilder extends ShapeBuilder { + + public static final GeoShapeType TYPE = GeoShapeType.POLYGON; + + // line string defining the shell of the polygon + private LineStringBuilder shell; + + // List of line strings defining the holes of the polygon + private final ArrayList holes = new ArrayList<>(); public PolygonBuilder() { this(new ArrayList(), Orientation.RIGHT); @@ -33,14 +62,460 @@ public class PolygonBuilder extends BasePolygonBuilder { this(new ArrayList(), orientation); } - protected PolygonBuilder(ArrayList points, Orientation orientation) { + public PolygonBuilder(ArrayList points, Orientation orientation) { super(orientation); - this.shell = new LineStringBuilder(points); + this.shell = new LineStringBuilder().points(points); + } + + public PolygonBuilder point(double longitude, double latitude) { + shell.point(longitude, latitude); + return this; + } + + /** + * Add a point to the shell of the polygon + * @param coordinate coordinate of the new point + * @return this + */ + public PolygonBuilder point(Coordinate coordinate) { + shell.point(coordinate); + return this; + } + + /** + * Add an array of points to the shell of the polygon + * @param coordinates coordinates of the new points to add + * @return this + */ + public PolygonBuilder points(Coordinate...coordinates) { + shell.points(coordinates); + return this; + } + + /** + * Add a new hole to the polygon + * @param hole linear ring defining the hole + * @return this + */ + public PolygonBuilder hole(LineStringBuilder hole) { + holes.add(hole); + return this; + } + + /** + * Close the shell of the polygon + */ + public PolygonBuilder close() { + shell.close(); + return this; + } + + /** + * Validates only 1 vertex is tangential (shared) between the interior and exterior of a polygon + */ + protected void validateHole(LineStringBuilder shell, LineStringBuilder hole) { + HashSet exterior = Sets.newHashSet(shell.points); + HashSet interior = Sets.newHashSet(hole.points); + exterior.retainAll(interior); + if (exterior.size() >= 2) { + throw new InvalidShapeException("Invalid polygon, interior cannot share more than one point with the exterior"); + } + } + + /** + * The coordinates setup by the builder will be assembled to a polygon. The result will consist of + * a set of polygons. Each of these components holds a list of linestrings defining the polygon: the + * first set of coordinates will be used as the shell of the polygon. The others are defined to holes + * within the polygon. + * This Method also wraps the polygons at the dateline. In order to this fact the result may + * contains more polygons and less holes than defined in the builder it self. + * + * @return coordinates of the polygon + */ + public Coordinate[][][] coordinates() { + int numEdges = shell.points.size()-1; // Last point is repeated + for (int i = 0; i < holes.size(); i++) { + numEdges += holes.get(i).points.size()-1; + validateHole(shell, this.holes.get(i)); + } + + Edge[] edges = new Edge[numEdges]; + Edge[] holeComponents = new Edge[holes.size()]; + int offset = createEdges(0, orientation, shell, null, edges, 0); + for (int i = 0; i < holes.size(); i++) { + int length = createEdges(i+1, orientation, shell, this.holes.get(i), edges, offset); + holeComponents[i] = edges[offset]; + offset += length; + } + + int numHoles = holeComponents.length; + + numHoles = merge(edges, 0, intersections(+DATELINE, edges), holeComponents, numHoles); + numHoles = merge(edges, 0, intersections(-DATELINE, edges), holeComponents, numHoles); + + return compose(edges, holeComponents, numHoles); } @Override - public PolygonBuilder close() { - super.close(); - return this; + public Shape build() { + return jtsGeometry(buildGeometry(FACTORY, wrapdateline)); + } + + protected XContentBuilder coordinatesArray(XContentBuilder builder, Params params) throws IOException { + shell.coordinatesToXcontent(builder, true); + for(LineStringBuilder hole : holes) { + hole.coordinatesToXcontent(builder, true); + } + return builder; + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + builder.startObject(); + builder.field(FIELD_TYPE, TYPE.shapename); + builder.startArray(FIELD_COORDINATES); + coordinatesArray(builder, params); + builder.endArray(); + builder.endObject(); + return builder; + } + + public Geometry buildGeometry(GeometryFactory factory, boolean fixDateline) { + if(fixDateline) { + Coordinate[][][] polygons = coordinates(); + return polygons.length == 1 + ? polygon(factory, polygons[0]) + : multipolygon(factory, polygons); + } else { + return toPolygon(factory); + } + } + + public Polygon toPolygon() { + return toPolygon(FACTORY); + } + + protected Polygon toPolygon(GeometryFactory factory) { + final LinearRing shell = linearRing(factory, this.shell.points); + final LinearRing[] holes = new LinearRing[this.holes.size()]; + Iterator iterator = this.holes.iterator(); + for (int i = 0; iterator.hasNext(); i++) { + holes[i] = linearRing(factory, iterator.next().points); + } + return factory.createPolygon(shell, holes); + } + + protected static LinearRing linearRing(GeometryFactory factory, ArrayList coordinates) { + return factory.createLinearRing(coordinates.toArray(new Coordinate[coordinates.size()])); + } + + @Override + public GeoShapeType type() { + return TYPE; + } + + protected static Polygon polygon(GeometryFactory factory, Coordinate[][] polygon) { + LinearRing shell = factory.createLinearRing(polygon[0]); + LinearRing[] holes; + + if(polygon.length > 1) { + holes = new LinearRing[polygon.length-1]; + for (int i = 0; i < holes.length; i++) { + holes[i] = factory.createLinearRing(polygon[i+1]); + } + } else { + holes = null; + } + return factory.createPolygon(shell, holes); + } + + /** + * Create a Multipolygon from a set of coordinates. Each primary array contains a polygon which + * in turn contains an array of linestrings. These line Strings are represented as an array of + * coordinates. The first linestring will be the shell of the polygon the others define holes + * within the polygon. + * + * @param factory {@link GeometryFactory} to use + * @param polygons definition of polygons + * @return a new Multipolygon + */ + protected static MultiPolygon multipolygon(GeometryFactory factory, Coordinate[][][] polygons) { + Polygon[] polygonSet = new Polygon[polygons.length]; + for (int i = 0; i < polygonSet.length; i++) { + polygonSet[i] = polygon(factory, polygons[i]); + } + return factory.createMultiPolygon(polygonSet); + } + + /** + * This method sets the component id of all edges in a ring to a given id and shifts the + * coordinates of this component according to the dateline + * + * @param edge An arbitrary edge of the component + * @param id id to apply to the component + * @param edges a list of edges to which all edges of the component will be added (could be null) + * @return number of edges that belong to this component + */ + private static int component(final Edge edge, final int id, final ArrayList edges) { + // find a coordinate that is not part of the dateline + Edge any = edge; + while(any.coordinate.x == +DATELINE || any.coordinate.x == -DATELINE) { + if((any = any.next) == edge) { + break; + } + } + + double shiftOffset = any.coordinate.x > DATELINE ? DATELINE : (any.coordinate.x < -DATELINE ? -DATELINE : 0); + if (debugEnabled()) { + LOGGER.debug("shift: {[]}", shiftOffset); + } + + // run along the border of the component, collect the + // edges, shift them according to the dateline and + // update the component id + int length = 0, connectedComponents = 0; + // if there are two connected components, splitIndex keeps track of where to split the edge array + // start at 1 since the source coordinate is shared + int splitIndex = 1; + Edge current = edge; + Edge prev = edge; + // bookkeep the source and sink of each visited coordinate + HashMap> visitedEdge = new HashMap<>(); + do { + current.coordinate = shift(current.coordinate, shiftOffset); + current.component = id; + + if (edges != null) { + // found a closed loop - we have two connected components so we need to slice into two distinct components + if (visitedEdge.containsKey(current.coordinate)) { + if (connectedComponents > 0 && current.next != edge) { + throw new InvalidShapeException("Shape contains more than one shared point"); + } + + // a negative id flags the edge as visited for the edges(...) method. + // since we're splitting connected components, we want the edges method to visit + // the newly separated component + final int visitID = -id; + Edge firstAppearance = visitedEdge.get(current.coordinate).v2(); + // correct the graph pointers by correcting the 'next' pointer for both the + // first appearance and this appearance of the edge + Edge temp = firstAppearance.next; + firstAppearance.next = current.next; + current.next = temp; + current.component = visitID; + // backtrack until we get back to this coordinate, setting the visit id to + // a non-visited value (anything positive) + do { + prev.component = visitID; + prev = visitedEdge.get(prev.coordinate).v1(); + ++splitIndex; + } while (!current.coordinate.equals(prev.coordinate)); + ++connectedComponents; + } else { + visitedEdge.put(current.coordinate, new Tuple(prev, current)); + } + edges.add(current); + prev = current; + } + length++; + } while(connectedComponents == 0 && (current = current.next) != edge); + + return (splitIndex != 1) ? length-splitIndex: length; + } + + /** + * Compute all coordinates of a component + * @param component an arbitrary edge of the component + * @param coordinates Array of coordinates to write the result to + * @return the coordinates parameter + */ + private static Coordinate[] coordinates(Edge component, Coordinate[] coordinates) { + for (int i = 0; i < coordinates.length; i++) { + coordinates[i] = (component = component.next).coordinate; + } + return coordinates; + } + + private static Coordinate[][][] buildCoordinates(ArrayList> components) { + Coordinate[][][] result = new Coordinate[components.size()][][]; + for (int i = 0; i < result.length; i++) { + ArrayList component = components.get(i); + result[i] = component.toArray(new Coordinate[component.size()][]); + } + + if(debugEnabled()) { + for (int i = 0; i < result.length; i++) { + LOGGER.debug("Component {[]}:", i); + for (int j = 0; j < result[i].length; j++) { + LOGGER.debug("\t" + Arrays.toString(result[i][j])); + } + } + } + + return result; + } + + private static final Coordinate[][] EMPTY = new Coordinate[0][]; + + private static Coordinate[][] holes(Edge[] holes, int numHoles) { + if (numHoles == 0) { + return EMPTY; + } + final Coordinate[][] points = new Coordinate[numHoles][]; + + for (int i = 0; i < numHoles; i++) { + int length = component(holes[i], -(i+1), null); // mark as visited by inverting the sign + points[i] = coordinates(holes[i], new Coordinate[length+1]); + } + + return points; + } + + private static Edge[] edges(Edge[] edges, int numHoles, ArrayList> components) { + ArrayList mainEdges = new ArrayList<>(edges.length); + + for (int i = 0; i < edges.length; i++) { + if (edges[i].component >= 0) { + int length = component(edges[i], -(components.size()+numHoles+1), mainEdges); + ArrayList component = new ArrayList<>(); + component.add(coordinates(edges[i], new Coordinate[length+1])); + components.add(component); + } + } + + return mainEdges.toArray(new Edge[mainEdges.size()]); + } + + private static Coordinate[][][] compose(Edge[] edges, Edge[] holes, int numHoles) { + final ArrayList> components = new ArrayList<>(); + assign(holes, holes(holes, numHoles), numHoles, edges(edges, numHoles, components), components); + return buildCoordinates(components); + } + + private static void assign(Edge[] holes, Coordinate[][] points, int numHoles, Edge[] edges, ArrayList> components) { + // Assign Hole to related components + // To find the new component the hole belongs to all intersections of the + // polygon edges with a vertical line are calculated. This vertical line + // is an arbitrary point of the hole. The polygon edge next to this point + // is part of the polygon the hole belongs to. + if (debugEnabled()) { + LOGGER.debug("Holes: " + Arrays.toString(holes)); + } + for (int i = 0; i < numHoles; i++) { + final Edge current = new Edge(holes[i].coordinate, holes[i].next); + // the edge intersects with itself at its own coordinate. We need intersect to be set this way so the binary search + // will get the correct position in the edge list and therefore the correct component to add the hole + current.intersect = current.coordinate; + final int intersections = intersections(current.coordinate.x, edges); + // if no intersection is found then the hole is not within the polygon, so + // don't waste time calling a binary search + final int pos; + boolean sharedVertex = false; + if (intersections == 0 || ((pos = Arrays.binarySearch(edges, 0, intersections, current, INTERSECTION_ORDER)) >= 0) + && !(sharedVertex = (edges[pos].intersect.compareTo(current.coordinate) == 0)) ) { + throw new InvalidShapeException("Invalid shape: Hole is not within polygon"); + } + final int index = -((sharedVertex) ? 0 : pos+2); + final int component = -edges[index].component - numHoles - 1; + + if(debugEnabled()) { + LOGGER.debug("\tposition ("+index+") of edge "+current+": " + edges[index]); + LOGGER.debug("\tComponent: " + component); + LOGGER.debug("\tHole intersections ("+current.coordinate.x+"): " + Arrays.toString(edges)); + } + + components.get(component).add(points[i]); + } + } + + private static int merge(Edge[] intersections, int offset, int length, Edge[] holes, int numHoles) { + // Intersections appear pairwise. On the first edge the inner of + // of the polygon is entered. On the second edge the outer face + // is entered. Other kinds of intersections are discard by the + // intersection function + + for (int i = 0; i < length; i += 2) { + Edge e1 = intersections[offset + i + 0]; + Edge e2 = intersections[offset + i + 1]; + + // If two segments are connected maybe a hole must be deleted + // Since Edges of components appear pairwise we need to check + // the second edge only (the first edge is either polygon or + // already handled) + if (e2.component > 0) { + //TODO: Check if we could save the set null step + numHoles--; + holes[e2.component-1] = holes[numHoles]; + holes[numHoles] = null; + } + // only connect edges if intersections are pairwise + // 1. per the comment above, the edge array is sorted by y-value of the intersection + // with the dateline. Two edges have the same y intercept when they cross the + // dateline thus they appear sequentially (pairwise) in the edge array. Two edges + // do not have the same y intercept when we're forming a multi-poly from a poly + // that wraps the dateline (but there are 2 ordered intercepts). + // The connect method creates a new edge for these paired edges in the linked list. + // For boundary conditions (e.g., intersect but not crossing) there is no sibling edge + // to connect. Thus the first logic check enforces the pairwise rule + // 2. the second logic check ensures the two candidate edges aren't already connected by an + // existing edge along the dateline - this is necessary due to a logic change in + // ShapeBuilder.intersection that computes dateline edges as valid intersect points + // in support of OGC standards + if (e1.intersect != Edge.MAX_COORDINATE && e2.intersect != Edge.MAX_COORDINATE + && !(e1.next.next.coordinate.equals3D(e2.coordinate) && Math.abs(e1.next.coordinate.x) == DATELINE + && Math.abs(e2.coordinate.x) == DATELINE) ) { + connect(e1, e2); + } + } + return numHoles; + } + + private static void connect(Edge in, Edge out) { + assert in != null && out != null; + assert in != out; + // Connecting two Edges by inserting the point at + // dateline intersection and connect these by adding + // two edges between this points. One per direction + if(in.intersect != in.next.coordinate) { + // NOTE: the order of the object creation is crucial here! Don't change it! + // first edge has no point on dateline + Edge e1 = new Edge(in.intersect, in.next); + + if(out.intersect != out.next.coordinate) { + // second edge has no point on dateline + Edge e2 = new Edge(out.intersect, out.next); + in.next = new Edge(in.intersect, e2, in.intersect); + } else { + // second edge intersects with dateline + in.next = new Edge(in.intersect, out.next, in.intersect); + } + out.next = new Edge(out.intersect, e1, out.intersect); + } else if (in.next != out && in.coordinate != out.intersect) { + // first edge intersects with dateline + Edge e2 = new Edge(out.intersect, in.next, out.intersect); + + if(out.intersect != out.next.coordinate) { + // second edge has no point on dateline + Edge e1 = new Edge(out.intersect, out.next); + in.next = new Edge(in.intersect, e1, in.intersect); + + } else { + // second edge intersects with dateline + in.next = new Edge(in.intersect, out.next, in.intersect); + } + out.next = e2; + } + } + + private static int createEdges(int component, Orientation orientation, LineStringBuilder shell, + LineStringBuilder hole, + Edge[] edges, int offset) { + // inner rings (holes) have an opposite direction than the outer rings + // XOR will invert the orientation for outer ring cases (Truth Table:, T/T = F, T/F = T, F/T = T, F/F = F) + boolean direction = (component == 0 ^ orientation == Orientation.RIGHT); + // set the points array accordingly (shell or hole) + Coordinate[] points = (hole != null) ? hole.coordinates(false) : shell.coordinates(false); + Edge.ring(component, direction, orientation == Orientation.LEFT, shell, points, 0, edges, offset, points.length-1); + return points.length-1; } } diff --git a/core/src/main/java/org/elasticsearch/common/geo/builders/ShapeBuilder.java b/core/src/main/java/org/elasticsearch/common/geo/builders/ShapeBuilder.java index 56af856eb31..13237727173 100644 --- a/core/src/main/java/org/elasticsearch/common/geo/builders/ShapeBuilder.java +++ b/core/src/main/java/org/elasticsearch/common/geo/builders/ShapeBuilder.java @@ -444,7 +444,7 @@ public abstract class ShapeBuilder extends ToXContentToBytes { * number of points * @return Array of edges */ - protected static Edge[] ring(int component, boolean direction, boolean handedness, BaseLineStringBuilder shell, + protected static Edge[] ring(int component, boolean direction, boolean handedness, LineStringBuilder shell, Coordinate[] points, int offset, Edge[] edges, int toffset, int length) { // calculate the direction of the points: // find the point a the top of the set and check its diff --git a/docs/reference/migration/migrate_3_0.asciidoc b/docs/reference/migration/migrate_3_0.asciidoc index 667beb8b65b..08bcb380985 100644 --- a/docs/reference/migration/migrate_3_0.asciidoc +++ b/docs/reference/migration/migrate_3_0.asciidoc @@ -430,6 +430,10 @@ For simplicity, only one way of adding the ids to the existing list (empty by de error description). This will influence code that use the `IndexRequest.opType()` or `IndexRequest.create()` to index a document only if it doesn't already exist. +==== ShapeBuilders + +`InternalLineStringBuilder` is removed in favour of `LineStringBuilder`, `InternalPolygonBuilder` in favour of PolygonBuilder` and `Ring` has been replaced with `LineStringBuilder`. Also the abstract base classes `BaseLineStringBuilder` and `BasePolygonBuilder` haven been merged with their corresponding implementations. + [[breaking_30_cache_concurrency]] === Cache concurrency level settings removed