package uk.ac.ox.zoo.seeg.abraid.mp.dataacquisition.qc; import com.vividsolutions.jts.geom.MultiPolygon; import com.vividsolutions.jts.geom.Point; import org.springframework.util.StringUtils; 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; /** * The Quality Control (QC) manager. Performs quality control on a location. * * Copyright (c) 2014 University of Oxford */ public class QCManager { private static final String MESSAGE_FORMAT = "QC stage %d %s: %s."; private static final String PASSED = "passed"; private static final String FAILED = "failed"; private static final String NOT_ADMIN12_MESSAGE = "location not an ADMIN1 or ADMIN2"; private static final String NO_COUNTRIES_MESSAGE = "no country geometries associated with this location"; private static final int STAGE_1_ID = 1; private static final int STAGE_2_ID = 2; private static final int STAGE_3_ID = 3; private static final int STAGE_2_MAXIMUM_DISTANCE = 5; private static final int STAGE_3_MAXIMUM_DISTANCE = 5; private static final String STAGE_2_GEOMETRY_DESCRIPTION = "land"; private static final String STAGE_3_GEOMETRY_DESCRIPTION = "country"; private final QCLookupData qcLookupData; public QCManager(QCLookupData qcLookupData) { this.qcLookupData = qcLookupData; } /** * Performs quality control (QC) on a location. Each QC step is performed until a step fails. * @param location The location. * @return True if the location passed all QC checks, otherwise false. */ public boolean performQC(Location location) { initializeLocationForQC(location); return performQCStage1(location) && performQCStage2(location) && performQCStage3(location); } private void initializeLocationForQC(Location location) { location.setAdminUnitQCGaulCode(null); location.setQcMessage(null); } private boolean performQCStage1(Location location) { boolean passed = true; String message = NOT_ADMIN12_MESSAGE; if (isAdmin1OrAdmin2(location)) { // Location is an admin1 or admin2, so find the closest admin unit to the location // (as long as it is close enough - see class AdminUnitFinder for details) AdminUnitFinder adminUnitFinder = new AdminUnitFinder(); adminUnitFinder.findClosestAdminUnit(location, qcLookupData.getAdminUnits()); AdminUnitQC closestAdminUnit = adminUnitFinder.getClosestAdminUnit(); if (closestAdminUnit != null) { location.setAdminUnitQCGaulCode(closestAdminUnit.getGaulCode()); } passed = adminUnitFinder.hasPassed(); message = adminUnitFinder.getMessage(); } appendQcMessage(location, STAGE_1_ID, passed, message); return passed; } private boolean performQCStage2(Location location) { boolean passed = true; String message; // Adjust the centroid of the country if necessary. For example, the centroid of the Philippines is not on // land, so replace it with a predefined point in the Philippines that is on land. CountryCentroidAdjuster adjuster = new CountryCentroidAdjuster(); boolean hasBeenAdjusted = adjuster.adjustCountryCentroid(location, qcLookupData.getHealthMapCountryMap()); if (hasBeenAdjusted) { message = adjuster.getMessage(); } else { // Ensure that the location is on land. If not, snap to land if within the maximum distance away. Snapper snapper = new Snapper(STAGE_2_GEOMETRY_DESCRIPTION, STAGE_2_MAXIMUM_DISTANCE); applySnapperToLocation(snapper, location, qcLookupData.getLandSeaBorders()); message = snapper.getMessage(); passed = snapper.hasPassed(); } appendQcMessage(location, STAGE_2_ID, passed, message); return passed; } private boolean performQCStage3(Location location) { // Ensure that the location is within the geometries associated with the location's country. If not, snap // to the geometries if within the maximum distance away. By default the stage fails, because if a country // geometry is not found then the country is not of interest. boolean passed = false; String message = NO_COUNTRIES_MESSAGE; MultiPolygon countryGeometry = getCountryGeometryForLocation(location); if (countryGeometry != null) { Snapper snapper = new Snapper(STAGE_3_GEOMETRY_DESCRIPTION, STAGE_3_MAXIMUM_DISTANCE); applySnapperToLocation(snapper, location, countryGeometry); passed = snapper.hasPassed(); message = snapper.getMessage(); } appendQcMessage(location, STAGE_3_ID, passed, message); return passed; } private boolean isAdmin1OrAdmin2(Location location) { LocationPrecision precision = location.getPrecision(); return (precision == LocationPrecision.ADMIN1) || (precision == LocationPrecision.ADMIN2); } private void applySnapperToLocation(Snapper snapper, Location location, MultiPolygon geometry) { if (geometry != null) { snapper.ensureWithinGeometry(location, geometry); Point closestPoint = snapper.getClosestPoint(); if (closestPoint != null) { // Set the location to the returned closest point, which has been snapped if necessary location.setGeom(closestPoint); } } } private MultiPolygon getCountryGeometryForLocation(Location location) { MultiPolygon countryGeometry = null; if (location.getHealthMapCountryId() != null) { countryGeometry = qcLookupData.getHealthMapCountryGeometryMap().get(location.getHealthMapCountryId()); } else if (location.getCountryGaulCode() != null) { countryGeometry = qcLookupData.getCountryGeometryMap().get(location.getCountryGaulCode()); } return countryGeometry; } private void appendQcMessage(Location location, int qcStage, boolean hasPassed, String qcMessage) { if (StringUtils.hasText(qcMessage)) { qcMessage = String.format(MESSAGE_FORMAT, qcStage, hasPassed ? PASSED : FAILED, qcMessage); String locationQcMessage = location.getQcMessage(); if (locationQcMessage == null) { locationQcMessage = ""; } if (StringUtils.hasText(locationQcMessage)) { // QC messages are separated by a single space locationQcMessage += " "; } locationQcMessage += qcMessage; location.setQcMessage(locationQcMessage); } } }