Skip to content

Commit

Permalink
[2.19.x] G-10122 Fix buffering across the antimeridian (#6690)
Browse files Browse the repository at this point in the history
  • Loading branch information
jrnorth authored Jun 6, 2022
1 parent 5d1fe7c commit d11874b
Show file tree
Hide file tree
Showing 5 changed files with 134 additions and 4 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,13 @@
package ddf.catalog.source.solr;

import com.google.common.collect.ImmutableMap;
import com.vividsolutions.jts.geom.Envelope;
import com.vividsolutions.jts.geom.Geometry;
import com.vividsolutions.jts.geom.GeometryCollection;
import com.vividsolutions.jts.geom.GeometryFactory;
import com.vividsolutions.jts.geom.MultiPolygon;
import com.vividsolutions.jts.geom.Point;
import com.vividsolutions.jts.geom.Polygon;
import com.vividsolutions.jts.io.ParseException;
import com.vividsolutions.jts.io.WKTReader;
import com.vividsolutions.jts.io.WKTWriter;
Expand Down Expand Up @@ -104,7 +108,11 @@ public class SolrFilterDelegate extends FilterDelegate<SolrQuery> {
"spatialContextFactory",
JtsSpatialContextFactory.class.getName(),
"validationRule",
ValidationRule.repairConvexHull.name());
ValidationRule.repairConvexHull.name(),
"normWrapLongitude",
"true",
"allowMultiOverlap",
"true");

private static final SpatialContext SPATIAL_CONTEXT =
SpatialContextFactory.makeSpatialContext(SPATIAL_CONTEXT_ARGUMENTS, null);
Expand Down Expand Up @@ -765,6 +773,33 @@ public SolrQuery contains(String propertyName, String wkt) {
return operationToQuery("Contains", propertyName, wkt);
}

public static Geometry removeHoles(final Geometry geo) {
if (geo.getGeometryType().equalsIgnoreCase("Polygon")) {
final Polygon p = (Polygon) geo;
return GEOMETRY_FACTORY.createPolygon(p.getExteriorRing().getCoordinateSequence());
} else if (geo.getGeometryType().equalsIgnoreCase("MultiPolygon")) {
final MultiPolygon mp = (MultiPolygon) geo;
final List<Polygon> polys = new ArrayList<>();
for (int i = 0; i < mp.getNumGeometries(); i++) {
final Polygon poly =
GEOMETRY_FACTORY.createPolygon(
((Polygon) mp.getGeometryN(i)).getExteriorRing().getCoordinateSequence());
polys.add(poly);
}
return GEOMETRY_FACTORY.createMultiPolygon(polys.toArray(new Polygon[0]));
} else if (geo.getGeometryType().equalsIgnoreCase("GeometryCollection")) {
final GeometryCollection gc = (GeometryCollection) geo;
final List<Geometry> geos = new ArrayList<>();
for (int i = 0; i < gc.getNumGeometries(); i++) {
final Geometry geometry = removeHoles(gc.getGeometryN(i));
geos.add(geometry);
}
return GEOMETRY_FACTORY.createGeometryCollection(geos.toArray(new Geometry[0]));
}

return geo;
}

