Geo: Merging BaseLineString and BasePolygonBuilder with subclass
After the removal of some internal shape builders in #14482 the BaseLineStringBuilder has only one implementation, the LineStringBuilder. Same for the BasePolygonBuilder. This PR removes the abstract classes and merges them with their concrete implementation to simplify the inheritance hierarchy.
This commit is contained in:
parent
18c9eba40a
commit
a2dca2f6cb
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue