Skip to content

Commit

Permalink
Add new geofence XPath function and tests (#771)
Browse files Browse the repository at this point in the history
Co-authored-by: Dr. Gareth S. Bestor <tiritea@yahoo.com>
  • Loading branch information
lognaturel and tiritea committed Jun 21, 2024
1 parent 3bba4dd commit 6e72a2a
Show file tree
Hide file tree
Showing 4 changed files with 114 additions and 1 deletion.
34 changes: 34 additions & 0 deletions src/main/java/org/javarosa/core/util/GeoUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,40 @@ public static double calculateDistance(List<LatLong> points) {
return totalDistance;
}

/**
* Returns whether a geopoint is inside the specified geoshape; aka 'geofencing'
* @param point the geopoint location to check for inclusion.
* @param polygon the closed list of geoshape coordinates defining the polygon 'fence'.
* @return true if the location is inside the polygon; false otherwise.
*
* Adapted from https://wrfranklin.org/Research/Short_Notes/pnpoly.html:
*
* int pnpoly(int nvert, float *vertx, float *verty, float testx, float testy) {
* int i, j, c = 0;
* for (i = 0, j = nvert - 1; i < nvert; j = i++) {
* if (((verty[i] > testy) != (verty[j] > testy)) &&
* (testx < (vertx[j] - vertx[i]) * (testy - verty[i]) / (verty[j] - verty[i]) + vertx[i]))
* c = !c;
* }
* return c;
* }
*/
public static boolean calculateIsPointInGPSPolygon(LatLong point, List<LatLong> polygon) {
double nvert = polygon.size();
double testx = point.longitude; // x maps to longitude
double testy = point.latitude; // y maps to latitude
boolean c = false;
for (int i = 1; i < nvert; i++) { // geoshapes already duplicate the first point to last, so unlike the original algorithm there is no need to wrap j
LatLong p1 = polygon.get(i-1); // this is effectively j in the original algorithm
LatLong p2 = polygon.get(i); // this is effectively i in the original algorithm
if (((p2.latitude > testy) != (p1.latitude > testy)) &&
(testx < (p1.longitude - p2.longitude) * (testy - p2.latitude) / (p1.latitude - p2.latitude) + p2.longitude)) {
c = !c;
}
}
return c;
}

private static void logDistance(LatLong p1, LatLong p2, double distance, double totalDistance) {
logger.trace("\t{}\t{}\t{}\t{}\t{}\t{}",
p1.latitude, p1.longitude,
Expand Down
10 changes: 9 additions & 1 deletion src/main/java/org/javarosa/xpath/expr/XPathFuncExpr.java
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@
import org.javarosa.core.model.condition.IFallbackFunctionHandler;
import org.javarosa.core.model.condition.IFunctionHandler;
import org.javarosa.core.model.condition.pivot.UnpivotableExpressionException;
import org.javarosa.core.model.data.GeoPointData;
import org.javarosa.core.model.data.UncastData;
import org.javarosa.core.model.instance.DataInstance;
import org.javarosa.core.model.instance.FormInstance;
import org.javarosa.core.model.instance.TreeReference;
Expand Down Expand Up @@ -490,7 +492,13 @@ public Object eval(DataInstance model, EvaluationContext evalContext) {
} else {
throw new XPathUnhandledException("function 'distance' requires at least one parameter.");
}
} else if (name.equals("digest") && (args.length == 2 || args.length == 3)) {
} else if (name.equals("geofence")) {
assertArgsCount(name, args, 2);
GeoPointData geoPointData = new GeoPointData().cast(new UncastData(XPathFuncExpr.toString(argVals[0])));
GeoUtils.LatLong point = new GeoUtils.LatLong(geoPointData.getPart(0), geoPointData.getPart(1));
List<GeoUtils.LatLong> latLongs = new XPathFuncExprGeo().getGpsCoordinatesFromNodeset(name, argVals[1]);
return GeoUtils.calculateIsPointInGPSPolygon(point, latLongs);
} else if (name.equals("digest") && (args.length == 2 || args.length == 3)) {
return DigestAlgorithm.from(toString(argVals[1])).digest(
toString(argVals[0]),
args.length == 3 ? Encoding.from(toString(argVals[2])) : Encoding.BASE64
Expand Down
52 changes: 52 additions & 0 deletions src/test/java/org/javarosa/core/util/GeoUtilsTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,47 @@ public void oneDegreeLongChgAt90Lat() {
}), 1e-6);
}

/*
https://www.mapdevelopers.com/area_finder.php?polygons=%5B%5B%5B%5B38.253094215699576%2C21.756382658677467%5D%2C%5B38.25021274773806%2C21.756382658677467%5D%2C%5B38.25007793942195%2C21.763892843919166%5D%2C%5B38.25290886154963%2C21.763935759263404%5D%2C%5B38.25146813817506%2C21.758421137528785%5D%2C%5B38.253094215699576%2C21.756382658677467%5D%5D%2C%22%230000FF%22%2C%22%23FF0000%22%2C0.4%5D%5D
*/
@Test
public void pointIsInPolygon() {
checkPointInPolygon(38.25081280703969, 21.760299116099116,
new double[][]{
{38.253094215699576, 21.756382658677467},
{38.25021274773806, 21.756382658677467},
{38.25007793942195, 21.763892843919166},
{38.25290886154963, 21.763935759263404},
{38.25146813817506, 21.758421137528785},
{38.253094215699576, 21.756382658677467} // last point in geoshape must match first
}, true);
}

@Test
public void point2IsInPolygon() {
checkPointInPolygon(38.251790, 21.756845,
new double[][]{
{38.253094215699576, 21.756382658677467},
{38.25021274773806, 21.756382658677467},
{38.25007793942195, 21.763892843919166},
{38.25290886154963, 21.763935759263404},
{38.25146813817506, 21.758421137528785},
{38.253094215699576, 21.756382658677467} // last point in geoshape must match first
}, true);
}
@Test
public void pointIsNotInPolygon() {
checkPointInPolygon(38.252062644683356, 21.758894013612437,
new double[][]{
{38.253094215699576, 21.756382658677467},
{38.25021274773806, 21.756382658677467},
{38.25007793942195, 21.763892843919166},
{38.25290886154963, 21.763935759263404},
{38.25146813817506, 21.758421137528785},
{38.253094215699576, 21.756382658677467} // last point in geoshape must match first
}, false);
}

private double distance(double[][] points) {
return GeoUtils.calculateDistance(getLatLongs(points));
}
Expand All @@ -126,4 +167,15 @@ private List<GeoUtils.LatLong> getLatLongs(double[][] points) {
}
return latLongs;
}

