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

PM-Xpath implementation to calculate the distance from the boundary #1455

Open
wants to merge 7 commits into
base: master
Choose a base branch
from
Open
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
124 changes: 124 additions & 0 deletions src/main/java/org/javarosa/core/model/utils/PolygonUtils.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
package org.javarosa.core.model.utils;
import java.util.List;

public class PolygonUtils {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

curious if this implementation was taken from somewhere , if so it might be good to reference it in a comment just in case we need to track any future updates to this code.


/**
* Determines if a point is inside a polygon.
*
* @param polygonPoints A list of doubles representing the polygon vertices
* (latitude and longitude pairs).
* @param testPoint A list of doubles representing the latitude and longitude of the test point.
* @return true if the point is inside the polygon, false otherwise.
* This code is written with the help of chatgpt
*/
public static boolean isPointInsidePolygon(List<Double> polygonPoints, double[] testPoint) {
int intersectCount = 0;
int vertexCount = polygonPoints.size() / 2;

double testLat = testPoint[0];
double testLng = testPoint[1];

for (int i = 0; i < vertexCount; i++) {
double lat1 = polygonPoints.get(2 * i);
double lng1 = polygonPoints.get(2 * i + 1);
double lat2 = polygonPoints.get((2 * ((i + 1) % vertexCount)));
double lng2 = polygonPoints.get((2 * ((i + 1) % vertexCount)) + 1);

if (rayIntersectsEdge(testLat, testLng, lat1, lng1, lat2, lng2)) {
intersectCount++;
}
}

return (intersectCount % 2 == 1);
}

/**
* Finds the minimum distance from a point to the polygon and the closest coordinate on the polygon.
*
* @param polygonPoints A list of doubles representing the polygon vertices
* (latitude and longitude pairs).
* @param testPoint A list of doubles representing the latitude and longitude of the test point.
* @return A result containing the minimum distance and the closest point on the polygon.
*/
public static String getClosestPoint(List<Double> polygonPoints, double[] testPoint) {
int numVertices = polygonPoints.size() / 2;
double[] closestPoint = null;
double minDistance = Double.MAX_VALUE;

for (int i = 0; i < numVertices; i++) {
// Get the start and end points of the current edge
double startX = polygonPoints.get(2 * i);
double startY = polygonPoints.get(2 * i + 1);
double endX = polygonPoints.get(2 * ((i + 1) % numVertices));
double endY = polygonPoints.get(2 * ((i + 1) % numVertices) + 1);

// Find the closest point on this edge
double[] candidatePoint = getClosestPointOnSegment(
startX, startY, endX, endY, testPoint[0], testPoint[1]);
double distance = distanceBetween(candidatePoint, testPoint);

// Update the closest point if necessary
if (distance < minDistance) {
minDistance = distance;
closestPoint = candidatePoint;
}
}

// Return the closest point as a space-separated string
return closestPoint[0] + " " + closestPoint[1];
}

private static double[] getClosestPointOnSegment(double startX, double startY, double endX, double endY, double px, double py) {
double dx = endX - startX;
double dy = endY - startY;

if (dx == 0 && dy == 0) {
// The segment is a single point
return new double[]{startX, startY};
}

// Calculate the projection factor t
double t = ((px - startX) * dx + (py - startY) * dy) / (dx * dx + dy * dy);

// Clamp t to the range [0, 1] to stay on the segment
t = Math.max(0, Math.min(1, t));

// Compute the closest point
return new double[]{startX + t * dx, startY + t * dy};
}

private static double distanceBetween(double[] a, double[] b) {
return Math.sqrt((a[0] - b[0]) * (a[0] - b[0]) + (a[1] - b[1]) * (a[1] - b[1]));
}

/**
* Checks if a ray starting from the test point intersects the edge defined by two vertices.
*/
private static boolean rayIntersectsEdge(double testLat, double testLng, double lat1, double lng1, double lat2, double lng2) {
if (lat1 > lat2) {
double tempLat = lat1, tempLng = lng1;
lat1 = lat2;
lng1 = lng2;
lat2 = tempLat;
lng2 = tempLng;
}

if (testLat < lat1 || testLat > lat2) {
return false;
}

if (testLng > Math.max(lng1, lng2)) {
return false;
}

if (testLng < Math.min(lng1, lng2)) {
return true;
}

double slope = (lng2 - lng1) / (lat2 - lat1);
double intersectLng = lng1 + (testLat - lat1) * slope;

return testLng < intersectLng;
}
}
2 changes: 2 additions & 0 deletions src/main/java/org/javarosa/xpath/expr/FunctionUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,8 @@ public class FunctionUtils {
funcList.put(XPathEncryptStringFunc.NAME, XPathEncryptStringFunc.class);
funcList.put(XPathDecryptStringFunc.NAME, XPathDecryptStringFunc.class);
funcList.put(XPathJsonPropertyFunc.NAME, XPathJsonPropertyFunc.class);
funcList.put(XPathClosestPolygonPointFunc.NAME, XPathClosestPolygonPointFunc.class);
funcList.put(XPathPointInsidePolygon.NAME, XPathPointInsidePolygon.class);
}

