Skip to content
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

Add support for dateline wrapping for circle to geometry conversion #194

Merged
merged 4 commits into from
Oct 28, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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).
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you think you can address this second line of the comment you deleted? if you look in unwrapDateline(LineString), there will be a final shifting going on that I think the improvement you added here obsoletes. Granted I wrote that 8 years ago so I'm a little unclear now looking back at it!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will take a look at it!

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