package org.wikibrain.spatial.distance; import com.vividsolutions.jts.geom.Coordinate; import com.vividsolutions.jts.geom.Geometry; import com.vividsolutions.jts.geom.MultiPolygon; import com.vividsolutions.jts.geom.Point; import com.vividsolutions.jts.operation.distance.DistanceOp; import gnu.trove.set.TIntSet; import org.geotools.referencing.GeodeticCalculator; import org.wikibrain.core.dao.DaoException; import org.wikibrain.spatial.dao.SpatialDataDao; import org.wikibrain.spatial.util.ClosestPointIndex; import org.wikibrain.spatial.util.WikiBrainSpatialUtils; import java.awt.geom.Point2D; import java.util.ArrayList; import java.util.Collections; import java.util.List; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Estimates the number of kilometers between geometries. * * TODO: handle non-points for neighbors. * * @author Shilad Sen */ public class GeodeticDistanceMetric implements SpatialDistanceMetric { private static final double EARTH_RADIUS = 6371.0 * 1000; // radius of the earth in meters private static final Logger LOG = LoggerFactory.getLogger(SpatialDistanceMetric.class); private final ClosestPointIndex index; private final SpatialDataDao spatialDao; private TIntSet concepts; private boolean useBorders = false; /** * Creates a new geodetic spatial distance metric. * * If useBorder is true, it computes the distance between * (multi)polygons by finding the closest two points on the boundary. * * Otherwise it computes distances between centroids. * * TODO: consider other methods (e.g. capitials,population-weighted centroids, etc). * * @param spatialDao * @param useBorders */ public GeodeticDistanceMetric(SpatialDataDao spatialDao, ClosestPointIndex index, boolean useBorders) { this.spatialDao = spatialDao; this.index = index; this.useBorders = useBorders; } public GeodeticDistanceMetric(SpatialDataDao spatialDao, SphericalDistanceMetric spherical) { this(spatialDao, spherical.getIndex(), false); } @Override public void setValidConcepts(TIntSet concepts) { this.concepts = concepts; } /** * TODO: handle non-point geometries. * @param enable * @throws DaoException */ @Override public void enableCache(boolean enable) throws DaoException { // Do nothing, for now } @Override public String getName() { return "geodetic distance metric"; } @Override public double distance(Geometry g1, Geometry g2) { return distance(new GeodeticCalculator(), g1, g2); } @Override public float[][] distance(List<Geometry> rowGeometries, List<Geometry> colGeometries) { GeodeticCalculator calc = new GeodeticCalculator(); float [][] matrix = new float[rowGeometries.size()][colGeometries.size()]; for (int i = 0; i < rowGeometries.size(); i++) { for (int j = 0; j < colGeometries.size(); j++) { if (rowGeometries.get(i) == colGeometries.get(j)) { matrix[i][j] = 0f; } else { matrix[i][j] = (float) distance(calc, rowGeometries.get(i), colGeometries.get(j)); } } } return matrix; } @Override public float[][] distance(List<Geometry> geometries) { return distance(geometries, geometries); } @Override public List<Neighbor> getNeighbors(Geometry g, int maxNeighbors) { return getNeighbors(g, maxNeighbors, Double.MAX_VALUE); } /** * Returns the closest points to a particular geometry. * Note that this ONLY currently uses centroids (FIXME). * * @param g * @param maxNeighbors * @param maxDistance * @return */ @Override public List<Neighbor> getNeighbors(Geometry g, int maxNeighbors, double maxDistance) { Point c = WikiBrainSpatialUtils.getCenter(g); GeodeticCalculator calc = new GeodeticCalculator(); calc.setStartingGeographicPoint(c.getX(), c.getY()); List<Neighbor> results = new ArrayList<Neighbor>(); for (ClosestPointIndex.Result r: index.query(g, maxNeighbors)) { if (r.distance < maxDistance) { double kms; try { calc.setDestinationGeographicPoint(r.point.getX(), r.point.getY()); kms = calc.getOrthodromicDistance(); } catch (ArithmeticException e) { kms = r.distance; } catch (IllegalArgumentException e) { kms = r.distance; } if (kms <= maxDistance) { results.add(new Neighbor(r.id, kms)); } } } Collections.sort(results); if (results.size() > maxNeighbors) { results = results.subList(0, maxNeighbors); } return results; } public Geometry cleanupGeometry(Geometry g) { if (!(g instanceof MultiPolygon)) { return g; } Geometry largest = null; double largestArea = -1; MultiPolygon mp = (MultiPolygon)g; for (int i = 0; i < mp.getNumGeometries(); i++) { Geometry g2 = mp.getGeometryN(i); double area = g2.getArea(); if (area > largestArea) { largestArea = area; largest = g2; } } return largest; } private double distance(GeodeticCalculator calc, Geometry g1, Geometry g2) { try { if (useBorders) { g1 = cleanupGeometry(g1); g2 = cleanupGeometry(g2); Coordinate[] pair = DistanceOp.nearestPoints(g1, g2); calc.setStartingGeographicPoint(pair[0].x, pair[0].y); calc.setDestinationGeographicPoint(pair[1].x, pair[1].y); } else { Point p1 = WikiBrainSpatialUtils.getCenter(g1); Point p2 = WikiBrainSpatialUtils.getCenter(g2); calc.setStartingGeographicPoint(p1.getX(), p1.getY()); calc.setDestinationGeographicPoint(p2.getX(), p2.getY()); } return calc.getOrthodromicDistance(); } catch (ArithmeticException e) { Point p1 = WikiBrainSpatialUtils.getCenter(g1); Point p2 = WikiBrainSpatialUtils.getCenter(g2); return WikiBrainSpatialUtils.haversine(p1.getX(), p1.getY(), p2.getX(), p2.getY()); } catch (IllegalArgumentException e) { Point p1 = WikiBrainSpatialUtils.getCenter(g1); Point p2 = WikiBrainSpatialUtils.getCenter(g2); return WikiBrainSpatialUtils.haversine(p1.getX(), p1.getY(), p2.getX(), p2.getY()); } } }