private static final CacheTable<String, Double> mDoubleParseCache = new CacheTable<>();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package org.javarosa.xpath.expr;

import org.javarosa.core.model.condition.EvaluationContext;
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.utils.PolygonUtils;
import org.javarosa.xpath.XPathTypeMismatchException;
import org.javarosa.xpath.parser.XPathSyntaxException;

import java.util.ArrayList;
import java.util.List;

public class XPathClosestPolygonPointFunc extends XPathFuncExpr{
public static final String NAME = "polygon-point";
private static final int EXPECTED_ARG_COUNT = 2;

public XPathClosestPolygonPointFunc() {
name = NAME;
expectedArgCount = EXPECTED_ARG_COUNT;
}

public XPathClosestPolygonPointFunc(XPathExpression[] args) throws XPathSyntaxException {
super(NAME, args, EXPECTED_ARG_COUNT, true);
}

/**
* Returns the point on polygon closest to the geopoint, in "Lat Lng", given objects to unpack.
* Ignores altitude and accuracy.
* Note that the arguments can be strings.
* Returns "" if one of the arguments is null or the empty string.
*/
@Override
protected Object evalBody(DataInstance model, EvaluationContext evalContext, Object[] evaluatedArgs) {
return closestPoint(evaluatedArgs[0], evaluatedArgs[1]);
}

public static String closestPoint(Object from, Object to) {
String unpackedFrom = (String)FunctionUtils.unpack(from);
String unpackedTo = (String)FunctionUtils.unpack(to);
if (unpackedFrom == null || "".equals(unpackedFrom) || unpackedTo == null || "".equals(unpackedTo)) {
return "";
}
try {
String[] coordinates=unpackedFrom.split(" ");
List<Double> polygonList = new ArrayList<Double>();

for (String coordinate : coordinates) {
polygonList.add(Double.parseDouble(coordinate));
}
// Casting and uncasting seems strange but is consistent with the codebase
GeoPointData castedTo = new GeoPointData().cast(new UncastData(unpackedTo));
String closestPointResult=PolygonUtils.getClosestPoint(polygonList,new double[]{castedTo.getLatitude(), castedTo.getLongitude()});

return closestPointResult;
} catch (NumberFormatException e) {
throw new XPathTypeMismatchException("polygon-point() function requires arguments containing " +
"numeric values only, but received arguments: " + unpackedFrom + " and " + unpackedTo);
}
}
}
60 changes: 60 additions & 0 deletions src/main/java/org/javarosa/xpath/expr/XPathPointInsidePolygon.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package org.javarosa.xpath.expr;

import org.javarosa.core.model.condition.EvaluationContext;
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.utils.PolygonUtils;
import org.javarosa.xpath.XPathTypeMismatchException;
import org.javarosa.xpath.parser.XPathSyntaxException;
import java.util.ArrayList;
import java.util.List;

