Skip to content

Commit

Permalink
Add support for dateline wrapping for circle to geometry conversion (#…
Browse files Browse the repository at this point in the history
…194)

* Circles that cross a dateline can now be converted to a JTS Geometry. Previous attempts would throw an exception. 
* JtsGeometry now supports input an Geometry that crosses the dateline multiple times (wraps the globe multiple times). Previous attempts would yield erroneous behavior.

Signed-off-by: Stijn Caerts <stijncaerts@gmail.com>
  • Loading branch information
StijnCaerts committed Oct 28, 2020
1 parent 2435128 commit ee39723
Show file tree
Hide file tree
Showing 5 changed files with 65 additions and 8 deletions.
6 changes: 6 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@

DATE: _unreleased_

* \#194: Circles that cross a dateline can now be converted to a JTS Geometry. Previous attempts would throw an exception.
(Stijn Caerts)

* \#194: JtsGeometry now supports input an Geometry that crosses the dateline multiple times (wraps the globe multiple times). Previous attempts would yield erroneous behavior.
(Stijn Caerts)

* \#188: Upgraded to JTS 1.17.0. This JTS release has a small [API change](https://github.com/locationtech/jts/blob/master/doc/JTS_Version_History.md#api-changes) and it requires Java 1.8.
Spatial4J should work fine with older versions still.
(Jim Hughes)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -573,11 +573,10 @@ private static Geometry cutUnwrappedGeomInto360(Geometry geom) {
return geom;
assert geom.isValid() : "geom";

//TODO opt: support geom's that start at negative pages --
// ... will avoid need to previously shift in unwrapDateline(geom).
List<Geometry> geomList = new ArrayList<Geometry>();
//page 0 is the standard -180 to 180 range
for (int page = 0; true; page++) {
int startPage = (int) Math.floor((geomEnv.getMinX() + 180) / 360);
for (int page = startPage; true; page++) {
double minX = -180 + page * 360;
if (geomEnv.getMaxX() <= minX)
break;
Expand All @@ -586,7 +585,10 @@ private static Geometry cutUnwrappedGeomInto360(Geometry geom) {
Geometry pageGeom = rect.intersection(geom);//JTS is doing some hard work
assert pageGeom.isValid() : "pageGeom";

shiftGeomByX(pageGeom, page * -360);
if (page != 0) {
pageGeom = pageGeom.copy(); // because shiftGeomByX modifies the underlying coordinates shared by geom.
shiftGeomByX(pageGeom, page * -360);
}
geomList.add(pageGeom);
}
return UnaryUnionOp.union(geomList);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -157,14 +157,16 @@ public Geometry getGeometryFrom(Shape shape) {
// http://docs.codehaus.org/display/GEOTDOC/01+How+to+Create+a+Geometry#01HowtoCreateaGeometry-CreatingaCircle
//TODO This should ideally have a geodetic version
Circle circle = (Circle)shape;
if (circle.getBoundingBox().getCrossesDateLine())
throw new IllegalArgumentException("Doesn't support dateline cross yet: "+circle);//TODO
GeometricShapeFactory gsf = new GeometricShapeFactory(geometryFactory);
gsf.setWidth(circle.getBoundingBox().getWidth());
gsf.setHeight(circle.getBoundingBox().getHeight());
gsf.setNumPoints(4*25);//multiple of 4 is best
gsf.setCentre(new Coordinate(circle.getCenter().getX(), circle.getCenter().getY()));
return gsf.createCircle();
Geometry geom = gsf.createCircle();
if (circle.getBoundingBox().getCrossesDateLine())
// wrap the geometry in a JtsGeometry to handle date line wrapping
geom = new JtsGeometry(geom, (JtsSpatialContext) getSpatialContext(),false, false).getGeom();
return geom;
}
//TODO add BufferedLineString
throw new InvalidShapeException("can't make Geometry from: " + shape);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import org.locationtech.spatial4j.shape.jts.JtsGeometry;
import org.locationtech.jts.geom.GeometryCollection;
import org.locationtech.jts.geom.Polygon;
import org.locationtech.jts.geom.MultiPolygon;
import org.junit.Test;
import org.locationtech.spatial4j.shape.jts.JtsShapeFactory;
import org.locationtech.spatial4j.util.Geom;
Expand Down Expand Up @@ -62,4 +63,20 @@ public void testDatelineRuleWithMultiPolygon() {
"((0 0, 1 1, 1 0, 0 0))" +
")", jtsGeometry.toString());
}

@Test
public void testMultiDatelineWrap() {
// polygon crosses the dateline twice
Polygon polygon = Geom.build().points(-179, 45, 179, 44, 1, 35, -179, 25, 179, 24, 179, 19, -179, 20, 1, 30, 179, 39, -179, 40).toPolygon();

JtsSpatialContextFactory factory = new JtsSpatialContextFactory();
factory.datelineRule = DatelineRule.width180;
JtsSpatialContext ctx = factory.newSpatialContext();
JtsShapeFactory shapeFactory = ctx.getShapeFactory();
JtsGeometry jtsGeometry = shapeFactory.makeShape(polygon);
Geometry geometry = jtsGeometry.getGeom();

assertTrue(geometry.isValid());
assertTrue(geometry instanceof MultiPolygon);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,13 @@
import org.locationtech.spatial4j.context.jts.JtsSpatialContextFactory;
import org.locationtech.spatial4j.distance.DistanceUtils;
import org.locationtech.spatial4j.distance.GeodesicSphereDistCalc;
import org.locationtech.spatial4j.shape.Circle;
import org.locationtech.spatial4j.shape.Point;
import org.locationtech.spatial4j.shape.Shape;
import org.locationtech.spatial4j.shape.impl.GeoCircle;
import org.locationtech.spatial4j.shape.impl.PointImpl;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;

public class JtsShapeFactoryTest {
Expand Down Expand Up @@ -94,7 +96,35 @@ private void circleGeometryConversionTest(double x, double y, double radiusKm) {
Point point = new PointImpl(coordinate.x, coordinate.y, SpatialContext.GEO);
double distance = distCalc.distance(point, circleCenter);
double distanceKm = DistanceUtils.degrees2Dist(distance, DistanceUtils.EARTH_MEAN_RADIUS_KM);
Assert.assertEquals(String.format("Distance from point to center: %.2f km. Expected: %.2f km", distanceKm,
assertEquals(String.format("Distance from point to center: %.2f km. Expected: %.2f km", distanceKm,
radiusKm), radiusKm, distanceKm, maxDeltaKm);
}
}

@Test
public void testCircleDateLineWrapping() {
// left wrapping (-180)
circleGeometryConversionDateLineTest(-179.99, 51.22, 5);
// right wrapping (+180)
circleGeometryConversionDateLineTest(179.99, -35.6, 10);
}

private void circleGeometryConversionDateLineTest(double lon, double lat, double radiusKm) {
JtsShapeFactory shapeFactory = JtsSpatialContext.GEO.getShapeFactory();
Circle circle = shapeFactory.circle(lon, lat, DistanceUtils.dist2Degrees(radiusKm, DistanceUtils.EARTH_MEAN_RADIUS_KM));
Geometry geom = shapeFactory.getGeometryFrom(circle);

assertTrue(circle.getBoundingBox().getCrossesDateLine());
assertEquals("MultiPolygon", geom.getGeometryType());

GeodesicSphereDistCalc distCalc = new GeodesicSphereDistCalc.Haversine();
double maxDeltaKm = radiusKm / 100; // allow 1% inaccuracy
for (Coordinate c : geom.getCoordinates()) {
// Check distance from center of each point
Point point = new PointImpl(c.x, c.y, SpatialContext.GEO);
double distance = distCalc.distance(point, lon, lat);
double distanceKm = DistanceUtils.degrees2Dist(distance, DistanceUtils.EARTH_MEAN_RADIUS_KM);
assertEquals(String.format("Distance from point to center: %.2f km. Expected: %.2f km", distanceKm,
radiusKm), radiusKm, distanceKm, maxDeltaKm);
}
}
Expand Down

0 comments on commit ee39723

Please sign in to comment.