1
2
3
4
5
6
7
8
9 package org.locationtech.spatial4j.shape.jts;
10
11 import org.locationtech.spatial4j.context.SpatialContext;
12 import org.locationtech.spatial4j.context.jts.JtsSpatialContext;
13 import org.locationtech.spatial4j.distance.CartesianDistCalc;
14 import org.locationtech.spatial4j.exception.InvalidShapeException;
15 import org.locationtech.spatial4j.shape.*;
16 import org.locationtech.spatial4j.shape.Point;
17 import org.locationtech.spatial4j.shape.impl.BBoxCalculator;
18 import org.locationtech.spatial4j.shape.impl.BufferedLineString;
19 import org.locationtech.spatial4j.shape.impl.RectangleImpl;
20 import org.locationtech.jts.geom.*;
21 import org.locationtech.jts.geom.prep.PreparedGeometry;
22 import org.locationtech.jts.geom.prep.PreparedGeometryFactory;
23 import org.locationtech.jts.operation.union.UnaryUnionOp;
24 import org.locationtech.jts.operation.valid.IsValidOp;
25
26 import java.util.ArrayList;
27 import java.util.Collection;
28 import java.util.List;
29
30
31
32
33
34
35 public class JtsGeometry extends BaseShape<JtsSpatialContext> {
36
37 public static final String SYSPROP_ASSERT_VALIDATE = "spatial4j.JtsGeometry.assertValidate";
38
39 private final Geometry geom;
40 private final boolean hasArea;
41 private final Rectangle bbox;
42 protected PreparedGeometry preparedGeometry;
43 protected boolean validated = false;
44
45 public JtsGeometry(Geometry geom, JtsSpatialContext ctx, boolean dateline180Check, boolean allowMultiOverlap) {
46 super(ctx);
47
48 if (geom.getClass().equals(GeometryCollection.class)) {
49 geom = narrowCollectionIfPossible((GeometryCollection)geom);
50 if (geom == null) {
51 throw new IllegalArgumentException("JtsGeometry does not support GeometryCollection but does support its subclasses.");
52 }
53 }
54
55
56 if (geom.isEmpty()) {
57 bbox = new RectangleImpl(Double.NaN, Double.NaN, Double.NaN, Double.NaN, this.ctx);
58 } else if (ctx.isGeo()) {
59
60 if (dateline180Check)
61 geom = unwrapDateline(geom);
62
63 if (allowMultiOverlap)
64 geom = unionGeometryCollection(geom);
65
66
67 geom = cutUnwrappedGeomInto360(geom);
68 assert geom.getEnvelopeInternal().getWidth() <= 360;
69 assert ! geom.getClass().equals(GeometryCollection.class) : "GeometryCollection unsupported";
70
71
72 bbox = computeGeoBBox(geom);
73 } else {
74
75 if (allowMultiOverlap)
76 geom = unionGeometryCollection(geom);
77
78 Envelope env = geom.getEnvelopeInternal();
79 bbox = new RectangleImpl(env.getMinX(), env.getMaxX(), env.getMinY(), env.getMaxY(), ctx);
80 }
81 geom.getEnvelopeInternal();
82
83 this.geom = geom;
84 assert assertValidate();
85
86 this.hasArea = !((geom instanceof Lineal) || (geom instanceof Puntal));
87 }
88
89
90
91
92
93
94
95
96
97
98
99 private Geometry narrowCollectionIfPossible(GeometryCollection gc) {
100 List<Geometry> geoms = new ArrayList<>();
101 for (int i = 0; i < gc.getNumGeometries(); i++) {
102 geoms.add(gc.getGeometryN(i));
103 }
104
105 Geometry result = gc.getFactory().buildGeometry(geoms);
106 return !result.getClass().equals(GeometryCollection.class) ? result : null;
107 }
108
109
110 private boolean assertValidate() {
111 String assertValidate = System.getProperty(SYSPROP_ASSERT_VALIDATE);
112 if (assertValidate == null || Boolean.parseBoolean(assertValidate))
113 validate();
114 return true;
115 }
116
117
118
119
120
121
122
123 public void validate() throws InvalidShapeException {
124 if (!validated) {
125 IsValidOp isValidOp = new IsValidOp(geom);
126 if (!isValidOp.isValid())
127 throw new InvalidShapeException(isValidOp.getValidationError().toString());
128 validated = true;
129 }
130 }
131
132
133
134
135 boolean isIndexed() {
136 return preparedGeometry != null;
137 }
138
139
140
141
142
143
144
145
146 public void index() {
147 if (preparedGeometry == null)
148 preparedGeometry = PreparedGeometryFactory.prepare(geom);
149 }
150
151 @Override
152 public boolean isEmpty() {
153 return bbox.isEmpty();
154 }
155
156
157
158
159 protected Rectangle computeGeoBBox(Geometry geoms) {
160 final Envelope env = geoms.getEnvelopeInternal();
161 if (ctx.isGeo() && env.getWidth() > 180 && geoms.getNumGeometries() > 1) {
162
163 BBoxCalculator bboxCalc = new BBoxCalculator(ctx);
164 for (int i = 0; i < geoms.getNumGeometries(); i++ ) {
165 Envelope envI = geoms.getGeometryN(i).getEnvelopeInternal();
166 bboxCalc.expandXRange(envI.getMinX(), envI.getMaxX());
167 if (bboxCalc.doesXWorldWrap())
168 break;
169 }
170 return new RectangleImpl(bboxCalc.getMinX(), bboxCalc.getMaxX(), env.getMinY(), env.getMaxY(), ctx);
171 } else {
172 return new RectangleImpl(env.getMinX(), env.getMaxX(), env.getMinY(), env.getMaxY(), ctx);
173 }
174 }
175
176 @Override
177 public JtsGeometry getBuffered(double distance, SpatialContext ctx) {
178
179
180 return this.ctx.makeShape(geom.buffer(distance), true, true);
181 }
182
183 @Override
184 public boolean hasArea() {
185 return hasArea;
186 }
187
188 @Override
189 public double getArea(SpatialContext ctx) {
190 double geomArea = geom.getArea();
191 if (ctx == null || geomArea == 0)
192 return geomArea;
193
194 double bboxArea = getBoundingBox().getArea(null);
195 assert bboxArea >= geomArea;
196 double filledRatio = geomArea / bboxArea;
197 return getBoundingBox().getArea(ctx) * filledRatio;
198
199
200 }
201
202 @Override
203 public Rectangle getBoundingBox() {
204 return bbox;
205 }
206
207 @Override
208 public JtsPoint getCenter() {
209 if (isEmpty())
210 return new JtsPoint(ctx.getGeometryFactory().createPoint((Coordinate)null), ctx);
211 return new JtsPoint(geom.getCentroid(), ctx);
212 }
213
214 @Override
215 public SpatialRelation relate(Shape other) {
216 if (other instanceof Point)
217 return relate((Point)other);
218 else if (other instanceof Rectangle)
219 return relate((Rectangle) other);
220 else if (other instanceof Circle)
221 return relate((Circle) other);
222 else if (other instanceof JtsGeometry)
223 return relate((JtsGeometry) other);
224 else if (other instanceof BufferedLineString)
225 throw new UnsupportedOperationException("Can't use BufferedLineString with JtsGeometry");
226 return other.relate(this).transpose();
227 }
228
229 public SpatialRelation relate(Point pt) {
230 if (!getBoundingBox().relate(pt).intersects())
231 return SpatialRelation.DISJOINT;
232 Geometry ptGeom;
233 if (pt instanceof JtsPoint)
234 ptGeom = ((JtsPoint)pt).getGeom();
235 else
236 ptGeom = ctx.getGeometryFactory().createPoint(new Coordinate(pt.getX(), pt.getY()));
237 return relate(ptGeom);
238 }
239
240 public SpatialRelation relate(Rectangle rectangle) {
241 SpatialRelation bboxR = bbox.relate(rectangle);
242 if (bboxR == SpatialRelation.WITHIN || bboxR == SpatialRelation.DISJOINT)
243 return bboxR;
244
245 return relate(ctx.getGeometryFrom(rectangle));
246 }
247
248 public SpatialRelation relate(final Circle circle) {
249 SpatialRelation bboxR = bbox.relate(circle);
250 if (bboxR == SpatialRelation.WITHIN || bboxR == SpatialRelation.DISJOINT)
251 return bboxR;
252
253
254 final SpatialRelation[] result = {null};
255
256 geom.apply(new GeometryFilter() {
257
258
259
260
261
262
263 final CartesianDistCalc calcSqd = CartesianDistCalc.INSTANCE_SQUARED;
264 final double radiusSquared = circle.getRadius() * circle.getRadius();
265 final Geometry ctrGeom = ctx.getGeometryFrom(circle.getCenter());
266
267 @Override
268 public void filter(Geometry geom) {
269 if (result[0] == SpatialRelation.INTERSECTS || result[0] == SpatialRelation.CONTAINS) {
270
271 return;
272 }
273
274 if (geom instanceof Polygon) {
275 Polygon polygon = (Polygon) geom;
276 SpatialRelation rel = relateEnclosedRing((LinearRing) polygon.getExteriorRing());
277
278 if (rel == SpatialRelation.CONTAINS) {
279
280 HOLE_LOOP: for (int i = 0; i < polygon.getNumInteriorRing(); i++){
281
282 switch (relateEnclosedRing((LinearRing) polygon.getInteriorRingN(i))) {
283 case WITHIN:
284 case INTERSECTS:
285 rel = SpatialRelation.INTERSECTS;
286 break HOLE_LOOP;
287 case CONTAINS:
288 rel = SpatialRelation.DISJOINT;
289 break HOLE_LOOP;
290
291 }
292 }
293 }
294 result[0] = rel.combine(result[0]);
295 } else if (geom instanceof LineString) {
296 LineString lineString = (LineString) geom;
297 SpatialRelation rel = relateLineString(lineString);
298 result[0] = rel.combine(result[0]);
299 } else if (geom instanceof org.locationtech.jts.geom.Point) {
300 org.locationtech.jts.geom.Point point = (org.locationtech.jts.geom.Point) geom;
301 SpatialRelation rel =
302 calcSqd.distance(circle.getCenter(), point.getX(), point.getY()) > radiusSquared
303 ? SpatialRelation.DISJOINT : SpatialRelation.WITHIN;
304 result[0] = rel.combine(result[0]);
305 }
306
307 }
308
309
310 SpatialRelation relateEnclosedRing(LinearRing ring) {
311 SpatialRelation rel = relateLineString(ring);
312 if (rel == SpatialRelation.DISJOINT
313 && ctx.getGeometryFactory().createPolygon(ring, null).contains(ctrGeom)) {
314
315 rel = SpatialRelation.CONTAINS;
316 }
317 return rel;
318 }
319
320 SpatialRelation relateLineString(LineString lineString) {
321 final CoordinateSequence seq = lineString.getCoordinateSequence();
322 final boolean isRing = lineString instanceof LinearRing;
323 int numOutside = 0;
324
325 for (int i = 0, numComparisons = 0; i < seq.size(); i++) {
326 if (i == 0 && isRing) {
327 continue;
328 }
329 numComparisons++;
330 boolean outside = calcSqd.distance(circle.getCenter(), seq.getX(i), seq.getY(i)) > radiusSquared;
331 if (outside) {
332 numOutside++;
333 }
334
335 if (numComparisons != numOutside && numOutside != 0) {
336 assert numComparisons > 1;
337 return SpatialRelation.INTERSECTS;
338 }
339 }
340
341 if (numOutside == 0) {
342 return SpatialRelation.WITHIN.combine(result[0]);
343 }
344
345
346 for (int i = 1; i < seq.size(); i++) {
347 boolean outside = calcSqd.distanceToLineSegment(
348 circle.getCenter(), seq.getX(i-1), seq.getY(i-1), seq.getX(i), seq.getY(i))
349 > radiusSquared;
350 if (!outside) {
351 return SpatialRelation.INTERSECTS;
352 }
353 }
354 return SpatialRelation.DISJOINT;
355 }
356 });
357
358 return result[0] == null ? SpatialRelation.DISJOINT : result[0];
359 }
360
361 public SpatialRelation relate(JtsGeometry jtsGeometry) {
362
363 return relate(jtsGeometry.geom);
364 }
365
366 protected SpatialRelation relate(Geometry oGeom) {
367
368 if (oGeom instanceof org.locationtech.jts.geom.Point) {
369 if (preparedGeometry != null)
370 return preparedGeometry.disjoint(oGeom) ? SpatialRelation.DISJOINT : SpatialRelation.CONTAINS;
371 return geom.disjoint(oGeom) ? SpatialRelation.DISJOINT : SpatialRelation.CONTAINS;
372 }
373 if (preparedGeometry == null)
374 return intersectionMatrixToSpatialRelation(geom.relate(oGeom));
375 else if (preparedGeometry.covers(oGeom))
376 return SpatialRelation.CONTAINS;
377 else if (preparedGeometry.coveredBy(oGeom))
378 return SpatialRelation.WITHIN;
379 else if (preparedGeometry.intersects(oGeom))
380 return SpatialRelation.INTERSECTS;
381 return SpatialRelation.DISJOINT;
382 }
383
384 public static SpatialRelation intersectionMatrixToSpatialRelation(IntersectionMatrix matrix) {
385
386
387 if (matrix.isCovers())
388 return SpatialRelation.CONTAINS;
389 else if (matrix.isCoveredBy())
390 return SpatialRelation.WITHIN;
391 else if (matrix.isDisjoint())
392 return SpatialRelation.DISJOINT;
393 return SpatialRelation.INTERSECTS;
394 }
395
396 @Override
397 public String toString() {
398 return geom.toString();
399 }
400
401 @Override
402 public boolean equals(Object o) {
403 if (this == o) return true;
404 if (o == null || getClass() != o.getClass()) return false;
405 JtsGeometry that = (JtsGeometry) o;
406 return geom.equalsExact(that.geom);
407 }
408
409 @Override
410 public int hashCode() {
411
412 return geom.getEnvelopeInternal().hashCode();
413 }
414
415 public Geometry getGeom() {
416 return geom;
417 }
418
419
420
421
422
423
424
425
426 private static Geometry unwrapDateline(Geometry geom) {
427 if (geom.getEnvelopeInternal().getWidth() < 180)
428 return geom;
429
430
431 if (geom instanceof GeometryCollection) {
432 if (geom instanceof MultiPoint) {
433 return geom;
434 }
435 GeometryCollection gc = (GeometryCollection) geom;
436 List<Geometry> list = new ArrayList<>(gc.getNumGeometries());
437 boolean didUnwrap = false;
438 for (int n = 0; n < gc.getNumGeometries(); n++) {
439 Geometry geometryN = gc.getGeometryN(n);
440 Geometry geometryUnwrapped = unwrapDateline(geometryN);
441 list.add(geometryUnwrapped);
442 didUnwrap |= (geometryUnwrapped != geometryN);
443 }
444 return !didUnwrap ? geom : geom.getFactory().buildGeometry(list);
445 }
446
447
448
449 Geometry newGeom = geom.copy();
450
451 final int[] crossings = {0};
452 newGeom.apply(new GeometryFilter() {
453 @Override
454 public void filter(Geometry geom) {
455 int cross;
456 if (geom instanceof LineString) {
457 if (geom.getEnvelopeInternal().getWidth() < 180)
458 return;
459 cross = unwrapDateline((LineString) geom);
460 } else if (geom instanceof Polygon) {
461 if (geom.getEnvelopeInternal().getWidth() < 180)
462 return;
463 cross = unwrapDateline((Polygon) geom);
464 } else {
465
466
467 return;
468 }
469 crossings[0] = Math.max(crossings[0], cross);
470 }
471 });
472
473 if (crossings[0] > 0) {
474 newGeom.geometryChanged();
475 return newGeom;
476 } else {
477 return geom;
478 }
479 }
480
481
482 private static int unwrapDateline(Polygon poly) {
483 LineString exteriorRing = poly.getExteriorRing();
484 int cross = unwrapDateline(exteriorRing);
485 if (cross > 0) {
486
487 for(int i = 0; i < poly.getNumInteriorRing(); i++) {
488 LineString innerLineString = poly.getInteriorRingN(i);
489 unwrapDateline(innerLineString);
490 for(int shiftCount = 0; ! exteriorRing.contains(innerLineString); shiftCount++) {
491 if (shiftCount > cross)
492 throw new IllegalArgumentException("The inner ring doesn't appear to be within the exterior: "
493 +exteriorRing+" inner: "+innerLineString);
494 shiftGeomByX(innerLineString, 360);
495 }
496 }
497 }
498 return cross;
499 }
500
501
502 private static int unwrapDateline(LineString lineString) {
503 CoordinateSequence cseq = lineString.getCoordinateSequence();
504 int size = cseq.size();
505 if (size <= 1)
506 return 0;
507
508 int shiftX = 0;
509 int shiftXPage = 0;
510 int shiftXPageMin = 0, shiftXPageMax = 0;
511 double prevX = cseq.getX(0);
512 for(int i = 1; i < size; i++) {
513 double thisX_orig = cseq.getX(i);
514 assert thisX_orig >= -180 && thisX_orig <= 180 : "X not in geo bounds";
515 double thisX = thisX_orig + shiftX;
516 if (prevX - thisX > 180) {
517 thisX += 360;
518 shiftX += 360;
519 shiftXPage += 1;
520 shiftXPageMax = Math.max(shiftXPageMax,shiftXPage);
521 } else if (thisX - prevX > 180) {
522 thisX -= 360;
523 shiftX -= 360;
524 shiftXPage -= 1;
525 shiftXPageMin = Math.min(shiftXPageMin,shiftXPage);
526 }
527 if (shiftXPage != 0)
528 cseq.setOrdinate(i, CoordinateSequence.X, thisX);
529 prevX = thisX;
530 }
531 if (lineString instanceof LinearRing) {
532 assert cseq.getCoordinate(0).equals(cseq.getCoordinate(size-1));
533 assert shiftXPage == 0;
534 }
535 assert shiftXPageMax >= 0 && shiftXPageMin <= 0;
536
537 shiftGeomByX(lineString, shiftXPageMin * -360);
538 int crossings = shiftXPageMax - shiftXPageMin;
539 return crossings;
540 }
541
542 private static void shiftGeomByX(Geometry geom, final int xShift) {
543 if (xShift == 0)
544 return;
545 geom.apply(new CoordinateSequenceFilter() {
546 @Override
547 public void filter(CoordinateSequence seq, int i) {
548 seq.setOrdinate(i, CoordinateSequence.X, seq.getX(i) + xShift );
549 }
550
551 @Override public boolean isDone() { return false; }
552
553 @Override public boolean isGeometryChanged() { return true; }
554 });
555 }
556
557 private static Geometry unionGeometryCollection(Geometry geom) {
558 if (geom instanceof GeometryCollection) {
559 return geom.union();
560 }
561 return geom;
562 }
563
564
565
566
567
568
569
570 private static Geometry cutUnwrappedGeomInto360(Geometry geom) {
571 Envelope geomEnv = geom.getEnvelopeInternal();
572 if (geomEnv.getMinX() >= -180 && geomEnv.getMaxX() <= 180)
573 return geom;
574 assert geom.isValid() : "geom";
575
576 List<Geometry> geomList = new ArrayList<>();
577
578 int startPage = (int) Math.floor((geomEnv.getMinX() + 180) / 360);
579 for (int page = startPage; true; page++) {
580 double minX = -180 + page * 360;
581 if (geomEnv.getMaxX() <= minX)
582 break;
583 Geometry rect = geom.getFactory().toGeometry(new Envelope(minX, minX + 360, -90, 90));
584 assert rect.isValid() : "rect";
585 Geometry pageGeom = rect.intersection(geom);
586 assert pageGeom.isValid() : "pageGeom";
587
588 if (page != 0) {
589 pageGeom = pageGeom.copy();
590 shiftGeomByX(pageGeom, page * -360);
591 }
592 geomList.add(pageGeom);
593 }
594 return UnaryUnionOp.union(geomList);
595 }
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613 }