19
19
20
20
package org .elasticsearch .common .geo .builders ;
21
21
22
+ import com .google .common .collect .Sets ;
23
+ import com .spatial4j .core .exception .InvalidShapeException ;
22
24
import com .spatial4j .core .shape .Shape ;
23
25
import com .vividsolutions .jts .geom .*;
24
- import org .elasticsearch . ElasticsearchParseException ;
26
+ import org .apache . commons . lang3 . tuple . Pair ;
25
27
import org .elasticsearch .common .xcontent .XContentBuilder ;
26
28
27
29
import java .io .IOException ;
28
30
import java .util .ArrayList ;
29
31
import java .util .Arrays ;
32
+ import java .util .HashMap ;
33
+ import java .util .HashSet ;
30
34
import java .util .Iterator ;
31
35
32
36
/**
@@ -111,6 +115,18 @@ public ShapeBuilder close() {
111
115
return shell .close ();
112
116
}
113
117
118
+ /**
119
+ * Validates only 1 vertex is tangential (shared) between the interior and exterior of a polygon
120
+ */
121
+ protected void validateHole (BaseLineStringBuilder shell , BaseLineStringBuilder hole ) {
122
+ HashSet exterior = Sets .newHashSet (shell .points );
123
+ HashSet interior = Sets .newHashSet (hole .points );
124
+ exterior .retainAll (interior );
125
+ if (exterior .size () >= 2 ) {
126
+ throw new InvalidShapeException ("Invalid polygon, interior cannot share more than one point with the exterior" );
127
+ }
128
+ }
129
+
114
130
/**
115
131
* The coordinates setup by the builder will be assembled to a polygon. The result will consist of
116
132
* a set of polygons. Each of these components holds a list of linestrings defining the polygon: the
@@ -125,6 +141,7 @@ public Coordinate[][][] coordinates() {
125
141
int numEdges = shell .points .size ()-1 ; // Last point is repeated
126
142
for (int i = 0 ; i < holes .size (); i ++) {
127
143
numEdges += holes .get (i ).points .size ()-1 ;
144
+ validateHole (shell , this .holes .get (i ));
128
145
}
129
146
130
147
Edge [] edges = new Edge [numEdges ];
@@ -253,28 +270,62 @@ private static int component(final Edge edge, final int id, final ArrayList<Edge
253
270
}
254
271
}
255
272
256
- double shift = any .coordinate .x > DATELINE ? DATELINE : (any .coordinate .x < -DATELINE ? -DATELINE : 0 );
273
+ double shiftOffset = any .coordinate .x > DATELINE ? DATELINE : (any .coordinate .x < -DATELINE ? -DATELINE : 0 );
257
274
if (debugEnabled ()) {
258
- LOGGER .debug ("shift: {[]}" , shift );
275
+ LOGGER .debug ("shift: {[]}" , shiftOffset );
259
276
}
260
277
261
278
// run along the border of the component, collect the
262
279
// edges, shift them according to the dateline and
263
280
// update the component id
264
- int length = 0 ;
281
+ int length = 0 , connectedComponents = 0 ;
282
+ // if there are two connected components, splitIndex keeps track of where to split the edge array
283
+ // start at 1 since the source coordinate is shared
284
+ int splitIndex = 1 ;
265
285
Edge current = edge ;
286
+ Edge prev = edge ;
287
+ // bookkeep the source and sink of each visited coordinate
288
+ HashMap <Coordinate , Pair <Edge , Edge >> visitedEdge = new HashMap <>();
266
289
do {
267
-
268
- current .coordinate = shift (current .coordinate , shift );
290
+ current .coordinate = shift (current .coordinate , shiftOffset );
269
291
current .component = id ;
270
- if (edges != null ) {
292
+
293
+ if (edges != null ) {
294
+ // found a closed loop - we have two connected components so we need to slice into two distinct components
295
+ if (visitedEdge .containsKey (current .coordinate )) {
296
+ if (connectedComponents > 0 && current .next != edge ) {
297
+ throw new InvalidShapeException ("Shape contains more than one shared point" );
298
+ }
299
+
300
+ // a negative id flags the edge as visited for the edges(...) method.
301
+ // since we're splitting connected components, we want the edges method to visit
302
+ // the newly separated component
303
+ final int visitID = -id ;
304
+ Edge firstAppearance = visitedEdge .get (current .coordinate ).getRight ();
305
+ // correct the graph pointers by correcting the 'next' pointer for both the
306
+ // first appearance and this appearance of the edge
307
+ Edge temp = firstAppearance .next ;
308
+ firstAppearance .next = current .next ;
309
+ current .next = temp ;
310
+ current .component = visitID ;
311
+ // backtrack until we get back to this coordinate, setting the visit id to
312
+ // a non-visited value (anything positive)
313
+ do {
314
+ prev .component = visitID ;
315
+ prev = visitedEdge .get (prev .coordinate ).getLeft ();
316
+ ++splitIndex ;
317
+ } while (!current .coordinate .equals (prev .coordinate ));
318
+ ++connectedComponents ;
319
+ } else {
320
+ visitedEdge .put (current .coordinate , Pair .of (prev , current ));
321
+ }
271
322
edges .add (current );
323
+ prev = current ;
272
324
}
273
-
274
325
length ++;
275
- } while ((current = current .next ) != edge );
326
+ } while (connectedComponents == 0 && (current = current .next ) != edge );
276
327
277
- return length ;
328
+ return ( splitIndex != 1 ) ? length - splitIndex : length ;
278
329
}
279
330
280
331
/**
@@ -364,11 +415,12 @@ private static void assign(Edge[] holes, Coordinate[][] points, int numHoles, Ed
364
415
// if no intersection is found then the hole is not within the polygon, so
365
416
// don't waste time calling a binary search
366
417
final int pos ;
367
- if (intersections == 0 ||
368
- (pos = Arrays .binarySearch (edges , 0 , intersections , current , INTERSECTION_ORDER )) >= 0 ) {
369
- throw new ElasticsearchParseException ("Invalid shape: Hole is not within polygon" );
418
+ boolean sharedVertex = false ;
419
+ if (intersections == 0 || ((pos = Arrays .binarySearch (edges , 0 , intersections , current , INTERSECTION_ORDER )) >= 0 )
420
+ && !(sharedVertex = (edges [pos ].intersect .compareTo (current .coordinate ) == 0 )) ) {
421
+ throw new InvalidShapeException ("Invalid shape: Hole is not within polygon" );
370
422
}
371
- final int index = -(pos +2 );
423
+ final int index = -(( sharedVertex ) ? 0 : pos +2 );
372
424
final int component = -edges [index ].component - numHoles - 1 ;
373
425
374
426
if (debugEnabled ()) {
@@ -465,7 +517,7 @@ private static int createEdges(int component, Orientation orientation, BaseLineS
465
517
Edge [] edges , int offset ) {
466
518
// inner rings (holes) have an opposite direction than the outer rings
467
519
// XOR will invert the orientation for outer ring cases (Truth Table:, T/T = F, T/F = T, F/T = T, F/F = F)
468
- boolean direction = (component ! = 0 ^ orientation == Orientation .RIGHT );
520
+ boolean direction = (component = = 0 ^ orientation == Orientation .RIGHT );
469
521
// set the points array accordingly (shell or hole)
470
522
Coordinate [] points = (hole != null ) ? hole .coordinates (false ) : shell .coordinates (false );
471
523
Edge .ring (component , direction , orientation == Orientation .LEFT , shell , points , 0 , edges , offset , points .length -1 );
0 commit comments