private void checkPointInPolygon(double latitude, double longitude, double[][] points, boolean expectedResult) {
List<GeoUtils.LatLong> latLongs = new ArrayList<>();
for (double[] point : points) {
latLongs.add(new GeoUtils.LatLong(point[0], point[1]));
}

GeoUtils.LatLong point = new GeoUtils.LatLong(latitude, longitude);
boolean result = GeoUtils.calculateIsPointInGPSPolygon(point, latLongs);
assertEquals(expectedResult, result);
}
}
19 changes: 19 additions & 0 deletions src/test/java/org/javarosa/xpath/test/XPathEvalTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,21 @@ public void substring_functions() {
testEval("ends-with('', '')", true);
}

@Test
public void geo_functions() {
testEval("geofence('')", new XPathUnhandledException());
testEval("geofence('', '')", new XPathUnhandledException());
testEval("geofence('0 0 0 0', '')", new XPathUnhandledException());
testEval("geofence('0.5 0.5 0 0', /data/geoshape)", buildInstance(), null, true); // inside
testEval("geofence('-1 0.5 0 0', /data/geoshape)", buildInstance(), null, false); // outside left
testEval("geofence('2 0.5 0 0', /data/geoshape)", buildInstance(), null, false); // outside right
testEval("geofence('0.5 2 0 0', /data/geoshape)", buildInstance(), null, false); // outside above
testEval("geofence('0.5 -1 0 0', /data/geoshape)", buildInstance(), null, false); // outside below
testEval("geofence('-1 0 0 0', /data/geoshape)", buildInstance(), null, false); // outside co-linear w/ bottom edge
testEval("geofence('-1 1 0 0', /data/geoshape)", buildInstance(), null, false); // outside co-linear w/ top edge
testEval("geofence('0 -1 0 0', /data/geoshape)", buildInstance(), null, false); // outside below vertex ("...They were carefully chosen to make the program work correctly when the point is vertically below a vertex.")
}

@Test
public void other_string_functions() {
testEval("normalize-space('')", "");
Expand Down Expand Up @@ -777,6 +792,10 @@ private static FormInstance buildInstance() {

data.addChild(new TreeElement("path", 4));

path = new TreeElement("geoshape", 0);
path.setValue(new StringData("0 0 0 0;0 1 0 0;1 1 0 0;1 0 0 0;0 0 0 0"));
data.addChild(path);

return new FormInstance(data);
}

Expand Down

0 comments on commit 6e72a2a

Please sign in to comment.