Merge pull request #14887 from cbuescher/merge-base-shapebuilders
Merging BaseLineString and BasePolygonBuilder with subclass
This commit is contained in:
commit
be9dd035e2
|
@ -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<E extends BaseLineStringBuilder<E>> extends PointCollection<E> {
|
||||
|
||||
public BaseLineStringBuilder(ArrayList<Coordinate> 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<LineString> strings = decompose(FACTORY, coordinates, new ArrayList<LineString>());
|
||||
|
||||
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<LineString> decompose(GeometryFactory factory, Coordinate[] coordinates, ArrayList<LineString> 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<Coordinate[]> 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;
|
||||
}
|
||||
}
|
|
@ -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 <code>E</code>
|
||||
|
||||
* @param <E> type of the embedding class
|
||||
*/
|
||||
public abstract class BasePolygonBuilder<E extends BasePolygonBuilder<E>> 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<LineStringBuilder> 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<LineStringBuilder> 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<Coordinate> 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 <code>null</code>)
|
||||
* @return number of edges that belong to this component
|
||||
*/
|
||||
private static int component(final Edge edge, final int id, final ArrayList<Edge> 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<Coordinate, Tuple<Edge, Edge>> 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<Edge, Edge>(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<ArrayList<Coordinate[]>> components) {
|
||||
Coordinate[][][] result = new Coordinate[components.size()][][];
|
||||
for (int i = 0; i < result.length; i++) {
|
||||
ArrayList<Coordinate[]> 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<ArrayList<Coordinate[]>> components) {
|
||||
ArrayList<Edge> 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<Coordinate[]> 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<ArrayList<Coordinate[]>> 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<ArrayList<Coordinate[]>> 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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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<LineStringBuilder> {
|
||||
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<Coordinate>());
|
||||
}
|
||||
|
||||
public LineStringBuilder(ArrayList<Coordinate> points) {
|
||||
super(points);
|
||||
}
|
||||
public class LineStringBuilder extends PointCollection<LineStringBuilder> {
|
||||
|
||||
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<LineStringBuilder>
|
|||
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<LineStringBuilder>
|
|||
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<LineString> strings = decompose(FACTORY, coordinates, new ArrayList<LineString>());
|
||||
|
||||
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<LineString> decompose(GeometryFactory factory, Coordinate[] coordinates, ArrayList<LineString> 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<Coordinate[]> 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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<LineString> 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);
|
||||
|
|
|
@ -31,6 +31,7 @@ import java.util.List;
|
|||
|
||||
public class MultiPointBuilder extends PointCollection<MultiPointBuilder> {
|
||||
|
||||
|
||||
public static final GeoShapeType TYPE = GeoShapeType.MULTIPOINT;
|
||||
|
||||
@Override
|
||||
|
|
|
@ -33,7 +33,7 @@ public class MultiPolygonBuilder extends ShapeBuilder {
|
|||
|
||||
public static final GeoShapeType TYPE = GeoShapeType.MULTIPOLYGON;
|
||||
|
||||
protected final ArrayList<BasePolygonBuilder<?>> polygons = new ArrayList<>();
|
||||
protected final ArrayList<PolygonBuilder> 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<Shape> 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)));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -34,7 +34,6 @@ import com.vividsolutions.jts.geom.Coordinate;
|
|||
public abstract class PointCollection<E extends PointCollection<E>> extends ShapeBuilder {
|
||||
|
||||
protected final ArrayList<Coordinate> points;
|
||||
protected boolean translated = false;
|
||||
|
||||
protected PointCollection() {
|
||||
this(new ArrayList<Coordinate>());
|
||||
|
|
|
@ -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<PolygonBuilder> {
|
||||
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<LineStringBuilder> holes = new ArrayList<>();
|
||||
|
||||
public PolygonBuilder() {
|
||||
this(new ArrayList<Coordinate>(), Orientation.RIGHT);
|
||||
|
@ -33,14 +62,460 @@ public class PolygonBuilder extends BasePolygonBuilder<PolygonBuilder> {
|
|||
this(new ArrayList<Coordinate>(), orientation);
|
||||
}
|
||||
|
||||
protected PolygonBuilder(ArrayList<Coordinate> points, Orientation orientation) {
|
||||
public PolygonBuilder(ArrayList<Coordinate> 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<Coordinate> exterior = Sets.newHashSet(shell.points);
|
||||
HashSet<Coordinate> 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<LineStringBuilder> 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<Coordinate> 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 <code>null</code>)
|
||||
* @return number of edges that belong to this component
|
||||
*/
|
||||
private static int component(final Edge edge, final int id, final ArrayList<Edge> 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<Coordinate, Tuple<Edge, Edge>> 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<Edge, Edge>(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<ArrayList<Coordinate[]>> components) {
|
||||
Coordinate[][][] result = new Coordinate[components.size()][][];
|
||||
for (int i = 0; i < result.length; i++) {
|
||||
ArrayList<Coordinate[]> 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<ArrayList<Coordinate[]>> components) {
|
||||
ArrayList<Edge> 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<Coordinate[]> 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<ArrayList<Coordinate[]>> 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<ArrayList<Coordinate[]>> 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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
Loading…
Reference in New Issue