/**
* Copyright (c) Codice Foundation
* <p>
* This is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser
* General Public License as published by the Free Software Foundation, either version 3 of the
* License, or any later version.
* <p>
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without
* even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details. A copy of the GNU Lesser General Public License
* is distributed along with this program and can be found at
* <http://www.gnu.org/licenses/lgpl.html>.
*/
package ddf.catalog.pubsub.criteria.geospatial;
import java.io.IOException;
import java.io.StringReader;
import java.util.ArrayList;
import java.util.regex.Pattern;
import javax.xml.parsers.ParserConfigurationException;
import org.geotools.xml.Configuration;
import org.geotools.xml.Parser;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.xml.sax.SAXException;
import com.vividsolutions.jts.geom.Coordinate;
import com.vividsolutions.jts.geom.Geometry;
import com.vividsolutions.jts.geom.GeometryFactory;
import com.vividsolutions.jts.geom.Point;
import com.vividsolutions.jts.geom.Polygon;
import com.vividsolutions.jts.operation.distance.DistanceOp;
public class GeospatialEvaluator {
public static final String METADATA_DOD_MIL_CRS_WGS84E_2D =
"http://metadata.dod.mil/mdr/ns/GSIP/crs/WGS84E_2D";
public static final String EPSG_4326 = "EPSG:4326";
private static final Logger LOGGER = LoggerFactory.getLogger(GeospatialEvaluator.class);
// If both criteria and input are GeometryCollections, each element of input must lie entirely
// within one component
// of criteria.
private static boolean containsWithGeometryCollection(Geometry criteria, Geometry input) {
for (int whichInput = 0; whichInput < input.getNumGeometries(); ++whichInput) {
boolean thisInputOk = false;
for (int whichCriteria = 0; whichCriteria < criteria.getNumGeometries(); ++whichCriteria) {
if (criteria.getGeometryN(whichCriteria)
.contains(input.getGeometryN(whichInput))) {
thisInputOk = true;
break;
}
}
if (!thisInputOk) {
// We found an input component which is not in a criteria component
return false;
}
}
return true;
}
private static boolean overlapsWithGeometryCollection(Geometry criteria, Geometry input) {
for (int i = 0; i < criteria.getNumGeometries(); ++i) {
for (int j = 0; j < input.getNumGeometries(); ++j) {
// The legacy interpretation of OVERLAPS corresponds better to a JTS INTERSECTS
// Intersects means NOT DISJOINT. In other words the two geometries have at least
// one point in common
// JTS's interpretation of OVERLAPS is the geometries have some but NOT all points
// in common. This
// means that if geometry A is a large, and geometry B is smaller than A and is
// completely inside A,
// A and B DO NOT overlap.
if (criteria.getGeometryN(i)
.intersects(input.getGeometryN(j))) {
// Criteria overlaps input if any component of either overlaps a component of
// the other.
return true;
}
}
}
// Nothing overlapped anything else
return false;
}
public static boolean evaluate(GeospatialEvaluationCriteria gec) {
String methodName = "evaluate";
LOGGER.debug("ENTERING: {}", methodName);
String operation = gec.getOperation();
Geometry input = gec.getInput();
Geometry criteria = gec.getCriteria();
double distance = gec.getDistance();
LOGGER.debug("operation = {}", operation);
boolean evaluation = false;
if (distance == 0.0) {
switch (SpatialOperator.valueOf(operation.toUpperCase())) {
case CONTAINS:
LOGGER.debug("Doing CONTAINS evaluation");
evaluation = containsWithGeometryCollection(criteria, input);
break;
case OVERLAPS:
LOGGER.debug("Doing OVERLAPS evaluation");
evaluation = overlapsWithGeometryCollection(criteria, input);
break;
// Unsupported as of release DDF 2.0.0 10/24/11
// case EQUALS:
// evaluation = criteria.equals(input);
// break;
//
// case DISJOINT:
// evaluation = criteria.disjoint(input);
// break;
//
// case INTERSECTS:
// evaluation = criteria.intersects(input);
// break;
//
// case TOUCHES:
// evaluation = criteria.touches(input);
// break;
//
// case CROSSES:
// evaluation = criteria.crosses(input);
// break;
//
// case WITHIN:
// evaluation = criteria.within(input);
// break;
default:
LOGGER.debug("Doing default evaluation - always false");
evaluation = false;
break;
}
} else {
LOGGER.debug("Doing DISTANCE evaluation");
// compare each geometry's closest distance to each other
double distanceBetweenNearestPtsOnGeometries = DistanceOp.distance(input, criteria);
LOGGER.debug("distanceBetweenNearestPtsOnGeometries = {}, distance = {}",
distanceBetweenNearestPtsOnGeometries,
distance);
evaluation = distanceBetweenNearestPtsOnGeometries <= distance;
}
LOGGER.debug("evaluation = {}", evaluation);
LOGGER.debug("EXITING: {}", methodName);
return evaluation;
}
public static Geometry buildGeometry(String gmlText)
throws IOException, SAXException, ParserConfigurationException {
String methodName = "buildGeometry";
LOGGER.debug("ENTERING: {}", methodName);
Geometry geometry = null;
gmlText = supportSRSName(gmlText);
try {
LOGGER.debug("Creating geoTools Configuration ...");
Configuration config = new org.geotools.gml3.GMLConfiguration();
LOGGER.debug("Parsing geoTools configuration");
Parser parser = new Parser(config);
LOGGER.debug("Parsing gmlText");
geometry = (Geometry) (parser.parse(new StringReader(gmlText)));
LOGGER.debug("geometry (before conversion): {}", geometry.toText());
// The metadata schema states that <gml:pos> elements specify points in
// LAT,LON order. But WKT specifies points in LON,LAT order. When the geoTools
// libraries return the geometry data, it's WKT is in LAT,LON order (which is
// incorrect).
// As a workaround here, for Polygons and Points (which are currently the only spatial
// criteria supported) we must swap the x,y of each coordinate so that they are
// specified in LON,LAT order and then use the swapped coordinates to create a new
// Polygon or Point to be returned to the caller.
GeometryFactory geometryFactory = new GeometryFactory();
if (geometry instanceof Polygon) {
// Build new array of coordinates using the swapped coordinates
ArrayList<Coordinate> newCoords = new ArrayList<Coordinate>();
// Swap each coordinate's x,y so that they specify LON,LAT order
for (Coordinate coord : geometry.getCoordinates()) {
newCoords.add(new Coordinate(coord.y, coord.x));
}
// Create a new polygon using the swapped coordinates
Polygon polygon =
new Polygon(geometryFactory.createLinearRing(newCoords.toArray(new Coordinate[newCoords.size()])),
null,
geometryFactory);
LOGGER.debug("Translates to {}", polygon.toText()); // this logs the transformed WKT
// with LON,LAT ordered points
LOGGER.debug("EXITING: {}", methodName);
return polygon;
}
if (geometry instanceof Point) {
// Create a new point using the swapped coordinates that specify LON,LAT order
Point point = geometryFactory.createPoint(new Coordinate(geometry.getCoordinate().y,
geometry.getCoordinate().x));
LOGGER.debug("Translates to {}", point.toText()); // this logs the transformed WKT
// with a LON,LAT ordered point
LOGGER.debug("EXITING: {}", methodName);
return point;
}
} catch (Exception e) {
LOGGER.debug("Exception using geotools", e);
}
LOGGER.debug("No translation done for geometry - probably not good ...");
LOGGER.debug("EXITING: {}", methodName);
return geometry;
}
public static String supportSRSName(String gml) {
String methodName = "supportSRSName";
LOGGER.debug("ENTERING: {}", methodName);
if (gml.contains(METADATA_DOD_MIL_CRS_WGS84E_2D)) {
gml = gml.replaceAll(Pattern.quote(METADATA_DOD_MIL_CRS_WGS84E_2D), EPSG_4326);
}
LOGGER.debug("EXITING: {} -- gml = {}", methodName, gml);
return gml;
}
}