Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add new geofence XPath function and tests #771

Merged
merged 1 commit into from
Jun 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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