package uk.ac.ox.zoo.seeg.abraid.mp.dataacquisition.qc; import com.vividsolutions.jts.geom.Point; import uk.ac.ox.zoo.seeg.abraid.mp.common.domain.AdminUnitQC; import uk.ac.ox.zoo.seeg.abraid.mp.common.domain.Location; import uk.ac.ox.zoo.seeg.abraid.mp.common.domain.LocationPrecision; import uk.ac.ox.zoo.seeg.abraid.mp.common.util.GeometryUtils; import java.util.List; /** * Finds the admin1 or admin2 that is associated with a location. * * Copyright (c) 2014 University of Oxford */ public class AdminUnitFinder { // Maximum width and height of the admin units (in degrees) private static final int MAX_ADMIN_UNIT_WIDTH = 61; private static final int MAX_ADMIN_UNIT_HEIGHT = 32; // The maximum distance that a location can be from an admin unit's centroid, expressed as a percentage of the // square root of the admin unit's area private static final double MAXIMUM_PERCENTAGE_OF_SQUARE_ROOT_OF_AREA = 30; private static final String FOUND_MESSAGE = "closest distance is %.2f%% of the square root of the area"; private static final String NOT_FOUND_MESSAGE = "closest distance is %.2f%% of the square root of the area " + "(GAUL code %d: \"%s\")"; private static final double RATIO_TO_PERCENTAGE = 100.0; private static final int MAX_LONGITUDE = 180; private static final int MAX_LATITUDE = 90; // The admin unit that is closest to the location, with the closest distance, within the maximum distance // allowed. private AdminUnitQC closestAdminUnit; private double closestDistance = 0; private String message; private boolean passed = true; /** * Finds the closest admin unit associated with the specified location. To do this, we find the distance between * each admin unit's centroid and the specified location, ignoring distances that are greater than the maximum * allowed distance for the admin unit (see method getMaximumDistanceFromCentroid). * * @param location The location. Must be ADMIN1 or ADMIN2. * @param adminUnits A list of admin units for comparison. */ public void findClosestAdminUnit(Location location, List<AdminUnitQC> adminUnits) { validateLocation(location); char adminLevel = location.getPrecision().getShapefileTableAdminLevel(); // The admin unit that is closest to the location, with the closest distance. This is stored for logging // purposes only. AdminUnitQC closestAdminUnitForLogging = null; double closestDistanceForLogging = 0; for (AdminUnitQC adminUnit : adminUnits) { if (adminUnit.getAdminLevel() == adminLevel) { // This admin unit is at the desired level // So find the distance between the input location and the admin unit's centroid Point adminUnitCentroid = GeometryUtils.createPoint( adminUnit.getCentroidLongitude(), adminUnit.getCentroidLatitude()); if (!isDistanceBeyondMaximumAdminUnitSize(location.getGeom(), adminUnitCentroid)) { double distance = GeometryUtils.findOrthodromicDistance(location.getGeom(), adminUnitCentroid); // If this is the closest admin unit so far, within the maximum distance allowed, store it if (distance < getMaximumDistanceFromCentroid(adminUnit) && (closestAdminUnit == null || distance < closestDistance)) { closestAdminUnit = adminUnit; closestDistance = distance; } // If this is the closest admin unit so far, store it so that we can log its details if there are // no admin units within the maximum distance allowed if (closestAdminUnitForLogging == null || distance < closestDistanceForLogging) { closestAdminUnitForLogging = adminUnit; closestDistanceForLogging = distance; } } } } // If no sufficiently close admin unit is found, log the closest match if (closestAdminUnit != null) { double percentage = percentageOfSquareRootOfArea(closestAdminUnit, closestDistance); message = String.format(FOUND_MESSAGE, percentage); } else if (closestAdminUnitForLogging != null) { double percentage = percentageOfSquareRootOfArea(closestAdminUnitForLogging, closestDistanceForLogging); message = String.format(NOT_FOUND_MESSAGE, percentage, closestAdminUnitForLogging.getGaulCode(), closestAdminUnitForLogging.getName()); passed = false; } } /** * Gets the closest admin unit. * @return The admin unit whose centroid is closest to the specified location, as long as the distance is within * the maximum allowed. Returns null if no such centroid exists. */ public AdminUnitQC getClosestAdminUnit() { return closestAdminUnit; } /** * Gets the message. * @return A message explaining the result of this QC stage. */ public String getMessage() { return message; } /** * Returns whether or not the location has passed this QC stage. * @return Whether or not the location has passed this QC stage. */ public boolean hasPassed() { return passed; } private void validateLocation(Location location) { if (location.getPrecision() != LocationPrecision.ADMIN1 && location.getPrecision() != LocationPrecision.ADMIN2) { throw new IllegalArgumentException("Location must be an admin1 or admin2"); } if (location.getGeom() == null) { throw new IllegalArgumentException("Location must have a point"); } } private double getMaximumDistanceFromCentroid(AdminUnitQC adminUnit) { return Math.sqrt(adminUnit.getArea()) * MAXIMUM_PERCENTAGE_OF_SQUARE_ROOT_OF_AREA / RATIO_TO_PERCENTAGE; } private double percentageOfSquareRootOfArea(AdminUnitQC adminUnit, double distance) { return distance * RATIO_TO_PERCENTAGE / Math.sqrt(adminUnit.getArea()); } private boolean isDistanceBeyondMaximumAdminUnitSize(Point location, Point adminUnitCentroid) { // The admin units are never greater than 61 degrees wide and 32 degrees high. So if the distance // between the points is greater than this, the location cannot possibly be close enough to the centroid. // This routine avoids calculating the orthodromic distance for such points. return (wrappedDifference(location.getX(), adminUnitCentroid.getX(), MAX_LONGITUDE) > MAX_ADMIN_UNIT_WIDTH) || (wrappedDifference(location.getY(), adminUnitCentroid.getY(), MAX_LATITUDE) > MAX_ADMIN_UNIT_HEIGHT); } private double wrappedDifference(double a, double b, double maximumDifference) { double difference = Math.abs(a - b); return (difference > maximumDifference) ? (maximumDifference * 2 - difference) : difference; } }