-
Notifications
You must be signed in to change notification settings - Fork 14
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
pm-dimagi
wants to merge
7
commits into
master
Choose a base branch
from
pm_cluster
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
7 commits
Select commit
Hold shift + click to select a range
ac492df
-xpath implementation to calculate the distance from the boundary of …
pm-dimagi 43b1b61
-xpath implementation to calculate that the point is inside the polyg…
pm-dimagi f718a08
-change name of the function file
pm-dimagi 74e90fb
-pr review requested
pm-dimagi 7db32a6
-value to two decimal place of the distance
pm-dimagi c20fe25
fix declared function name
ctsims 4041f13
- bug fix and test cases for polygon test
pm-dimagi File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
124 changes: 124 additions & 0 deletions
124
src/main/java/org/javarosa/core/model/utils/PolygonUtils.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 { | ||
|
||
/** | ||
* 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; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
61 changes: 61 additions & 0 deletions
61
src/main/java/org/javarosa/xpath/expr/XPathClosestPolygonPointFunc.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
60
src/main/java/org/javarosa/xpath/expr/XPathPointInsidePolygon.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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.