@Override
public SolrQuery dwithin(String propertyName, String wkt, double distance) {
Geometry geo = getGeometry(wkt);
Expand All @@ -779,6 +814,18 @@ public SolrQuery dwithin(String propertyName, String wkt, double distance) {
return new SolrQuery(pointRadiusQuery);
} else {
Geometry bufferGeo = geo.buffer(distanceInDegrees, QUADRANT_SEGMENTS);
final Envelope envelope = bufferGeo.getEnvelopeInternal();
final boolean crossesDateline =
envelope.getWidth() >= 180
|| (envelope.getMinX() <= 180 && envelope.getMaxX() > 180)
|| (envelope.getMinX() < -180 && envelope.getMaxX() >= -180);
// When spatial4j's JtsGeometry unwraps polygons that cross the dateline, it checks that all
// interior rings of the polygon are subsets of the exterior ring, which means polygons with
// holes will throw an exception. Remove any holes from polygons that cross the dateline to
// prevent that exception from being thrown.
if (crossesDateline) {
bufferGeo = removeHoles(bufferGeo);
}
String bufferWkt = WKT_WRITER.write(bufferGeo);
return operationToQuery(INTERSECTS_OPERATION, propertyName, bufferWkt);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@
import java.util.TimeZone;
import org.apache.solr.client.solrj.SolrQuery;
import org.junit.Test;
import org.locationtech.jts.geom.MultiPolygon;
import org.locationtech.jts.io.ParseException;
import org.locationtech.jts.io.WKTReader;
import org.locationtech.jts.operation.union.UnaryUnionOp;

public class SolrFilterDelegateTest {

Expand Down Expand Up @@ -142,17 +146,19 @@ public void polygonWithHoleAndSelfIntersecting() {
}

@Test
public void multiPolygon() {
public void multiPolygon() throws ParseException {
String wkt =
"MULTIPOLYGON (((30 20, 45 40, 10 40, 30 20)), ((15 5, 40 10, 10 20, 5 10, 15 5)))";
stub(mockResolver.getField(
"testProperty", AttributeFormat.GEOMETRY, false, Collections.emptyMap()))
.toReturn("testProperty_geohash_index");
SolrQuery query = toTest.contains("testProperty", wkt);
MultiPolygon multiPolygon = (MultiPolygon) new WKTReader().read(wkt);
// Need to use union since allowMultiOverlap is enabled
MultiPolygon unioned = (MultiPolygon) new UnaryUnionOp(multiPolygon).union();
assertThat(
query.getQuery(),
startsWith(
"testProperty_geohash_index:\"Contains(MULTIPOLYGON (((30 20, 45 40, 10 40, 30 20)), ((15 5, 40 10, 10 20, 5 10, 15 5))))\""));
startsWith(String.format("testProperty_geohash_index:\"Contains(%s)\"", unioned.toText())));
}

@Test
Expand All @@ -168,6 +174,51 @@ public void polygonWithHole() {
"testProperty_geohash_index:\"Contains(POLYGON ((35 10, 45 45, 15 40, 10 20, 35 10), (20 30, 35 35, 30 20, 20 30)))\""));
}

@Test
public void bufferedPolygonHolesRemovedIfCrossingDateline() {
String wkt =
"POLYGON ((170 10, -170 10, -170 0, 170 0, 170 10), (171 9, 172 9, 172 8, 172 8, 171 9))";
stub(mockResolver.getField(
"testProperty", AttributeFormat.GEOMETRY, false, Collections.emptyMap()))
.toReturn("testProperty_geohash_index");
// buffer of 0 so the final WKT is easy to calculate
SolrQuery query = toTest.dwithin("testProperty", wkt, 0);
assertThat(
query.getQuery(),
startsWith(
"testProperty_geohash_index:\"Intersects(MULTIPOLYGON (((-180 0, -180 10, -170 10, -170 0, -180 0)), ((180 10, 180 0, 170 0, 170 10, 180 10))))\""));
}

@Test
public void bufferedMultiPolygonHolesRemovedIfCrossingDateline() {
String wkt =
"MULTIPOLYGON (((170 10, -170 10, -170 0, 170 0, 170 10), (171 9, 172 9, 172 8, 172 8, 171 9)), ((170 30, -170 30, -170 20, 170 20, 170 30), (171 29, 172 29, 172 28, 172 28, 171 29)))";
stub(mockResolver.getField(
"testProperty", AttributeFormat.GEOMETRY, false, Collections.emptyMap()))
.toReturn("testProperty_geohash_index");
// buffer of 0 so the final WKT is easy to calculate
SolrQuery query = toTest.dwithin("testProperty", wkt, 0);
assertThat(
query.getQuery(),
startsWith(
"testProperty_geohash_index:\"Intersects(MULTIPOLYGON (((-180 0, -180 10, -170 10, -170 0, -180 0)), ((180 10, 180 0, 170 0, 170 10, 180 10)), ((-180 20, -180 30, -170 30, -170 20, -180 20)), ((180 30, 180 20, 170 20, 170 30, 180 30))))\""));
}

@Test
public void bufferedGeometryCollectionHolesRemovedIfCrossingDateline() {
String wkt =
"GEOMETRYCOLLECTION (POLYGON ((170 10, -170 10, -170 0, 170 0, 170 10), (171 9, 172 9, 172 8, 172 8, 171 9)), POLYGON ((170 30, -170 30, -170 20, 170 20, 170 30), (171 29, 172 29, 172 28, 172 28, 171 29)))";
stub(mockResolver.getField(
"testProperty", AttributeFormat.GEOMETRY, false, Collections.emptyMap()))
.toReturn("testProperty_geohash_index");
// buffer of 0 so the final WKT is easy to calculate
SolrQuery query = toTest.dwithin("testProperty", wkt, 0);
assertThat(
query.getQuery(),
startsWith(
"testProperty_geohash_index:\"Intersects(MULTIPOLYGON (((-180 0, -180 10, -170 10, -170 0, -180 0)), ((180 10, 180 0, 170 0, 170 10, 180 10)), ((-180 20, -180 30, -170 30, -170 20, -180 20)), ((180 30, 180 20, 170 20, 170 30, 180 30))))\""));
}

@Test
public void reservedSpecialCharactersIsEqual() {
// given a text property
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,9 @@ public class Library {
"POLYGON ((175 30, -175 30, -175 25, 175 25, 175 30))";
public static final String ACROSS_INTERNATIONAL_DATELINE_LARGE_CCW_WKT =
"POLYGON ((175 30, 175 25, -175 25, -175 30, 175 30))";
public static final String ON_INTERNATIONAL_DATELINE_CW_WKT =
"POLYGON ((175 75, 180 75, 180 70, 175 70, 175 75))";
public static final String ARCTIC_OCEAN_POINT_WKT = "POINT (-179.9 75.1)";
public static final String MIDWAY_ISLANDS_POINT_WKT = "POINT (-177.372736 28.208365)";
public static final String LAS_VEGAS_POINT_WKT = "POINT (-115.136389 36.175)";
public static final String ARABIAN_SEA_POINT_WKT = "POINT (62.5 14)";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import static ddf.catalog.source.solr.provider.SolrProviderTestUtil.update;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.greaterThanOrEqualTo;
import static org.hamcrest.Matchers.hasSize;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.lessThanOrEqualTo;
import static org.hamcrest.Matchers.notNullValue;
Expand Down Expand Up @@ -523,6 +524,33 @@ public void testSpatialQueryAcrossInternationalDateLine() throws Exception {
assertEquals("Should not find a record. ", 0, sourceResponse.getResults().size());
}

@Test
public void testSpatialQueryBufferedAcrossDateline() throws Exception {
deleteAll(provider);

MetacardImpl metacard = new MockMetacard(Library.getFlagstaffRecord());
metacard.setLocation(Library.ARCTIC_OCEAN_POINT_WKT);
List<Metacard> list = Collections.singletonList(metacard);

create(list, provider);

Filter filter =
getFilterBuilder()
.attribute(Metacard.GEOGRAPHY)
.is()
.withinBuffer()
.wkt(Library.ON_INTERNATIONAL_DATELINE_CW_WKT, 25_000);
SourceResponse sourceResponse = provider.query(new QueryRequestImpl(new QueryImpl(filter)));

assertThat("Failed to find the correct record.", sourceResponse.getResults(), hasSize(1));

for (Result r : sourceResponse.getResults()) {
assertTrue(
"Wrong record, Flagstaff keyword was not found.",
r.getMetacard().getMetadata().contains(Library.FLAGSTAFF_QUERY_PHRASE));
}
}

@Test
public void testSpatialCreateAndUpdateWithClockwiseRectangle() throws Exception {
deleteAll(provider);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@
class="solr.RptWithGeometrySpatialField"
spatialContextFactory="JTS"
distanceUnits="degrees"
normWrapLongitude="true"
format="WKT"
autoIndex="true"
allowMultiOverlap="true" />
Expand Down

0 comments on commit d11874b

Please sign in to comment.