Skip to content

Commit

Permalink
Adding implementation of tdwg/bdq#287 using @jhnwllr's catalog of cen…
Browse files Browse the repository at this point in the history
…troids PCLI file converted to a shape file. This was passing all but one row in @Tasilee's test validation data, but have also added the proposed test for large coordinate uncertainty relative to country size proposed as a change to the specification.
  • Loading branch information
chicoreus committed Aug 27, 2024
1 parent 28ac2d5 commit 307a00a
Show file tree
Hide file tree
Showing 15 changed files with 421 additions and 53 deletions.
167 changes: 118 additions & 49 deletions src/main/java/org/filteredpush/qc/georeference/DwCGeoRefDQ.java

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
import org.datakurator.ffdq.api.DQResponse;
import org.datakurator.ffdq.api.result.AmendmentValue;
import org.datakurator.ffdq.api.result.ComplianceValue;
import org.datakurator.ffdq.api.result.IssueValue;
import org.filteredpush.qc.georeference.util.GettyLookup;

/**
Expand Down Expand Up @@ -388,4 +389,29 @@ public static DQResponse<AmendmentValue> amendmentCountrycodeFromCoordinates(
) {
return DwCGeoRefDQ.amendmentCountrycodeFromCoordinates(decimalLatitude, decimalLongitude, countryCode, null);
}

/**
* Are the supplied geographic coordinates within a defined buffer of the center of the country?
* using the default source authority and spatial buffer.
*
* Provides: 287 ISSUE_COORDINATES_CENTEROFCOUNTRY
* Version: 2023-09-17
*
* @param decimalLatitude the provided dwc:decimalLatitude to evaluate as ActedUpon.
* @param decimalLongitude the provided dwc:decimalLongitude to evaluate as ActedUpon.
* @param countryCode the provided dwc:countryCode to evaluate as Consulted.
* @return DQResponse the response of type AmendmentValue to return
*/
@Amendment(label="ISSUE_COORDINATES_CENTEROFCOUNTRY", description="Are the supplied geographic coordinates within a defined buffer of the center of the country?")
@Provides("256e51b3-1e08-4349-bb7e-5186631c3f8e")
@ProvidesVersion("https://rs.tdwg.org/bdqcore/terms/256e51b3-1e08-4349-bb7e-5186631c3f8e/2024-08-20")
@Specification("EXTERNAL_PREREQUISITES_NOT_MET if the bdq:sourceAuthority is not available; INTERNAL_PREREQUISITES_NOT_MET if any of dwc:countryCode, dwc:decimalLatitude, dwc:decimalLongitude are EMPTY; POTENTIAL_ISSUE if the geographic coordinates are within the distance given by bdq:spatialBufferInMeters from the center (or one of the centers), of the bdq:sourceAuthority provides more than one per country code of the supplied dwc:countryCode as represented in the bdq:sourceAuthority; otherwise NOT_ISSUE.")
public static DQResponse<IssueValue> issueCoordinatesCenterofcountry(
@ActedUpon("dwc:decimalLatitude") String decimalLatitude,
@ActedUpon("dwc:decimalLongitude") String decimalLongitude,
@Consulted("dwc:countryCode") String countryCode,
@Consulted("dwc:coordinateUncertaintyInMeters") String coordinateUncertaintyInMeters
) {
return DwCGeoRefDQ.issueCoordinatesCenterofcountry(decimalLatitude, decimalLongitude, countryCode, coordinateUncertaintyInMeters, null, null);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ public enum EnumGeoRefSourceAuthority {
NE_ADMIN_0,
GADM_ADM1,
GETTY_TGN,
GBIF_CENTROIDS,
DATAHUB,
INVALID;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,12 @@ public GeoRefSourceAuthority(String authorityString) throws SourceAuthorityExcep
this.authority = EnumGeoRefSourceAuthority.GETTY_TGN;
} else if (authorityString.toUpperCase().equals("GETTY TGN")) {
this.authority = EnumGeoRefSourceAuthority.GETTY_TGN;
} else if (authorityString.toUpperCase().equals("GBIF CATALOGUE OF COUNTRY CENTROIDES")) {
this.authority = EnumGeoRefSourceAuthority.GBIF_CENTROIDS;
} else if (authorityString.toUpperCase().equals("GBIF CATALOGUE OF COUNTRY CENTROIDS")) {
this.authority = EnumGeoRefSourceAuthority.GBIF_CENTROIDS;
} else if (authorityString.toUpperCase().equals("CATALOGUE-OF-CENTROIDS")) {
this.authority = EnumGeoRefSourceAuthority.GBIF_CENTROIDS;
} else if (authorityString.toUpperCase().startsWith("HTTPS://INVALID/")) {
this.authority = EnumGeoRefSourceAuthority.INVALID;
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.filteredpush.qc.georeference.SourceAuthorityException;
import org.geotools.api.data.FileDataStore;
import org.geotools.api.data.FileDataStoreFinder;
import org.geotools.api.data.SimpleFeatureSource;
Expand Down Expand Up @@ -373,8 +374,9 @@ public static boolean isOnLand(double Xvalue, double Yvalue, boolean invertSense
* false if the x/y value is inside land and invertSense is true
* true if the x/y value is outside land and invertSense is true
* @param bufferInMeters a double.
* @throws SourceAuthorityException
*/
public static boolean isOnOrNearLand(double Xvalue, double Yvalue, boolean invertSense, double bufferInMeters) {
public static boolean isOnOrNearLand(double Xvalue, double Yvalue, boolean invertSense, double bufferInMeters) throws SourceAuthorityException {
boolean result = false;

double bufferKm = bufferInMeters/1000;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,14 @@

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.filteredpush.qc.georeference.SourceAuthorityException;
import org.geotools.api.data.FileDataStore;
import org.geotools.api.data.FileDataStoreFinder;
import org.geotools.api.data.SimpleFeatureSource;
import org.geotools.api.feature.simple.SimpleFeature;
import org.geotools.api.filter.Filter;
import org.geotools.data.simple.SimpleFeatureCollection;
import org.geotools.data.simple.SimpleFeatureIterator;
import org.geotools.filter.text.cql2.CQLException;
import org.geotools.filter.text.ecql.ECQL;

Expand Down Expand Up @@ -93,8 +96,9 @@ public boolean pointIsWithinLand(double longitude, double latitude, boolean inve
* @param invertSense a boolean.
* @return a boolean.
* @param distanceKm a double.
* @throws SourceAuthorityException
*/
public boolean pointIsWithinOrNearLand(double longitude, double latitude, boolean invertSense, double distanceKm) {
public boolean pointIsWithinOrNearLand(double longitude, double latitude, boolean invertSense, double distanceKm) throws SourceAuthorityException {

boolean result = false;

Expand All @@ -115,8 +119,10 @@ public boolean pointIsWithinOrNearLand(double longitude, double latitude, boolea
result = !collection.isEmpty();
} catch (IOException e) {
logger.error(e.getMessage(), e);
throw new SourceAuthorityException("Error reading spatial data file: " + e.getMessage());
} catch (CQLException e) {
logger.error(e.getMessage(), e);
throw new SourceAuthorityException("Error querying spatial data file: " + e.getMessage());
} finally {
// close
if (store!=null) {
Expand All @@ -135,6 +141,110 @@ public boolean pointIsWithinOrNearLand(double longitude, double latitude, boolea
return result;
}

/**
* Determine if a point is near the centroid of a country.
*
* @param longitude a double representation of the longigude.
* @param latitude a double representation of the latitude.
* @param countryCode to check for centroids of.
* @param distanceKm the buffer distance in km.
* @return true if latitude and longitude are within buffer distance of a centroid for the country code.
* @throws SourceAuthorityException
*/
public static boolean isPointNearCentroid(double longitude, double latitude, String countryCode, double distanceKm) throws SourceAuthorityException {

boolean result = false;

URL centroidShapeFile = GEOUtil.class.getResource("/org.filteredpush.kuration.services/gbif_pcli_country_centroids.shp");
FileDataStore store = null;
try {
store = FileDataStoreFinder.getDataStore(centroidShapeFile);
SimpleFeatureSource featureSource = store.getFeatureSource();
logger.debug(featureSource.getInfo().toString());
logger.debug(featureSource.getName().toString());
double distanceD = distanceKm / 111d; // GeoTools ignores units, uses units of underlying projection (degrees in this case), fudge by dividing km by number of km in one degree of latitude (this will describe a wide ellipse far north or south).
StringBuffer filterString = new StringBuffer();
filterString.append("DWITHIN(the_geom, POINT(" + Double.toString(longitude) + " " + Double.toString(latitude) + "), "+ distanceD +", kilometers)");
filterString.append(" AND ");
filterString.append("iso2 ILIKE '"+ countryCode +"' ");
logger.debug(filterString);
Filter filter = ECQL.toFilter(filterString.toString());
SimpleFeatureCollection collection=featureSource.getFeatures(filter);
result = !collection.isEmpty();
} catch (IOException e) {
logger.error(e.getMessage(), e);
throw new SourceAuthorityException("Error reading country centroids: " + e.getMessage());
} catch (CQLException e) {
logger.error(e.getMessage(), e);
throw new SourceAuthorityException("Error reading country centroids: " + e.getMessage());
} finally {
// close
if (store!=null) {
try {
store.dispose();
} catch (Exception e) {
logger.error(e.getMessage());
}
}
}

return result;
}

/**
* Obtain a value for the area of a country.
*
* @param countryCode to check for area.
* @return area of country in square km, or null if not found
*/
public static Double getAreaOfCountry(String countryCode) {

Double result = null;

URL centroidShapeFile = GEOUtil.class.getResource("/org.filteredpush.kuration.services/gbif_pcli_country_centroids.shp");
FileDataStore store = null;
try {
store = FileDataStoreFinder.getDataStore(centroidShapeFile);
SimpleFeatureSource featureSource = store.getFeatureSource();
logger.debug(featureSource.getInfo().toString());
logger.debug(featureSource.getName().toString());
StringBuffer filterString = new StringBuffer();
filterString.append("iso2 ILIKE '"+ countryCode +"' ");
logger.debug(filterString);
Filter filter = ECQL.toFilter(filterString.toString());
SimpleFeatureCollection collection=featureSource.getFeatures(filter);
if (!collection.isEmpty()) {
SimpleFeatureIterator i = collection.features();
boolean found = false;
while (i.hasNext() && !found) {
SimpleFeature feature = i.next();
Object areaObject = feature.getAttribute("area_sqkm");
logger.debug(areaObject);
try {
result = (Double)areaObject;
found = true;
} catch (ClassCastException e) {
logger.debug(e.getMessage());
}
}
i.close();
}
} catch (IOException e) {
logger.error(e.getMessage(), e);
} catch (CQLException e) {
logger.error(e.getMessage(), e);
} finally {
// close
if (store!=null) {
try {
store.dispose();
} catch (Exception e) {
logger.error(e.getMessage());
}
}
}

return result;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
UTF-8
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
GEOGCS["GCS_WGS_1984",DATUM["D_WGS_1984",SPHEROID["WGS_1984",6378137.0,298.257223563]],PRIMEM["Greenwich",0.0],UNIT["Degree",0.0174532925199433]]
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<!DOCTYPE qgis PUBLIC 'http://mrcc.com/qgis.dtd' 'SYSTEM'>
<qgis version="3.22.16-Białowieża">
<identifier>https://github.com/jhnwllr/catalogue-of-centroids/</identifier>
<parentidentifier></parentidentifier>
<language></language>
<type></type>
<title>GBIF Catalog of Country Centroids</title>
<abstract>From John Waller https://github.com/jhnwllr/catalogue-of-centroids/ source file https://raw.githubusercontent.com/jhnwllr/catalogue-of-centroids/0959e14d0e7a90de28ae2bc23cf570b791e112ef/PCLI.tsv</abstract>
<contact>
<name></name>
<organization></organization>
<position></position>
<voice></voice>
<fax></fax>
<email></email>
<role></role>
</contact>
<links/>
<fees></fees>
<rights>John Waller</rights>
<rights>GBIF</rights>
<encoding></encoding>
<crs>
<spatialrefsys>
<wkt></wkt>
<proj4></proj4>
<srsid>0</srsid>
<srid>0</srid>
<authid></authid>
<description></description>
<projectionacronym></projectionacronym>
<ellipsoidacronym></ellipsoidacronym>
<geographicflag>false</geographicflag>
</spatialrefsys>
</crs>
<extent>
<spatial maxx="0" maxz="0" crs="" minx="0" miny="0" dimensions="2" minz="0" maxy="0"/>
<temporal>
<period>
<start></start>
<end></end>
</period>
</temporal>
</extent>
</qgis>
Binary file not shown.
Binary file not shown.
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,5 @@ Flanders Marine Institute (2019). Maritime Boundaries Geodatabase: Maritime Boun
Creative Commons Attribution 4.0 International License.

The files land_and_eez.shp and merged_countries_and_eez.shp are made from mergers of the above data sources.

Country Centroids: This (gbif_pcli_country_centroids.shp) is a shape file created from the Centroids for Country Codes PCLI.tsv file compiled by John Waller for GBIF see: https://github.com/jhnwllr/catalogue-of-centroids. The shapefile was created by importing the PCLI.tsv file into QGIS, then exporting as a shapefile.
39 changes: 38 additions & 1 deletion src/test/java/org/filteredpush/qc/geo/test/GeoTesterTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@
*/
package org.filteredpush.qc.geo.test;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.filteredpush.qc.georeference.GeoTester;
import org.filteredpush.qc.georeference.SourceAuthorityException;
import org.filteredpush.qc.georeference.util.GEOUtil;
import org.filteredpush.qc.georeference.util.GISDataLoader;
import org.junit.Before;
Expand All @@ -21,6 +24,8 @@
*/
public class GeoTesterTest {
private GeoTester geoTester;

private static final Log logger = LogFactory.getLog(GEOUtil.class);

@Before
public void init() throws IOException {
Expand Down Expand Up @@ -162,7 +167,39 @@ public void testparseVerbatimLatLongToDecimalDegree() {
@Test
public void testisPrimaryAloneKnown() {
assertTrue(GEOUtil.isPrimaryAloneKnown("Rio Negro"));

assertTrue(GEOUtil.isPrimaryAloneKnown("Nevada"));
assertFalse(GEOUtil.isPrimaryAloneKnown("Not the name of a state"));
}

@Test
public void testisPointNearCentroid() {
String countryCode="JM";
double decimalLongitude = -77.250d;
double decimalLatitude = 18.1667d;
double bufferKm = 3d;
try {
assertTrue(GISDataLoader.isPointNearCentroid(decimalLongitude, decimalLatitude, countryCode, bufferKm));
} catch (SourceAuthorityException e) {
fail("Unexpected exception: " + e.getMessage());
}

countryCode="JM";
decimalLongitude = 77.250d;
decimalLatitude = 18.1667d;
bufferKm = 3d;
try {
assertFalse(GISDataLoader.isPointNearCentroid(decimalLongitude, decimalLatitude, countryCode, bufferKm));
} catch (SourceAuthorityException e) {
fail("Unexpected exception: " + e.getMessage());
}

}

@Test
public void testgetAreaOfCountry() {
String countryCode="JM";
logger.debug(GISDataLoader.getAreaOfCountry(countryCode));
assertEquals(11032d, GISDataLoader.getAreaOfCountry(countryCode), 1d);
}
}

Expand Down
Loading

0 comments on commit 307a00a

Please sign in to comment.