From f04d128c0b18f8ca4ee0ccda05caa2be1df1ddd9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9l=C3=A8ne=20Martin?= Date: Fri, 10 May 2024 10:18:01 -0700 Subject: [PATCH 1/3] Add multi-argument distance variant --- .../javarosa/xpath/expr/XPathFuncExpr.java | 38 ++++++++++--------- .../javarosa/xpath/expr/XPathFuncExprGeo.java | 28 ++++++++------ .../javarosa/core/util/GeoDistanceTest.java | 31 ++++++++++++++- 3 files changed, 67 insertions(+), 30 deletions(-) diff --git a/src/main/java/org/javarosa/xpath/expr/XPathFuncExpr.java b/src/main/java/org/javarosa/xpath/expr/XPathFuncExpr.java index 2a1e6f8fd..36e699ae7 100644 --- a/src/main/java/org/javarosa/xpath/expr/XPathFuncExpr.java +++ b/src/main/java/org/javarosa/xpath/expr/XPathFuncExpr.java @@ -16,6 +16,18 @@ package org.javarosa.xpath.expr; +import static java.lang.Double.NaN; + +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.math.BigDecimal; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.regex.Pattern; import org.javarosa.core.model.condition.EvaluationContext; import org.javarosa.core.model.condition.IFallbackFunctionHandler; import org.javarosa.core.model.condition.IFunctionHandler; @@ -43,19 +55,6 @@ import org.jetbrains.annotations.NotNull; import org.joda.time.DateTime; -import java.io.DataInputStream; -import java.io.DataOutputStream; -import java.io.IOException; -import java.math.BigDecimal; -import java.nio.charset.StandardCharsets; -import java.util.Arrays; -import java.util.Date; -import java.util.HashMap; -import java.util.List; -import java.util.regex.Pattern; - -import static java.lang.Double.NaN; - /** * Representation of an xpath function expression. *

@@ -476,9 +475,14 @@ public Object eval(DataInstance model, EvaluationContext evalContext) { List latLongs = new XPathFuncExprGeo().getGpsCoordinatesFromNodeset(name, argVals[0]); return GeoUtils.calculateAreaOfGPSPolygonOnEarthInSquareMeters(latLongs); } else if (name.equals("distance")) { - assertArgsCount(name, args, 1); - List latLongs = new XPathFuncExprGeo().getGpsCoordinatesFromNodeset(name, argVals[0]); - return GeoUtils.calculateDistance(latLongs); + if (args.length == 1) { + List latLongs = new XPathFuncExprGeo().getGpsCoordinatesFromNodeset(name, argVals[0]); + return GeoUtils.calculateDistance(latLongs); + } else if (args.length > 1) { + return GeoUtils.calculateDistance(new XPathFuncExprGeo().geopointsToLatLongs(name, argVals)); + } else { + throw new XPathUnhandledException("function 'distance' requires at least one parameter."); + } } else if (name.equals("digest") && (args.length == 2 || args.length == 3)) { return DigestAlgorithm.from(toString(argVals[1])).digest( toString(argVals[0]), @@ -494,7 +498,7 @@ public Object eval(DataInstance model, EvaluationContext evalContext) { if (args.length == 2) return XPathNodeset.shuffle((XPathNodeset) argVals[0], toNumeric(argVals[1]).longValue()); - throw new XPathUnhandledException("function \'randomize\' requires 1 or 2 arguments. " + args.length + " provided."); + throw new XPathUnhandledException("function 'randomize' requires 1 or 2 arguments. " + args.length + " provided."); } else if (name.equals("base64-decode")) { assertArgsCount(name, args, 1); return base64Decode(argVals[0]); diff --git a/src/main/java/org/javarosa/xpath/expr/XPathFuncExprGeo.java b/src/main/java/org/javarosa/xpath/expr/XPathFuncExprGeo.java index f7ab7e5a8..b9af82a70 100644 --- a/src/main/java/org/javarosa/xpath/expr/XPathFuncExprGeo.java +++ b/src/main/java/org/javarosa/xpath/expr/XPathFuncExprGeo.java @@ -1,5 +1,7 @@ package org.javarosa.xpath.expr; +import java.util.ArrayList; +import java.util.List; import org.javarosa.core.model.data.GeoPointData; import org.javarosa.core.model.data.GeoShapeData; import org.javarosa.core.model.data.UncastData; @@ -10,9 +12,6 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.util.ArrayList; -import java.util.List; - /** XPath function expression geographic logic */ class XPathFuncExprGeo { private static final Logger logger = LoggerFactory.getLogger(XPathFuncExprGeo.class.getSimpleName()); @@ -37,17 +36,22 @@ List getGpsCoordinatesFromNodeset(String name, Object argVal) throwMismatch(name); } } else if (repeatSize >= 2) { - // treat the input as a series of GeoPointData - - for (Object arg : argList) { - try { - GeoPointData geoPointData = new GeoPointData().cast(new UncastData(XPathFuncExpr.toString(arg))); - latLongs.add(new GeoUtils.LatLong(geoPointData.getPart(0), geoPointData.getPart(1))); - } catch (Exception e) { - throwMismatch(name); - } + latLongs.addAll(geopointsToLatLongs(name, argList)); + } + return latLongs; + } + + public List geopointsToLatLongs(String callingFunction, Object[] args) { + List latLongs = new ArrayList<>(); + for (Object arg : args) { + try { + GeoPointData geoPointData = new GeoPointData().cast(new UncastData(XPathFuncExpr.toString(arg))); + latLongs.add(new GeoUtils.LatLong(geoPointData.getPart(0), geoPointData.getPart(1))); + } catch (Exception e) { + throwMismatch(callingFunction); } } + return latLongs; } diff --git a/src/test/java/org/javarosa/core/util/GeoDistanceTest.java b/src/test/java/org/javarosa/core/util/GeoDistanceTest.java index 28ef33e6d..21530facc 100644 --- a/src/test/java/org/javarosa/core/util/GeoDistanceTest.java +++ b/src/test/java/org/javarosa/core/util/GeoDistanceTest.java @@ -17,6 +17,7 @@ package org.javarosa.core.util; import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.closeTo; import static org.javarosa.core.util.BindBuilderXFormsElement.bind; import static org.javarosa.core.util.GeoUtils.EARTH_EQUATORIAL_CIRCUMFERENCE_METERS; @@ -29,7 +30,6 @@ import static org.javarosa.core.util.XFormsElement.repeat; import static org.javarosa.core.util.XFormsElement.t; import static org.javarosa.core.util.XFormsElement.title; -import static org.junit.Assert.assertThat; import java.io.IOException; import org.hamcrest.number.IsCloseTo; @@ -149,6 +149,35 @@ public void distance_isComputedForString() throws IOException, XFormParser.Parse IsCloseTo.closeTo(1801, 0.5)); } + @Test + public void distance_isComputedForMultipleArguments() throws IOException, XFormParser.ParseException { + Scenario scenario = Scenario.init("string distance", html( + head( + title("Multi parameter distance"), + model( + mainInstance(t("data id=\"string-distance\"", + t("point1", "38.253094215699576 21.756382658677467 0 0"), + t("point3", "38.25007793942195 21.763892843919166 0 0"), + t("point4", "38.25290886154963 21.763935759263404 0 0"), + t("point5", "38.25146813817506 21.758421137528785 0 0"), + t("distance") + )), + bind("/data/point1").type("geopoint"), + bind("/data/point3").type("geopoint"), + bind("/data/point4").type("geopoint"), + bind("/data/point5").type("geopoint"), + bind("/data/distance").type("decimal").calculate("distance(/data/point1, '38.25021274773806 21.756382658677467 0 0', /data/point3, /data/point4, /data/point5)") + )), + body( + input("/data/point1") + ) + )); + + // http://www.mapdevelopers.com/area_finder.php?&points=%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%5D + assertThat(Double.parseDouble(scenario.answerOf("/data/distance").getDisplayText()), + IsCloseTo.closeTo(1801, 0.5)); + } + @Test public void distance_whenTraceHasFewerThanTwoPoints_isZero() throws Exception { Scenario scenario = Scenario.init("geotrace distance", html( From fb3557bd696368103e1a13304187977158d6b585 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9l=C3=A8ne=20Martin?= Date: Mon, 13 May 2024 20:51:28 -0700 Subject: [PATCH 2/3] Add support for single string argument --- .../javarosa/xpath/expr/XPathFuncExpr.java | 11 +++- .../javarosa/xpath/expr/XPathFuncExprGeo.java | 2 +- .../javarosa/core/util/GeoDistanceTest.java | 59 +++++++++++++++++++ 3 files changed, 69 insertions(+), 3 deletions(-) diff --git a/src/main/java/org/javarosa/xpath/expr/XPathFuncExpr.java b/src/main/java/org/javarosa/xpath/expr/XPathFuncExpr.java index 36e699ae7..0fd08edc0 100644 --- a/src/main/java/org/javarosa/xpath/expr/XPathFuncExpr.java +++ b/src/main/java/org/javarosa/xpath/expr/XPathFuncExpr.java @@ -476,8 +476,15 @@ public Object eval(DataInstance model, EvaluationContext evalContext) { return GeoUtils.calculateAreaOfGPSPolygonOnEarthInSquareMeters(latLongs); } else if (name.equals("distance")) { if (args.length == 1) { - List latLongs = new XPathFuncExprGeo().getGpsCoordinatesFromNodeset(name, argVals[0]); - return GeoUtils.calculateDistance(latLongs); + if (argVals[0] instanceof XPathNodeset) { + List latLongs = new XPathFuncExprGeo().getGpsCoordinatesFromNodeset(name, argVals[0]); + return GeoUtils.calculateDistance(latLongs); + } else if (argVals[0] instanceof String) { + List latLongs = new XPathFuncExprGeo().geopointsToLatLongs(name, ((String) argVals[0]).split(";")); + return GeoUtils.calculateDistance(latLongs); + } else { + throw new XPathUnhandledException("function 'distance' requires a field or text as the parameter."); + } } else if (args.length > 1) { return GeoUtils.calculateDistance(new XPathFuncExprGeo().geopointsToLatLongs(name, argVals)); } else { diff --git a/src/main/java/org/javarosa/xpath/expr/XPathFuncExprGeo.java b/src/main/java/org/javarosa/xpath/expr/XPathFuncExprGeo.java index b9af82a70..5d1e76edd 100644 --- a/src/main/java/org/javarosa/xpath/expr/XPathFuncExprGeo.java +++ b/src/main/java/org/javarosa/xpath/expr/XPathFuncExprGeo.java @@ -18,7 +18,7 @@ class XPathFuncExprGeo { List getGpsCoordinatesFromNodeset(String name, Object argVal) { if (!(argVal instanceof XPathNodeset)) { - throw new XPathUnhandledException("function \'" + name + "\' requires a field as the parameter."); + throw new XPathUnhandledException("function '" + name + "' requires a field as the parameter."); } Object[] argList = ((XPathNodeset) argVal).toArgList(); int repeatSize = argList.length; diff --git a/src/test/java/org/javarosa/core/util/GeoDistanceTest.java b/src/test/java/org/javarosa/core/util/GeoDistanceTest.java index 21530facc..574eb9d9b 100644 --- a/src/test/java/org/javarosa/core/util/GeoDistanceTest.java +++ b/src/test/java/org/javarosa/core/util/GeoDistanceTest.java @@ -16,6 +16,8 @@ package org.javarosa.core.util; +import static org.hamcrest.CoreMatchers.containsString; +import static org.hamcrest.CoreMatchers.instanceOf; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.closeTo; @@ -30,11 +32,13 @@ import static org.javarosa.core.util.XFormsElement.repeat; import static org.javarosa.core.util.XFormsElement.t; import static org.javarosa.core.util.XFormsElement.title; +import static org.junit.Assert.fail; import java.io.IOException; import org.hamcrest.number.IsCloseTo; import org.javarosa.core.test.Scenario; import org.javarosa.xform.parse.XFormParser; +import org.javarosa.xpath.XPathTypeMismatchException; import org.junit.Test; public class GeoDistanceTest { @@ -149,6 +153,61 @@ public void distance_isComputedForString() throws IOException, XFormParser.Parse IsCloseTo.closeTo(1801, 0.5)); } + @Test + public void distance_isComputedForInlineString() throws IOException, XFormParser.ParseException { + Scenario scenario = Scenario.init("string distance", html( + head( + title("String distance"), + model( + mainInstance(t("data id=\"string-distance\"", + t("point1", "38.253094215699576 21.756382658677467 0 0"), + t("point2", "38.25021274773806 21.756382658677467 0 0"), + t("point3", "38.25007793942195 21.763892843919166 0 0"), + t("point4", "38.25290886154963 21.763935759263404 0 0"), + t("point5", "38.25146813817506 21.758421137528785 0 0"), + t("distance") + )), + bind("/data/point1").type("geopoint"), + bind("/data/point2").type("geopoint"), + bind("/data/point3").type("geopoint"), + bind("/data/point4").type("geopoint"), + bind("/data/point5").type("geopoint"), + bind("/data/distance").type("decimal").calculate("distance(concat(/data/point1, ';', /data/point2, ';', /data/point3, ';', /data/point4, ';', /data/point5))") + )), + body( + input("/data/point1") + ) + )); + + // http://www.mapdevelopers.com/area_finder.php?&points=%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%5D + assertThat(Double.parseDouble(scenario.answerOf("/data/distance").getDisplayText()), + IsCloseTo.closeTo(1801, 0.5)); + } + + @Test + public void distance_throwsForNonPoint() throws IOException, XFormParser.ParseException { + try { + Scenario.init("string distance", html( + head( + title("String distance"), + model( + mainInstance(t("data id=\"string-distance\"", + t("distance") + )), + bind("/data/distance").type("decimal").calculate("distance('foo')") + )), + body( + input("distance") + ) + )); + + fail("Exception expected"); + } catch (RuntimeException e) { + assertThat(e.getCause(), instanceOf(XPathTypeMismatchException.class)); + assertThat(e.getMessage(), containsString("The function 'distance' received a value that does not represent GPS coordinates")); + } + } + @Test public void distance_isComputedForMultipleArguments() throws IOException, XFormParser.ParseException { Scenario scenario = Scenario.init("string distance", html( From 4d71a59b326f23f660956b2378de585e1bdf02d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9l=C3=A8ne=20Martin?= Date: Tue, 21 May 2024 08:32:56 -0700 Subject: [PATCH 3/3] Separate out test for mixed argument types --- .../javarosa/core/util/GeoDistanceTest.java | 33 +++++++++++++++++-- 1 file changed, 31 insertions(+), 2 deletions(-) diff --git a/src/test/java/org/javarosa/core/util/GeoDistanceTest.java b/src/test/java/org/javarosa/core/util/GeoDistanceTest.java index 574eb9d9b..b97eb1a4d 100644 --- a/src/test/java/org/javarosa/core/util/GeoDistanceTest.java +++ b/src/test/java/org/javarosa/core/util/GeoDistanceTest.java @@ -209,23 +209,25 @@ public void distance_throwsForNonPoint() throws IOException, XFormParser.ParseEx } @Test - public void distance_isComputedForMultipleArguments() throws IOException, XFormParser.ParseException { + public void distance_isComputedForMultiplePathArguments() throws IOException, XFormParser.ParseException { Scenario scenario = Scenario.init("string distance", html( head( title("Multi parameter distance"), model( mainInstance(t("data id=\"string-distance\"", t("point1", "38.253094215699576 21.756382658677467 0 0"), + t("point2", "38.25021274773806 21.756382658677467 0 0"), t("point3", "38.25007793942195 21.763892843919166 0 0"), t("point4", "38.25290886154963 21.763935759263404 0 0"), t("point5", "38.25146813817506 21.758421137528785 0 0"), t("distance") )), bind("/data/point1").type("geopoint"), + bind("/data/point2").type("geopoint"), bind("/data/point3").type("geopoint"), bind("/data/point4").type("geopoint"), bind("/data/point5").type("geopoint"), - bind("/data/distance").type("decimal").calculate("distance(/data/point1, '38.25021274773806 21.756382658677467 0 0', /data/point3, /data/point4, /data/point5)") + bind("/data/distance").type("decimal").calculate("distance(/data/point1, /data/point2, /data/point3, /data/point4, /data/point5)") )), body( input("/data/point1") @@ -237,6 +239,33 @@ public void distance_isComputedForMultipleArguments() throws IOException, XFormP IsCloseTo.closeTo(1801, 0.5)); } + @Test + public void distance_isComputedForMixedPathAndStringArguments() throws IOException, XFormParser.ParseException { + Scenario scenario = Scenario.init("string distance", html( + head( + title("Multi parameter distance"), + model( + mainInstance(t("data id=\"string-distance\"", + t("point2", "38.25021274773806 21.756382658677467 0 0"), + t("point3", "38.25007793942195 21.763892843919166 0 0"), + t("point5", "38.25146813817506 21.758421137528785 0 0"), + t("distance") + )), + bind("/data/point2").type("geopoint"), + bind("/data/point3").type("geopoint"), + bind("/data/point5").type("geopoint"), + bind("/data/distance").type("decimal").calculate("distance('38.253094215699576 21.756382658677467 0 0', /data/point2, /data/point3, '38.25290886154963 21.763935759263404 0 0', /data/point5)") + )), + body( + input("/data/point2") + ) + )); + + // http://www.mapdevelopers.com/area_finder.php?&points=%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%5D + assertThat(Double.parseDouble(scenario.answerOf("/data/distance").getDisplayText()), + IsCloseTo.closeTo(1801, 0.5)); + } + @Test public void distance_whenTraceHasFewerThanTwoPoints_isZero() throws Exception { Scenario scenario = Scenario.init("geotrace distance", html(