New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Fix hole intersection at tangential coordinate #10332
Closed
Closed
Changes from 4 commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
84f2976
[GEO] Fix hole intersection at tangential coordinate
nknize a0e376e
removing unnecessary intermediate ArrayLists from validateHole
nknize 1fcdfe3
variable name change for readability
nknize 96e6c17
Fix infinite loop for self-loops in edge graph
nknize 7445271
updating exception verbiage
nknize d34f722
change WeakHashMap and update exception handling
nknize File filter
Filter by extension
Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -19,15 +19,20 @@ | |
|
||
package org.elasticsearch.common.geo.builders; | ||
|
||
import com.google.common.collect.Lists; | ||
import com.google.common.collect.Sets; | ||
import com.spatial4j.core.exception.InvalidShapeException; | ||
import com.spatial4j.core.shape.Shape; | ||
import com.vividsolutions.jts.geom.*; | ||
import org.elasticsearch.ElasticsearchParseException; | ||
import org.apache.commons.lang3.tuple.Pair; | ||
import org.elasticsearch.common.xcontent.XContentBuilder; | ||
|
||
import java.io.IOException; | ||
import java.util.ArrayList; | ||
import java.util.Arrays; | ||
import java.util.HashSet; | ||
import java.util.Iterator; | ||
import java.util.WeakHashMap; | ||
|
||
/** | ||
* The {@link BasePolygonBuilder} implements the groundwork to create polygons. This contains | ||
|
@@ -111,6 +116,18 @@ public ShapeBuilder close() { | |
return shell.close(); | ||
} | ||
|
||
/** | ||
* 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"); | ||
} | ||
} | ||
|
||
/** | ||
* 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 | ||
|
@@ -125,6 +142,7 @@ 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]; | ||
|
@@ -253,28 +271,62 @@ private static int component(final Edge edge, final int id, final ArrayList<Edge | |
} | ||
} | ||
|
||
double shift = any.coordinate.x > DATELINE ? DATELINE : (any.coordinate.x < -DATELINE ? -DATELINE : 0); | ||
double shiftOffset = any.coordinate.x > DATELINE ? DATELINE : (any.coordinate.x < -DATELINE ? -DATELINE : 0); | ||
if (debugEnabled()) { | ||
LOGGER.debug("shift: {[]}", shift); | ||
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; | ||
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 | ||
WeakHashMap<Coordinate, Pair<Edge, Edge>> visitedEdge = new WeakHashMap<>(); | ||
do { | ||
|
||
current.coordinate = shift(current.coordinate, shift); | ||
current.coordinate = shift(current.coordinate, shiftOffset); | ||
current.component = id; | ||
if(edges != null) { | ||
|
||
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 tangential point"); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should we make this message a bit more user friendly? I'm not sure may users will know what a tangential point is |
||
} | ||
|
||
// 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).getRight(); | ||
// 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).getLeft(); | ||
++splitIndex; | ||
} while (!current.coordinate.equals(prev.coordinate)); | ||
++connectedComponents; | ||
} else { | ||
visitedEdge.put(current.coordinate, Pair.of(prev, current)); | ||
} | ||
edges.add(current); | ||
prev = current; | ||
} | ||
|
||
length++; | ||
} while((current = current.next) != edge); | ||
} while(connectedComponents == 0 && (current = current.next) != edge); | ||
|
||
return length; | ||
return (splitIndex != 1) ? length-splitIndex: length; | ||
} | ||
|
||
/** | ||
|
@@ -364,11 +416,12 @@ private static void assign(Edge[] holes, Coordinate[][] points, int numHoles, Ed | |
// 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; | ||
if (intersections == 0 || | ||
(pos = Arrays.binarySearch(edges, 0, intersections, current, INTERSECTION_ORDER)) >= 0) { | ||
throw new ElasticsearchParseException("Invalid shape: Hole is not within polygon"); | ||
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 = -(pos+2); | ||
final int index = -((sharedVertex) ? 0 : pos+2); | ||
final int component = -edges[index].component - numHoles - 1; | ||
|
||
if(debugEnabled()) { | ||
|
@@ -465,7 +518,7 @@ private static int createEdges(int component, Orientation orientation, BaseLineS | |
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); | ||
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); | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would this be clearer as "Invalid polygon, interior cannot share more than one point with the exterior"?