
org.opensearch.common.geo.builders.PolygonBuilder Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of opensearch Show documentation
Show all versions of opensearch Show documentation
OpenSearch subproject :server
/*
* SPDX-License-Identifier: Apache-2.0
*
* The OpenSearch Contributors require contributions made to
* this file be licensed under the Apache-2.0 license or a
* compatible open source license.
*/
/*
* 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.
*/
/*
* Modifications Copyright OpenSearch Contributors. See
* GitHub history for details.
*/
package org.opensearch.common.geo.builders;
import org.opensearch.common.collect.Tuple;
import org.opensearch.common.geo.GeoShapeType;
import org.opensearch.common.geo.parsers.ShapeParser;
import org.opensearch.common.io.stream.StreamInput;
import org.opensearch.common.io.stream.StreamOutput;
import org.opensearch.common.util.set.Sets;
import org.opensearch.common.xcontent.XContentBuilder;
import org.locationtech.jts.geom.Coordinate;
import org.locationtech.jts.geom.Geometry;
import org.locationtech.jts.geom.GeometryFactory;
import org.locationtech.jts.geom.LinearRing;
import org.locationtech.jts.geom.MultiPolygon;
import org.locationtech.jts.geom.Polygon;
import org.locationtech.spatial4j.exception.InvalidShapeException;
import org.locationtech.spatial4j.shape.jts.JtsGeometry;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicBoolean;
import static org.apache.lucene.geo.GeoUtils.orient;
/**
* 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;
private static final Coordinate[][] EMPTY = new Coordinate[0][];
private Orientation orientation = Orientation.RIGHT;
// line string defining the shell of the polygon
private LineStringBuilder shell;
// List of line strings defining the holes of the polygon
private final List holes = new ArrayList<>();
public PolygonBuilder(LineStringBuilder lineString, Orientation orientation, boolean coerce) {
this.orientation = orientation;
if (coerce) {
lineString.close();
}
validateLinearRing(lineString);
this.shell = lineString;
}
public PolygonBuilder(LineStringBuilder lineString, Orientation orientation) {
this(lineString, orientation, false);
}
public PolygonBuilder(CoordinatesBuilder coordinates, Orientation orientation) {
this(new LineStringBuilder(coordinates), orientation, false);
}
public PolygonBuilder(CoordinatesBuilder coordinates) {
this(coordinates, Orientation.RIGHT);
}
/**
* Read from a stream.
*/
public PolygonBuilder(StreamInput in) throws IOException {
shell = new LineStringBuilder(in);
orientation = Orientation.readFrom(in);
int holes = in.readVInt();
for (int i = 0; i < holes; i++) {
hole(new LineStringBuilder(in));
}
}
@Override
public void writeTo(StreamOutput out) throws IOException {
shell.writeTo(out);
orientation.writeTo(out);
out.writeVInt(holes.size());
for (LineStringBuilder hole : holes) {
hole.writeTo(out);
}
}
public Orientation orientation() {
return this.orientation;
}
/**
* Add a new hole to the polygon
* @param hole linear ring defining the hole
* @return this
*/
public PolygonBuilder hole(LineStringBuilder hole) {
return this.hole(hole, false);
}
/**
* Add a new hole to the polygon
* @param hole linear ring defining the hole
* @param coerce if set to true, it will try to close the hole by adding starting point as end point
* @return this
*/
public PolygonBuilder hole(LineStringBuilder hole, boolean coerce) {
if (coerce) {
hole.close();
}
validateLinearRing(hole);
holes.add(hole);
return this;
}
/**
* @return the list of holes defined for this polygon
*/
public List holes() {
return this.holes;
}
/**
* @return the list of points of the shell for this polygon
*/
public LineStringBuilder shell() {
return this.shell;
}
/**
* Close the shell of the polygon
*/
public PolygonBuilder close() {
shell.close();
return this;
}
private static void validateLinearRing(LineStringBuilder lineString) {
/**
* Per GeoJSON spec (http://geojson.org/geojson-spec.html#linestring)
* A LinearRing is closed LineString with 4 or more positions. The first and last positions
* are equivalent (they represent equivalent points). Though a LinearRing is not explicitly
* represented as a GeoJSON geometry type, it is referred to in the Polygon geometry type definition.
*/
List points = lineString.coordinates;
if (points.size() < 4) {
throw new IllegalArgumentException("invalid number of points in LinearRing (found [" + points.size() + "] - must be >= 4)");
}
if (!points.get(0).equals(points.get(points.size() - 1))) {
throw new IllegalArgumentException("invalid LinearRing found (coordinates are not closed)");
}
}
/**
* Validates only 1 vertex is tangential (shared) between the interior and exterior of a polygon
*/
protected void validateHole(LineStringBuilder shell, LineStringBuilder hole) {
HashSet exterior = Sets.newHashSet(shell.coordinates);
HashSet interior = Sets.newHashSet(hole.coordinates);
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.coordinates.size() - 1; // Last point is repeated
for (int i = 0; i < holes.size(); i++) {
numEdges += holes.get(i).coordinates.size() - 1;
validateHole(shell, this.holes.get(i));
}
Edge[] edges = new Edge[numEdges];
Edge[] holeComponents = new Edge[holes.size()];
final AtomicBoolean translated = new AtomicBoolean(false);
int offset = createEdges(0, orientation, shell, null, edges, 0, translated);
for (int i = 0; i < holes.size(); i++) {
int length = createEdges(i + 1, orientation, shell, this.holes.get(i), edges, offset, translated);
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 JtsGeometry buildS4J() {
return jtsGeometry(buildS4JGeometry(FACTORY, wrapdateline));
}
@Override
public org.opensearch.geometry.Geometry buildGeometry() {
return toPolygonGeometry();
}
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(ShapeParser.FIELD_TYPE.getPreferredName(), TYPE.shapeName());
builder.field(ShapeParser.FIELD_ORIENTATION.getPreferredName(), orientation.name().toLowerCase(Locale.ROOT));
builder.startArray(ShapeParser.FIELD_COORDINATES.getPreferredName());
coordinatesArray(builder, params);
builder.endArray();
builder.endObject();
return builder;
}
public Geometry buildS4JGeometry(GeometryFactory factory, boolean fixDateline) {
if (fixDateline) {
Coordinate[][][] polygons = coordinates();
return polygons.length == 1 ? polygonS4J(factory, polygons[0]) : multipolygonS4J(factory, polygons);
} else {
return toPolygonS4J(factory);
}
}
public Polygon toPolygonS4J() {
return toPolygonS4J(FACTORY);
}
protected Polygon toPolygonS4J(GeometryFactory factory) {
final LinearRing shell = linearRingS4J(factory, this.shell.coordinates);
final LinearRing[] holes = new LinearRing[this.holes.size()];
Iterator iterator = this.holes.iterator();
for (int i = 0; iterator.hasNext(); i++) {
holes[i] = linearRingS4J(factory, iterator.next().coordinates);
}
return factory.createPolygon(shell, holes);
}
public org.opensearch.geometry.Polygon toPolygonGeometry() {
final List holes = new ArrayList<>(this.holes.size());
for (int i = 0; i < this.holes.size(); ++i) {
holes.add(linearRing(this.holes.get(i).coordinates));
}
return new org.opensearch.geometry.Polygon(linearRing(this.shell.coordinates), holes);
}
protected static org.opensearch.geometry.LinearRing linearRing(List coordinates) {
return new org.opensearch.geometry.LinearRing(
coordinates.stream().mapToDouble(i -> i.x).toArray(),
coordinates.stream().mapToDouble(i -> i.y).toArray()
);
}
protected static LinearRing linearRingS4J(GeometryFactory factory, List coordinates) {
return factory.createLinearRing(coordinates.toArray(new Coordinate[coordinates.size()]));
}
@Override
public GeoShapeType type() {
return TYPE;
}
@Override
public int numDimensions() {
if (shell == null) {
throw new IllegalStateException("unable to get number of dimensions, " + "Polygon has not yet been initialized");
}
return shell.numDimensions();
}
protected static Polygon polygonS4J(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 multipolygonS4J(GeometryFactory factory, Coordinate[][][] polygons) {
Polygon[] polygonSet = new Polygon[polygons.length];
for (int i = 0; i < polygonSet.length; i++) {
polygonSet[i] = polygonS4J(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, double[] partitionPoint) {
// find a coordinate that is not part of the dateline
Edge any = edge;
while (any.coordinate.x == +DATELINE || any.coordinate.x == -DATELINE) {
if ((any = any.next) == edge) {
break;
}
}
double shiftOffset = any.coordinate.x > DATELINE ? DATELINE : (any.coordinate.x < -DATELINE ? -DATELINE : 0);
if (debugEnabled()) {
LOGGER.debug("shift: [{}]", shiftOffset);
}
// run along the border of the component, collect the
// edges, shift them according to the dateline and
// update the component id
int length = 0, connectedComponents = 0;
// if there are two connected components, splitIndex keeps track of where to split the edge array
// start at 1 since the source coordinate is shared
int splitIndex = 1;
Edge current = edge;
Edge prev = edge;
// bookkeep the source and sink of each visited coordinate
HashMap> visitedEdge = new HashMap<>();
do {
current.coordinate = shift(current.coordinate, shiftOffset);
current.component = id;
if (edges != null) {
// found a closed loop - we have two connected components so we need to slice into two distinct components
if (visitedEdge.containsKey(current.coordinate)) {
partitionPoint[0] = current.coordinate.x;
partitionPoint[1] = current.coordinate.y;
partitionPoint[2] = current.coordinate.z;
if (connectedComponents > 0 && current.next != edge) {
throw new InvalidShapeException("Shape contains more than one shared point");
}
// a negative id flags the edge as visited for the edges(...) method.
// since we're splitting connected components, we want the edges method to visit
// the newly separated component
final int visitID = -id;
Edge firstAppearance = visitedEdge.get(current.coordinate).v2();
// correct the graph pointers by correcting the 'next' pointer for both the
// first appearance and this appearance of the edge
Edge temp = firstAppearance.next;
firstAppearance.next = current.next;
current.next = temp;
current.component = visitID;
// backtrack until we get back to this coordinate, setting the visit id to
// a non-visited value (anything positive)
do {
prev.component = visitID;
prev = visitedEdge.get(prev.coordinate).v1();
++splitIndex;
} while (!current.coordinate.equals(prev.coordinate));
++connectedComponents;
} else {
visitedEdge.put(current.coordinate, new Tuple(prev, current));
}
edges.add(current);
prev = current;
}
length++;
} while (connectedComponents == 0 && (current = current.next) != edge);
return (splitIndex != 1) ? length - splitIndex : length;
}
/**
* Compute all coordinates of a component
* @param component an arbitrary edge of the component
* @param coordinates Array of coordinates to write the result to
* @return the coordinates parameter
*/
private static Coordinate[] coordinates(Edge component, Coordinate[] coordinates, double[] partitionPoint) {
for (int i = 0; i < coordinates.length; i++) {
coordinates[i] = (component = component.next).coordinate;
}
// First and last coordinates must be equal
if (coordinates[0].equals(coordinates[coordinates.length - 1]) == false) {
if (Double.isNaN(partitionPoint[2])) {
throw new InvalidShapeException("Self-intersection at or near point [" + partitionPoint[0] + "," + partitionPoint[1] + "]");
} else {
throw new InvalidShapeException(
"Self-intersection at or near point [" + partitionPoint[0] + "," + partitionPoint[1] + "," + partitionPoint[2] + "]"
);
}
}
return coordinates;
}
private static Coordinate[][][] buildCoordinates(List> components) {
Coordinate[][][] result = new Coordinate[components.size()][][];
for (int i = 0; i < result.length; i++) {
List 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 Coordinate[][] holes(Edge[] holes, int numHoles) {
if (numHoles == 0) {
return EMPTY;
}
final Coordinate[][] points = new Coordinate[numHoles][];
for (int i = 0; i < numHoles; i++) {
double[] partitionPoint = new double[3];
int length = component(holes[i], -(i + 1), null, partitionPoint); // mark as visited by inverting the sign
points[i] = coordinates(holes[i], new Coordinate[length + 1], partitionPoint);
}
return points;
}
private static Edge[] edges(Edge[] edges, int numHoles, List> components) {
ArrayList mainEdges = new ArrayList<>(edges.length);
for (int i = 0; i < edges.length; i++) {
if (edges[i].component >= 0) {
double[] partitionPoint = new double[3];
int length = component(edges[i], -(components.size() + numHoles + 1), mainEdges, partitionPoint);
List component = new ArrayList<>();
component.add(coordinates(edges[i], new Coordinate[length + 1], partitionPoint));
components.add(component);
}
}
return mainEdges.toArray(new Edge[mainEdges.size()]);
}
private static Coordinate[][][] compose(Edge[] edges, Edge[] holes, int numHoles) {
final List> 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, List> 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++) {
// To do the assignment we assume (and later, elsewhere, check) that each hole is within
// a single component, and the components do not overlap. Based on this assumption, it's
// enough to find a component that contains some vertex of the hole, and
// holes[i].coordinate is such a vertex, so we use that one.
// First, we sort all the edges according to their order of intersection with the line
// of longitude through holes[i].coordinate, in order from south to north. Edges that do
// not intersect this line are sorted to the end of the array and of no further interest
// here.
final Edge current = new Edge(holes[i].coordinate, holes[i].next);
current.intersect = current.coordinate;
final int intersections = intersections(current.coordinate.x, edges);
if (intersections == 0) {
// There were no edges that intersect the line of longitude through
// holes[i].coordinate, so there's no way this hole is within the polygon.
throw new InvalidShapeException("Invalid shape: Hole is not within polygon");
}
// Next we do a binary search to find the position of holes[i].coordinate in the array.
// The binary search returns the index of an exact match, or (-insertionPoint - 1) if
// the vertex lies between the intersections of edges[insertionPoint] and
// edges[insertionPoint+1]. The latter case is vastly more common.
final int pos;
boolean sharedVertex = false;
if (((pos = Arrays.binarySearch(edges, 0, intersections, current, INTERSECTION_ORDER)) >= 0)
&& !(sharedVertex = (edges[pos].intersect.compareTo(current.coordinate) == 0))) {
// The binary search returned an exact match, but we checked again using compareTo()
// and it didn't match after all.
// TODO Can this actually happen? Needs a test to exercise it, or else needs to be removed.
throw new InvalidShapeException("Invalid shape: Hole is not within polygon");
}
final int index;
if (sharedVertex) {
// holes[i].coordinate lies exactly on an edge.
index = 0; // TODO Should this be pos instead of 0? This assigns exact matches to the southernmost component.
} else if (pos == -1) {
// holes[i].coordinate is strictly south of all intersections. Assign it to the
// southernmost component, and allow later validation to spot that it is not
// entirely within the chosen component.
index = 0;
} else {
// holes[i].coordinate is strictly north of at least one intersection. Assign it to
// the component immediately to its south.
index = -(pos + 2);
}
final int component = -edges[index].component - numHoles - 1;
if (debugEnabled()) {
LOGGER.debug("\tposition ({}) of edge {}: {}", index, 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,
final AtomicBoolean translated
) {
// 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);
ring(component, direction, orientation == Orientation.LEFT, points, 0, edges, offset, points.length - 1, translated);
return points.length - 1;
}
/**
* Create a connected list of a list of coordinates
*
* @param points
* array of point
* @param offset
* index of the first point
* @param length
* number of points
* @return Array of edges
*/
private static Edge[] ring(
int component,
boolean direction,
boolean handedness,
Coordinate[] points,
int offset,
Edge[] edges,
int toffset,
int length,
final AtomicBoolean translated
) {
boolean orientation = getOrientation(points, offset, length);
// OGC requires shell as ccw (Right-Handedness) and holes as cw (Left-Handedness)
// since GeoJSON doesn't specify (and doesn't need to) GEO core will assume OGC standards
// thus if orientation is computed as cw, the logic will translate points across dateline
// and convert to a right handed system
// compute the bounding box and calculate range
double[] range = range(points, offset, length);
final double rng = range[1] - range[0];
// translate the points if the following is true
// 1. shell orientation is cw and range is greater than a hemisphere (180 degrees) but not spanning 2 hemispheres
// (translation would result in a collapsed poly)
// 2. the shell of the candidate hole has been translated (to preserve the coordinate system)
boolean incorrectOrientation = component == 0 && handedness != orientation;
if ((incorrectOrientation && (rng > DATELINE && rng != 2 * DATELINE)) || (translated.get() && component != 0)) {
translate(points);
// flip the translation bit if the shell is being translated
if (component == 0) {
translated.set(true);
}
// correct the orientation post translation (ccw for shell, cw for holes)
if (component == 0 || (component != 0 && handedness == orientation)) {
orientation = !orientation;
}
}
return concat(component, direction ^ orientation, points, offset, edges, toffset, length);
}
/**
* @return whether the points are clockwise (true) or anticlockwise (false)
*/
private static boolean getOrientation(Coordinate[] points, int offset, int length) {
// calculate the direction of the points: find the southernmost point
// and check its neighbors orientation.
final int top = top(points, offset, length);
final int prev = (top + length - 1) % length;
final int next = (top + 1) % length;
final int determinantSign = orient(
points[offset + prev].x,
points[offset + prev].y,
points[offset + top].x,
points[offset + top].y,
points[offset + next].x,
points[offset + next].y
);
if (determinantSign == 0) {
// Points are collinear, but `top` is not in the middle if so, so the edges either side of `top` are intersecting.
throw new InvalidShapeException(
"Cannot determine orientation: edges adjacent to (" + points[offset + top].x + "," + points[offset + top].y + ") coincide"
);
}
return determinantSign < 0;
}
/**
* @return the (offset) index of the point that is furthest west amongst
* those points that are the furthest south in the set.
*/
private static int top(Coordinate[] points, int offset, int length) {
int top = 0; // we start at 1 here since top points to 0
for (int i = 1; i < length; i++) {
if (points[offset + i].y < points[offset + top].y) {
top = i;
} else if (points[offset + i].y == points[offset + top].y) {
if (points[offset + i].x < points[offset + top].x) {
top = i;
}
}
}
return top;
}
private static double[] range(Coordinate[] points, int offset, int length) {
double minX = points[0].x;
double maxX = points[0].x;
double minY = points[0].y;
double maxY = points[0].y;
// compute the bounding coordinates (@todo: cleanup brute force)
for (int i = 1; i < length; ++i) {
if (points[offset + i].x < minX) {
minX = points[offset + i].x;
}
if (points[offset + i].x > maxX) {
maxX = points[offset + i].x;
}
if (points[offset + i].y < minY) {
minY = points[offset + i].y;
}
if (points[offset + i].y > maxY) {
maxY = points[offset + i].y;
}
}
return new double[] { minX, maxX, minY, maxY };
}
/**
* Concatenate a set of points to a polygon
*
* @param component
* component id of the polygon
* @param direction
* direction of the ring
* @param points
* list of points to concatenate
* @param pointOffset
* index of the first point
* @param edges
* Array of edges to write the result to
* @param edgeOffset
* index of the first edge in the result
* @param length
* number of points to use
* @return the edges creates
*/
private static Edge[] concat(
int component,
boolean direction,
Coordinate[] points,
final int pointOffset,
Edge[] edges,
final int edgeOffset,
int length
) {
assert edges.length >= length + edgeOffset;
assert points.length >= length + pointOffset;
edges[edgeOffset] = new Edge(points[pointOffset], null);
for (int i = 1; i < length; i++) {
if (direction) {
edges[edgeOffset + i] = new Edge(points[pointOffset + i], edges[edgeOffset + i - 1]);
edges[edgeOffset + i].component = component;
} else if (!edges[edgeOffset + i - 1].coordinate.equals(points[pointOffset + i])) {
edges[edgeOffset + i - 1].next = edges[edgeOffset + i] = new Edge(points[pointOffset + i], null);
edges[edgeOffset + i - 1].component = component;
} else {
throw new InvalidShapeException("Provided shape has duplicate consecutive coordinates at: " + points[pointOffset + i]);
}
}
if (direction) {
edges[edgeOffset].setNext(edges[edgeOffset + length - 1]);
edges[edgeOffset].component = component;
} else {
edges[edgeOffset + length - 1].setNext(edges[edgeOffset]);
edges[edgeOffset + length - 1].component = component;
}
return edges;
}
/**
* Transforms coordinates in the eastern hemisphere (-180:0) to a (180:360) range
*/
private static void translate(Coordinate[] points) {
for (Coordinate c : points) {
if (c.x < 0) {
c.x += 2 * DATELINE;
}
}
}
@Override
protected StringBuilder contentToWKT() {
StringBuilder sb = new StringBuilder();
sb.append('(');
sb.append(ShapeBuilder.coordinateListToWKT(shell.coordinates));
for (LineStringBuilder hole : holes) {
sb.append(", ");
sb.append(ShapeBuilder.coordinateListToWKT(hole.coordinates));
}
sb.append(')');
return sb;
}
@Override
public int hashCode() {
return Objects.hash(shell, holes, orientation);
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null || getClass() != obj.getClass()) {
return false;
}
PolygonBuilder other = (PolygonBuilder) obj;
return Objects.equals(shell, other.shell) && Objects.equals(holes, other.holes) && Objects.equals(orientation, other.orientation);
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy