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 1 commit
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
121 changes: 121 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,121 @@
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.
*/
public static boolean isPointInsidePolygon(List<Double> polygonPoints, List<Double> testPoint) {
int intersectCount = 0;
int vertexCount = polygonPoints.size() / 2;

double testLat = testPoint.get(0);
double testLng = testPoint.get(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);
}

/**
* 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;
}

/**
* Calculates the distance from a point to the closest boundary of 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 The distance from the test point to the closest edge of the polygon.
*/
public static double distanceToClosestBoundary(List<Double> polygonPoints, double[] testPoint) {
double minDistance = Double.MAX_VALUE;

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);

double distance = pointToSegmentDistance(testLat, testLng, lat1, lng1, lat2, lng2);
minDistance = Math.min(minDistance, distance);
}

return minDistance;
}

/**
* Calculates the shortest distance from a point to a line segment.
*/
private static double pointToSegmentDistance(double px, double py, double x1, double y1, double x2, double y2) {
double dx = x2 - x1;
double dy = y2 - y1;

if (dx == 0 && dy == 0) {
// The segment is a point
return Math.sqrt(Math.pow(px - x1, 2) + Math.pow(py - y1, 2));
}

// Calculate the projection of the point onto the line
double t = ((px - x1) * dx + (py - y1) * dy) / (dx * dx + dy * dy);

if (t < 0) {
// Closest to the first endpoint
return Math.sqrt(Math.pow(px - x1, 2) + Math.pow(py - y1, 2));
} else if (t > 1) {
// Closest to the second endpoint
return Math.sqrt(Math.pow(px - x2, 2) + Math.pow(py - y2, 2));
} else {
// Closest to a point on the segment
double projX = x1 + t * dx;
double projY = y1 + t * dy;
return Math.sqrt(Math.pow(px - projX, 2) + Math.pow(py - projY, 2));
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Add unit tests for PolygonUtils methods

Implementing unit tests for isPointInsidePolygon and distanceToClosestBoundary methods will help ensure their correctness and detect any regressions in future changes.

Would you like assistance in creating unit tests for these methods?

}
}
1 change: 1 addition & 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,7 @@ public class FunctionUtils {
funcList.put(XPathEncryptStringFunc.NAME, XPathEncryptStringFunc.class);
funcList.put(XPathDecryptStringFunc.NAME, XPathDecryptStringFunc.class);
funcList.put(XPathJsonPropertyFunc.NAME, XPathJsonPropertyFunc.class);
funcList.put(XPathBoundaryDistanceFunc.NAME, XPathBoundaryDistanceFunc.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,56 @@
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 XPathBoundaryDistanceFunc extends XPathFuncExpr{
Copy link
Contributor

Choose a reason for hiding this comment

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

can we add a javadoc here explaining what this function does.

public static final String NAME = "boundaryDistance";
private static final int EXPECTED_ARG_COUNT = 2;

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

public XPathBoundaryDistanceFunc(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 Double boundaryDistance(Object from, Object to) {
String unpackedFrom = (String)FunctionUtils.unpack(from);
String unpackedTo = (String)FunctionUtils.unpack(to);
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Ensure safe casting from FunctionUtils.unpack(from/to)

Casting the result of FunctionUtils.unpack() directly to String may lead to a ClassCastException if the unpacked objects are not strings. It's advisable to check the instance type before casting to prevent potential runtime exceptions.

Apply this diff to safely handle the casting:

- String unpackedFrom = (String)FunctionUtils.unpack(from);
- String unpackedTo = (String)FunctionUtils.unpack(to);
+ Object unpackedFromObj = FunctionUtils.unpack(from);
+ Object unpackedToObj = FunctionUtils.unpack(to);
+ if (!(unpackedFromObj instanceof String) || !(unpackedToObj instanceof String)) {
+     throw new XPathTypeMismatchException("boundaryDistance() function requires string arguments.");
+ }
+ String unpackedFrom = (String) unpackedFromObj;
+ String unpackedTo = (String) unpackedToObj;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
String unpackedFrom = (String)FunctionUtils.unpack(from);
String unpackedTo = (String)FunctionUtils.unpack(to);
Object unpackedFromObj = FunctionUtils.unpack(from);
Object unpackedToObj = FunctionUtils.unpack(to);
if (!(unpackedFromObj instanceof String) || !(unpackedToObj instanceof String)) {
throw new XPathTypeMismatchException("boundaryDistance() function requires string arguments.");
}
String unpackedFrom = (String) unpackedFromObj;
String unpackedTo = (String) unpackedToObj;

if (unpackedFrom == null || "".equals(unpackedFrom) || unpackedTo == null || "".equals(unpackedTo)) {
return Double.valueOf(-1.0);
}
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));
double distance=PolygonUtils.distanceToClosestBoundary(polygonList,new double[]{castedTo.getLatitude(), castedTo.getLongitude()});

return distance;
} catch (NumberFormatException e) {
throw new XPathTypeMismatchException("distance() function requires arguments containing " +
"numeric values only, but received arguments: " + unpackedFrom + " and " + unpackedTo);
}
}
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Add unit tests for XPathBoundaryDistanceFunc

To ensure the correctness and robustness of the boundaryDistance function, it's crucial to add unit tests covering various scenarios, including valid inputs, invalid inputs, and edge cases.

Would you like assistance in generating unit tests for this function or opening a GitHub issue to track this task?

Loading