org.elasticsearch.common.geo.builders.BasePolygonBuilder Maven / Gradle / Ivy
/*
* Licensed to ElasticSearch and Shay Banon 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 java.util.Iterator;
import org.elasticsearch.common.xcontent.XContentBuilder;
import com.spatial4j.core.shape.Shape;
import com.spatial4j.core.shape.jts.JtsGeometry;
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;
/**
* The {@link BasePolygonBuilder} implements the groundwork to create polygons. This contains
* Methods to wrap polygons at the dateline and building shapes from the data held by the
* builder.
* Since this Builder can be embedded to other builders (i.e. {@link MultiPolygonBuilder})
* the class of the embedding builder is given by the generic argument E
* @param type of the embedding class
*/
public abstract class BasePolygonBuilder> extends ShapeBuilder {
public static final GeoShapeType TYPE = GeoShapeType.POLYGON;
// Linear ring defining the shell of the polygon
protected Ring shell;
// List of linear rings defining the holes of the polygon
protected final ArrayList> holes = new ArrayList>();
@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(BaseLineStringBuilder> hole) {
holes.add(hole);
return thisRef();
}
/**
* build new hole to the polygon
* @param hole linear ring defining the hole
* @return this
*/
public Ring hole() {
Ring hole = new Ring(thisRef());
this.holes.add(hole);
return hole;
}
/**
* Close the shell of the polygon
* @return parent
*/
public ShapeBuilder close() {
return shell.close();
}
/**
* 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;
}
Edge[] edges = new Edge[numEdges];
Edge[] holeComponents = new Edge[holes.size()];
int offset = createEdges(0, true, shell, edges, 0);
for (int i = 0; i < holes.size(); i++) {
int length = createEdges(i+1, false, 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() {
Geometry geometry = buildGeometry(FACTORY, wrapdateline);
return new JtsGeometry(geometry, SPATIAL_CONTEXT, !wrapdateline);
}
protected XContentBuilder coordinatesArray(XContentBuilder builder, Params params) throws IOException {
shell.coordinatesToXcontent(builder, true);
for(BaseLineStringBuilder> hole : holes) {
hole.coordinatesToXcontent(builder, true);
}
return builder;
}
@Override
public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
builder.startObject();
builder.field(FIELD_TYPE, TYPE.shapename);
builder.startArray(FIELD_COORDINATES);
coordinatesArray(builder, params);
builder.endArray();
builder.endObject();
return builder;
}
public Geometry buildGeometry(GeometryFactory factory, boolean fixDateline) {
if(fixDateline) {
Coordinate[][][] polygons = coordinates();
return polygons.length == 1
? polygon(factory, polygons[0])
: multipolygon(factory, polygons);
} else {
return toPolygon(factory);
}
}
public Polygon toPolygon() {
return toPolygon(FACTORY);
}
protected Polygon toPolygon(GeometryFactory factory) {
final LinearRing shell = linearRing(factory, this.shell.points);
final LinearRing[] holes = new LinearRing[this.holes.size()];
Iterator> iterator = this.holes.iterator();
for (int i = 0; iterator.hasNext(); i++) {
holes[i] = linearRing(factory, iterator.next().points);
}
return factory.createPolygon(shell, holes);
}
protected static LinearRing linearRing(GeometryFactory factory, ArrayList coordinates) {
return factory.createLinearRing(coordinates.toArray(new Coordinate[coordinates.size()]));
}
@Override
public GeoShapeType type() {
return TYPE;
}
protected static Polygon polygon(GeometryFactory factory, Coordinate[][] polygon) {
LinearRing shell = factory.createLinearRing(polygon[0]);
LinearRing[] holes;
if(polygon.length > 1) {
holes = new LinearRing[polygon.length-1];
for (int i = 0; i < holes.length; i++) {
holes[i] = factory.createLinearRing(polygon[i+1]);
}
} else {
holes = null;
}
return factory.createPolygon(shell, holes);
}
/**
* Create a Multipolygon from a set of coordinates. Each primary array contains a polygon which
* in turn contains an array of linestrings. These line Strings are represented as an array of
* coordinates. The first linestring will be the shell of the polygon the others define holes
* within the polygon.
*
* @param factory {@link GeometryFactory} to use
* @param polygons definition of polygons
* @return a new Multipolygon
*/
protected static MultiPolygon multipolygon(GeometryFactory factory, Coordinate[][][] polygons) {
Polygon[] polygonSet = new Polygon[polygons.length];
for (int i = 0; i < polygonSet.length; i++) {
polygonSet[i] = polygon(factory, polygons[i]);
}
return factory.createMultiPolygon(polygonSet);
}
/**
* This method sets the component id of all edges in a ring to a given id and shifts the
* coordinates of this component according to the dateline
*
* @param edge An arbitrary edge of the component
* @param id id to apply to the component
* @param edges a list of edges to which all edges of the component will be added (could be null
)
* @return number of edges that belong to this component
*/
private static int component(final Edge edge, final int id, final ArrayList edges) {
// find a coordinate that is not part of the dateline
Edge any = edge;
while(any.coordinate.x == +DATELINE || any.coordinate.x == -DATELINE) {
if((any = any.next) == edge) {
break;
}
}
double shift = any.coordinate.x > DATELINE ? DATELINE : (any.coordinate.x < -DATELINE ? -DATELINE : 0);
if (debugEnabled()) {
LOGGER.debug("shift: {[]}", shift);
}
// run along the border of the component, collect the
// edges, shift them according to the dateline and
// update the component id
int length = 0;
Edge current = edge;
do {
current.coordinate = shift(current.coordinate, shift);
current.component = id;
if(edges != null) {
edges.add(current);
}
length++;
} while((current = current.next) != edge);
return length;
}
/**
* Compute all coordinates of a component
* @param component an arbitrary edge of the component
* @param coordinates Array of coordinates to write the result to
* @return the coordinates parameter
*/
private static Coordinate[] coordinates(Edge component, Coordinate[] coordinates) {
for (int i = 0; i < coordinates.length; i++) {
coordinates[i] = (component = component.next).coordinate;
}
return coordinates;
}
private static Coordinate[][][] buildCoordinates(ArrayList> components) {
Coordinate[][][] result = new Coordinate[components.size()][][];
for (int i = 0; i < result.length; i++) {
ArrayList component = components.get(i);
result[i] = component.toArray(new Coordinate[component.size()][]);
}
if(debugEnabled()) {
for (int i = 0; i < result.length; i++) {
LOGGER.debug("Component {[]}:", i);
for (int j = 0; j < result[i].length; j++) {
LOGGER.debug("\t" + Arrays.toString(result[i][j]));
}
}
}
return result;
}
private static final Coordinate[][] EMPTY = new Coordinate[0][];
private static Coordinate[][] holes(Edge[] holes, int numHoles) {
if (numHoles == 0) {
return EMPTY;
}
final Coordinate[][] points = new Coordinate[numHoles][];
for (int i = 0; i < numHoles; i++) {
int length = component(holes[i], -(i+1), null); // mark as visited by inverting the sign
points[i] = coordinates(holes[i], new Coordinate[length+1]);
}
return points;
}
private static Edge[] edges(Edge[] edges, int numHoles, ArrayList> components) {
ArrayList mainEdges = new ArrayList(edges.length);
for (int i = 0; i < edges.length; i++) {
if (edges[i].component >= 0) {
int length = component(edges[i], -(components.size()+numHoles+1), mainEdges);
ArrayList component = new ArrayList();
component.add(coordinates(edges[i], new Coordinate[length+1]));
components.add(component);
}
}
return mainEdges.toArray(new Edge[mainEdges.size()]);
}
private static Coordinate[][][] compose(Edge[] edges, Edge[] holes, int numHoles) {
final ArrayList> components = new ArrayList>();
assign(holes, holes(holes, numHoles), numHoles, edges(edges, numHoles, components), components);
return buildCoordinates(components);
}
private static void assign(Edge[] holes, Coordinate[][] points, int numHoles, Edge[] edges, ArrayList> components) {
// Assign Hole to related components
// To find the new component the hole belongs to all intersections of the
// polygon edges with a vertical line are calculated. This vertical line
// is an arbitrary point of the hole. The polygon edge next to this point
// is part of the polygon the hole belongs to.
if (debugEnabled()) {
LOGGER.debug("Holes: " + Arrays.toString(holes));
}
for (int i = 0; i < numHoles; i++) {
final Edge current = holes[i];
final int intersections = intersections(current.coordinate.x, edges);
final int pos = Arrays.binarySearch(edges, 0, intersections, current, INTERSECTION_ORDER);
assert pos < 0 : "illegal state: two edges cross the datum at the same position";
final int index = -(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;
}
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 {
// 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, boolean direction, BaseLineStringBuilder> line, Edge[] edges, int offset) {
Coordinate[] points = line.coordinates(false); // last point is repeated
Edge.ring(component, direction, points, 0, edges, offset, points.length-1);
return points.length-1;
}
public static class Ring extends BaseLineStringBuilder> {
private final P parent;
protected Ring(P parent) {
this(parent, new ArrayList());
}
protected Ring(P parent, ArrayList points) {
super(points);
this.parent = parent;
}
public P close() {
Coordinate start = points.get(0);
Coordinate end = points.get(points.size()-1);
if(start.x != end.x || start.y != end.y) {
points.add(start);
}
return parent;
}
@Override
public GeoShapeType type() {
return null;
}
}
}