public class XPathPointInsidePolygon extends XPathFuncExpr{
public static final String NAME = "inside-polygon";
private static final int EXPECTED_ARG_COUNT = 2;
/**
* Returns true if the geopoint is inside the polygon, in meters, given objects to unpack.
* Ignores altitude and accuracy.
* Note that the arguments can be strings.
* Returns false if one of the arguments is null or the empty string.
*/
public XPathPointInsidePolygon() {
name = NAME;
expectedArgCount = EXPECTED_ARG_COUNT;
}

public XPathPointInsidePolygon(XPathExpression[] args) throws XPathSyntaxException {
super(NAME, args, EXPECTED_ARG_COUNT, true);
}


@Override
protected Object evalBody(DataInstance model, EvaluationContext evalContext, Object[] evaluatedArgs) {
return boundaryDistance(evaluatedArgs[0], evaluatedArgs[1]);
}

public static boolean boundaryDistance(Object from, Object to) {
String unpackedFrom = (String)FunctionUtils.unpack(from);
String unpackedTo = (String)FunctionUtils.unpack(to);
if (unpackedFrom == null || "".equals(unpackedFrom) || unpackedTo == null || "".equals(unpackedTo)) {
return false;
}
try {
String[] coordinates=unpackedFrom.split(" ");
List<Double> polygonList = new ArrayList<Double>();

for (String coordinate : coordinates) {
polygonList.add(Double.parseDouble(coordinate));
}
// Casting and uncasting seems strange but is consistent with the codebase
GeoPointData castedTo = new GeoPointData().cast(new UncastData(unpackedTo));
boolean isInside= PolygonUtils.isPointInsidePolygon(polygonList,new double[]{castedTo.getLatitude(), castedTo.getLongitude()});

return isInside;
} catch (NumberFormatException e) {
throw new XPathTypeMismatchException("point-in-boundary() function requires arguments containing " +
"numeric values only, but received arguments: " + unpackedFrom + " and " + unpackedTo);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import org.javarosa.xpath.expr.XPathAtanTwoFunc;
import org.javarosa.xpath.expr.XPathBooleanFromStringFunc;
import org.javarosa.xpath.expr.XPathBooleanFunc;
import org.javarosa.xpath.expr.XPathClosestPolygonPointFunc;
import org.javarosa.xpath.expr.XPathCeilingFunc;
import org.javarosa.xpath.expr.XPathChecklistFunc;
import org.javarosa.xpath.expr.XPathChecksumFunc;
Expand Down Expand Up @@ -48,6 +49,7 @@
import org.javarosa.xpath.expr.XPathNowFunc;
import org.javarosa.xpath.expr.XPathNumberFunc;
import org.javarosa.xpath.expr.XPathPiFunc;
import org.javarosa.xpath.expr.XPathPointInsidePolygon;
import org.javarosa.xpath.expr.XPathPositionFunc;
import org.javarosa.xpath.expr.XPathPowFunc;
import org.javarosa.xpath.expr.XPathQName;
Expand Down Expand Up @@ -257,6 +259,10 @@ private static XPathFuncExpr buildFuncExpr(String name, XPathExpression[] args)
return new XPathDecryptStringFunc(args);
case "json-property":
return new XPathJsonPropertyFunc(args);
case "polygon-point":
return new XPathClosestPolygonPointFunc(args);
case "inside-polygon":
return new XPathPointInsidePolygon(args);
default:
return new XPathCustomRuntimeFunc(name, args);
}
Expand Down
10 changes: 8 additions & 2 deletions src/test/java/org/javarosa/xpath/test/XPathEvalTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

import static org.junit.Assert.fail;

import org.commcare.util.EncryptionUtils;
import org.javarosa.core.model.condition.EvaluationContext;
import org.javarosa.core.model.condition.IFunctionHandler;
import org.javarosa.core.model.data.IAnswerData;
Expand Down Expand Up @@ -609,7 +608,14 @@ public void doTests() {
testEval("$var_string_five", null, varContext, "five");
testEval("$var_int_five", null, varContext, Double.valueOf(5.0));
testEval("$var_double_five", null, varContext, Double.valueOf(5.0));

//Polygon point
testEval("polygon-point('78.041309 27.174957 78.042574 27.174884 78.042661 27.175493 78.041383 27.175569','78.041 27.176')", null, null, "78.041383 27.175569"); // Outside, near bottom-left vertex

//inside polygon
testEval("inside-polygon('78.0186987 27.2043773 78.0187201 27.203509 78.0202758 27.2035281 78.0203027 27.2044155','78.0195 27.204')", null, null, true); // Inside the polygon
testEval("inside-polygon('78.0186987 27.2043773 78.0187201 27.203509 78.0202758 27.2035281 78.0203027 27.2044155','78.0205 27.2035')", null, null, false); // Outside the polygon, near bottom-right
testEval("inside-polygon('78.0186987 27.2043773 78.0187201 27.203509 78.0202758 27.2035281 78.0203027 27.2044155','78.018 27.204')", null, null, false); // Outside the polygon, far left
testEval("inside-polygon('78.0186987 27.2043773 78.0187201 27.203509 78.0202758 27.2035281 78.0203027 27.2044155','78.0187201 27.203509')", null, null, true); // On the polygon vertex
//Attribute XPath References
//testEval("/@blah", null, null, new XPathUnsupportedException());
//TODO: Need to test with model, probably in a different file
Expand Down
Loading