convert degrees to kilometers for near calculation

This commit is contained in:
Ken Stevens 2020-01-22 16:55:19 -05:00
parent f2e8037c90
commit e6f5af01ed
6 changed files with 97 additions and 91 deletions

View File

@ -20,15 +20,7 @@ package ca.uhn.fhir.jpa.dao;
* #L%
*/
import ca.uhn.fhir.context.BaseRuntimeChildDefinition;
import ca.uhn.fhir.context.BaseRuntimeDeclaredChildDefinition;
import ca.uhn.fhir.context.BaseRuntimeElementDefinition;
import ca.uhn.fhir.context.ConfigurationException;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.context.RuntimeChildChoiceDefinition;
import ca.uhn.fhir.context.RuntimeChildResourceDefinition;
import ca.uhn.fhir.context.RuntimeResourceDefinition;
import ca.uhn.fhir.context.RuntimeSearchParam;
import ca.uhn.fhir.context.*;
import ca.uhn.fhir.interceptor.api.HookParams;
import ca.uhn.fhir.interceptor.api.IInterceptorBroadcaster;
import ca.uhn.fhir.interceptor.api.Pointcut;
@ -51,18 +43,8 @@ import ca.uhn.fhir.jpa.searchparam.registry.ISearchParamRegistry;
import ca.uhn.fhir.jpa.searchparam.util.SourceParam;
import ca.uhn.fhir.jpa.term.VersionIndependentConcept;
import ca.uhn.fhir.jpa.term.api.ITermReadSvc;
import ca.uhn.fhir.jpa.util.BaseIterator;
import ca.uhn.fhir.jpa.util.CurrentThreadCaptureQueriesListener;
import ca.uhn.fhir.jpa.util.JpaInterceptorBroadcaster;
import ca.uhn.fhir.jpa.util.ScrollableResultsIterator;
import ca.uhn.fhir.jpa.util.SqlQueryList;
import ca.uhn.fhir.model.api.IPrimitiveDatatype;
import ca.uhn.fhir.model.api.IQueryParameterAnd;
import ca.uhn.fhir.model.api.IQueryParameterOr;
import ca.uhn.fhir.model.api.IQueryParameterType;
import ca.uhn.fhir.model.api.IResource;
import ca.uhn.fhir.model.api.Include;
import ca.uhn.fhir.model.api.ResourceMetadataKeyEnum;
import ca.uhn.fhir.jpa.util.*;
import ca.uhn.fhir.model.api.*;
import ca.uhn.fhir.model.base.composite.BaseCodingDt;
import ca.uhn.fhir.model.base.composite.BaseIdentifierDt;
import ca.uhn.fhir.model.base.composite.BaseQuantityDt;
@ -71,11 +53,7 @@ import ca.uhn.fhir.model.primitive.IdDt;
import ca.uhn.fhir.model.primitive.InstantDt;
import ca.uhn.fhir.model.valueset.BundleEntrySearchModeEnum;
import ca.uhn.fhir.parser.DataFormatException;
import ca.uhn.fhir.rest.api.Constants;
import ca.uhn.fhir.rest.api.QualifiedParamList;
import ca.uhn.fhir.rest.api.RestSearchParameterTypeEnum;
import ca.uhn.fhir.rest.api.SortOrderEnum;
import ca.uhn.fhir.rest.api.SortSpec;
import ca.uhn.fhir.rest.api.*;
import ca.uhn.fhir.rest.api.server.IPreResourceAccessDetails;
import ca.uhn.fhir.rest.api.server.RequestDetails;
import ca.uhn.fhir.rest.param.*;
@ -99,6 +77,7 @@ import org.hibernate.ScrollableResults;
import org.hibernate.query.Query;
import org.hibernate.query.criteria.internal.CriteriaBuilderImpl;
import org.hibernate.query.criteria.internal.predicate.BooleanStaticAssertionPredicate;
import org.hibernate.search.spatial.impl.Point;
import org.hl7.fhir.instance.model.api.IAnyResource;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IIdType;
@ -121,11 +100,7 @@ import java.util.Map.Entry;
import java.util.stream.Collectors;
import static org.apache.commons.lang3.ObjectUtils.defaultIfNull;
import static org.apache.commons.lang3.StringUtils.defaultIfBlank;
import static org.apache.commons.lang3.StringUtils.defaultString;
import static org.apache.commons.lang3.StringUtils.isBlank;
import static org.apache.commons.lang3.StringUtils.isNotBlank;
import static org.apache.commons.lang3.StringUtils.trim;
import static org.apache.commons.lang3.StringUtils.*;
/**
* The SearchBuilder is responsible for actually forming the SQL query that handles
@ -2075,7 +2050,7 @@ public class SearchBuilder implements ISearchBuilder {
From<?, ResourceIndexedSearchParamCoords> theFrom) {
String latitudeValue;
String longitudeValue;
Double distance = 0.0;
Double distanceKm = 0.0;
if (theParam instanceof TokenParam) { // DSTU3
TokenParam param = (TokenParam) theParam;
@ -2091,7 +2066,7 @@ public class SearchBuilder implements ISearchBuilder {
}
QuantityParam distanceParam = myParams.getNearDistanceParam();
if (distanceParam != null) {
distance = distanceParam.getValue().doubleValue();
distanceKm = distanceParam.getValue().doubleValue();
}
} else if (theParam instanceof SpecialParam) { // R4
SpecialParam param = (SpecialParam) theParam;
@ -2108,7 +2083,7 @@ public class SearchBuilder implements ISearchBuilder {
if (parts.length >= 3) {
String distanceString = parts[2];
if (!isBlank(distanceString)) {
distance = Double.valueOf(distanceString);
distanceKm = Double.valueOf(distanceString);
}
}
} else {
@ -2117,22 +2092,27 @@ public class SearchBuilder implements ISearchBuilder {
Predicate latitudePredicate;
Predicate longitudePredicate;
if (distance == 0.0) {
if (distanceKm == 0.0) {
latitudePredicate = theBuilder.equal(theFrom.get("myLatitude"), latitudeValue);
longitudePredicate = theBuilder.equal(theFrom.get("myLongitude"), longitudeValue);
} else if (distance < 0.0) {
throw new IllegalArgumentException("Invalid " + Location.SP_NEAR_DISTANCE + " parameter '" + distance + "' must be >= 0.0");
} else if (distanceKm < 0.0) {
throw new IllegalArgumentException("Invalid " + Location.SP_NEAR_DISTANCE + " parameter '" + distanceKm + "' must be >= 0.0");
} else {
// FIXME KHS scale distance based on lat/long
Double latitude = Double.valueOf(latitudeValue);
Double latitudeDegrees = Double.valueOf(latitudeValue);
Double longitudeDegrees = Double.valueOf(longitudeValue);
Point northPoint = CoordCalculator.findTarget(latitudeDegrees, longitudeDegrees, 0.0, distanceKm);
Point eastPoint = CoordCalculator.findTarget(latitudeDegrees, longitudeDegrees, 90.0, distanceKm);
Point southPoint = CoordCalculator.findTarget(latitudeDegrees, longitudeDegrees, 180.0, distanceKm);
Point westPoint = CoordCalculator.findTarget(latitudeDegrees, longitudeDegrees, 270.0, distanceKm);
latitudePredicate = theBuilder.and(
theBuilder.greaterThanOrEqualTo(theFrom.get("myLatitude"), latitude - distance),
theBuilder.lessThanOrEqualTo(theFrom.get("myLatitude"), latitude + distance)
theBuilder.greaterThanOrEqualTo(theFrom.get("myLatitude"), southPoint.getLatitude()),
theBuilder.lessThanOrEqualTo(theFrom.get("myLatitude"), northPoint.getLatitude())
);
Double longitude = Double.valueOf(longitudeValue);
longitudePredicate = theBuilder.and(
theBuilder.greaterThanOrEqualTo(theFrom.get("myLongitude"), longitude - distance),
theBuilder.lessThanOrEqualTo(theFrom.get("myLongitude"), longitude + distance)
theBuilder.greaterThanOrEqualTo(theFrom.get("myLongitude"), westPoint.getLongitude()),
theBuilder.lessThanOrEqualTo(theFrom.get("myLongitude"), eastPoint.getLongitude())
);
}
Predicate singleCode = theBuilder.and(latitudePredicate, longitudePredicate);

View File

@ -1,13 +1,24 @@
package ca.uhn.fhir.jpa.util;
import org.springframework.data.geo.Point;
import org.hibernate.search.spatial.impl.Point;
public class CoordCalculator {
public static Point findTarget(double theLatitude, double theLongitude, double theBearing, double theDistance) {
double x;
double y;
x = theLatitude;
y = theLatitude;
return new Point(x, y);
public static final double RADIUS_EARTH_KM = 6378.1;
public static Point findTarget(double theLatitudeDegrees, double theLongitudeDegrees, double theBearingDegrees, double theDistanceKm) {
double latitudeRadians = Math.toRadians(theLatitudeDegrees);
double longitudeRadians = Math.toRadians(theLongitudeDegrees);
double bearingRadians = Math.toRadians(theBearingDegrees);
double distanceRadians = theDistanceKm / RADIUS_EARTH_KM;
double targetLatitude = Math.asin( Math.sin(latitudeRadians) * Math.cos(distanceRadians) +
Math.cos(latitudeRadians) * Math.sin(distanceRadians) * Math.cos(bearingRadians));
double targetLongitude = longitudeRadians + Math.atan2(Math.sin(bearingRadians) * Math.sin(distanceRadians) * Math.cos(latitudeRadians),
Math.cos(distanceRadians)-Math.sin(latitudeRadians) * Math.sin(targetLatitude));
return Point.fromDegrees(Math.toDegrees(targetLatitude), Math.toDegrees(targetLongitude));
}
}

View File

@ -7,6 +7,7 @@ import ca.uhn.fhir.jpa.searchparam.SearchParamConstants;
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap.EverythingModeEnum;
import ca.uhn.fhir.jpa.searchparam.registry.SearchParamRegistryImpl;
import ca.uhn.fhir.jpa.util.CoordCalculatorTest;
import ca.uhn.fhir.jpa.util.TestUtil;
import ca.uhn.fhir.model.api.Include;
import ca.uhn.fhir.model.api.TemporalPrecisionEnum;
@ -3476,8 +3477,8 @@ public class FhirResourceDaoDstu3SearchNoFtTest extends BaseJpaDstu3Test {
@Test
public void testNearSearchDistanceNoDistance() {
Location loc = new Location();
double latitude = 1000.0;
double longitude = 2000.0;
double latitude = CoordCalculatorTest.LATITUDE_CHIN;
double longitude = CoordCalculatorTest.LONGITUDE_CHIN;
Location.LocationPositionComponent position = new Location.LocationPositionComponent().setLatitude(latitude).setLongitude(longitude);
loc.setPosition(position);
String locId = myLocationDao.create(loc).getId().toUnqualifiedVersionless().getValue();
@ -3494,8 +3495,8 @@ public class FhirResourceDaoDstu3SearchNoFtTest extends BaseJpaDstu3Test {
@Test
public void testNearSearchDistanceZero() {
Location loc = new Location();
double latitude = 1000.0;
double longitude = 2000.0;
double latitude = CoordCalculatorTest.LATITUDE_CHIN;
double longitude = CoordCalculatorTest.LONGITUDE_CHIN;
Location.LocationPositionComponent position = new Location.LocationPositionComponent().setLatitude(latitude).setLongitude(longitude);
loc.setPosition(position);
String locId = myLocationDao.create(loc).getId().toUnqualifiedVersionless().getValue();
@ -3514,30 +3515,31 @@ public class FhirResourceDaoDstu3SearchNoFtTest extends BaseJpaDstu3Test {
@Test
public void testNearSearchApproximate() {
Location loc = new Location();
double latitude = 1000.0;
double longitude = 2000.0;
double latitude = CoordCalculatorTest.LATITUDE_UHN;
double longitude = CoordCalculatorTest.LONGITUDE_UHN;
Location.LocationPositionComponent position = new Location.LocationPositionComponent().setLatitude(latitude).setLongitude(longitude);
loc.setPosition(position);
String locId = myLocationDao.create(loc).getId().toUnqualifiedVersionless().getValue();
{ // In the box
double offset = 50.0;
double bigEnoughDistance = CoordCalculatorTest.DISTANCE_KM_CHIN_TO_UHN * 2;
SearchParameterMap map = myMatchUrlService.translateMatchUrl(
"Location?" +
Location.SP_NEAR + "=" + (latitude + offset) + ":" + (longitude - offset) +
Location.SP_NEAR + "=" + CoordCalculatorTest.LATITUDE_CHIN + ":" + CoordCalculatorTest.LONGITUDE_CHIN +
"&" +
Location.SP_NEAR_DISTANCE + "=" + (offset * 2) + "|http://unitsofmeasure.org|km", myFhirCtx.getResourceDefinition("Location"));
Location.SP_NEAR_DISTANCE + "=" + bigEnoughDistance + "|http://unitsofmeasure.org|km", myFhirCtx.getResourceDefinition("Location"));
List<String> ids = toUnqualifiedVersionlessIdValues(myLocationDao.search(map));
assertThat(ids, contains(locId));
}
{ // Outside the box
double offset = 50.0;
double tooSmallDistance = CoordCalculatorTest.DISTANCE_KM_CHIN_TO_UHN / 2;
SearchParameterMap map = myMatchUrlService.translateMatchUrl(
"Location?" +
Location.SP_NEAR + "=" + (latitude + offset) + ":" + (longitude - offset) +
Location.SP_NEAR + "=" + CoordCalculatorTest.LATITUDE_CHIN + ":" + CoordCalculatorTest.LONGITUDE_CHIN +
"&" +
Location.SP_NEAR_DISTANCE + "=" + (offset / 2) + "|http://unitsofmeasure.org|km", myFhirCtx.getResourceDefinition("Location"));
Location.SP_NEAR_DISTANCE + "=" + tooSmallDistance + "|http://unitsofmeasure.org|km", myFhirCtx.getResourceDefinition("Location"));
List<String> ids = toUnqualifiedVersionlessIdValues(myLocationDao.search(map));
assertThat(ids.size(), is(0));

View File

@ -15,6 +15,7 @@ import ca.uhn.fhir.jpa.model.entity.ResourceTable;
import ca.uhn.fhir.jpa.searchparam.MatchUrlService;
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap.EverythingModeEnum;
import ca.uhn.fhir.jpa.util.CoordCalculatorTest;
import ca.uhn.fhir.jpa.util.TestUtil;
import ca.uhn.fhir.model.api.Include;
import ca.uhn.fhir.model.api.TemporalPrecisionEnum;
@ -4146,8 +4147,8 @@ public class FhirResourceDaoR4SearchNoFtTest extends BaseJpaR4Test {
@Test
public void testNearSearchDistanceNoDistance() {
Location loc = new Location();
double latitude = 1000.0;
double longitude = 2000.0;
double latitude = CoordCalculatorTest.LATITUDE_CHIN;
double longitude = CoordCalculatorTest.LATITUDE_CHIN;
Location.LocationPositionComponent position = new Location.LocationPositionComponent().setLatitude(latitude).setLongitude(longitude);
loc.setPosition(position);
String locId = myLocationDao.create(loc).getId().toUnqualifiedVersionless().getValue();
@ -4164,8 +4165,8 @@ public class FhirResourceDaoR4SearchNoFtTest extends BaseJpaR4Test {
@Test
public void testNearSearchDistanceZero() {
Location loc = new Location();
double latitude = 1000.0;
double longitude = 2000.0;
double latitude = CoordCalculatorTest.LATITUDE_CHIN;
double longitude = CoordCalculatorTest.LATITUDE_CHIN;
Location.LocationPositionComponent position = new Location.LocationPositionComponent().setLatitude(latitude).setLongitude(longitude);
loc.setPosition(position);
String locId = myLocationDao.create(loc).getId().toUnqualifiedVersionless().getValue();
@ -4192,36 +4193,36 @@ public class FhirResourceDaoR4SearchNoFtTest extends BaseJpaR4Test {
@Test
public void testNearSearchApproximate() {
Location loc = new Location();
double latitude = 1000.0;
double longitude = 2000.0;
double latitude = CoordCalculatorTest.LATITUDE_UHN;
double longitude = CoordCalculatorTest.LONGITUDE_UHN;
Location.LocationPositionComponent position = new Location.LocationPositionComponent().setLatitude(latitude).setLongitude(longitude);
loc.setPosition(position);
String locId = myLocationDao.create(loc).getId().toUnqualifiedVersionless().getValue();
{ // In the box
double offset = 50.0;
double bigEnoughDistance = CoordCalculatorTest.DISTANCE_KM_CHIN_TO_UHN * 2;
SearchParameterMap map = myMatchUrlService.translateMatchUrl(
"Location?" +
Location.SP_NEAR + "=" + (latitude + offset) + "|" +
(longitude - offset) + "|" +
(offset * 2) + "|km",
myFhirCtx.getResourceDefinition("Location"));
Location.SP_NEAR + "=" + CoordCalculatorTest.LATITUDE_CHIN + "|"
+ CoordCalculatorTest.LONGITUDE_CHIN + "|" +
bigEnoughDistance, myFhirCtx.getResourceDefinition("Location"));
List<String> ids = toUnqualifiedVersionlessIdValues(myLocationDao.search(map));
assertThat(ids, contains(locId));
}
{ // Outside the box
double offset = 50.0;
double tooSmallDistance = CoordCalculatorTest.DISTANCE_KM_CHIN_TO_UHN / 2;
SearchParameterMap map = myMatchUrlService.translateMatchUrl(
"Location?" +
Location.SP_NEAR + "=" + (latitude + offset) + "|" +
(longitude - offset) + "|" +
(offset / 2) + "|km",
myFhirCtx.getResourceDefinition("Location"));
Location.SP_NEAR + "=" + CoordCalculatorTest.LATITUDE_CHIN + "|"
+ CoordCalculatorTest.LONGITUDE_CHIN + "|" +
tooSmallDistance, myFhirCtx.getResourceDefinition("Location"));
List<String> ids = toUnqualifiedVersionlessIdValues(myLocationDao.search(map));
assertThat(ids.size(), is(0));
}
}
private String toStringMultiline(List<?> theResults) {

View File

@ -1,20 +1,28 @@
package ca.uhn.fhir.jpa.util;
import org.hibernate.search.spatial.impl.Point;
import org.junit.Test;
import org.springframework.data.geo.Point;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import static org.junit.Assert.*;
import static org.junit.Assert.assertEquals;
public class CoordCalculatorTest {
@Test
public void testCoordCalculator() {
double latitude = 52.20472;
double longitude = 0.14056;
double bearing = 1.57;
double distance = 15.0;
private final Logger ourLog = LoggerFactory.getLogger(CoordCalculatorTest.class);
// CHIN and UHN coordinates from Google Maps
// Distance and bearing from https://www.movable-type.co.uk/scripts/latlong.html
public static final double LATITUDE_CHIN = 43.65513;
public static final double LONGITUDE_CHIN = -79.4170007;
public static final double LATITUDE_UHN = 43.656765;
public static final double LONGITUDE_UHN = -79.3987645;
public static final double DISTANCE_KM_CHIN_TO_UHN = 1.478;
public static final double BEARING_CHIN_TO_UHN = 82 + (55.0 / 60) + (46.0 / 3600);
Point result = CoordCalculator.findTarget(latitude, longitude, bearing, distance);
assertEquals(52.20444, result.getX(), 0.00001);
assertEquals(0.36056, result.getX(), 0.00001);
@Test
public void testCHINToUHN() {
Point result = CoordCalculator.findTarget(LATITUDE_CHIN, LONGITUDE_CHIN, BEARING_CHIN_TO_UHN, DISTANCE_KM_CHIN_TO_UHN);
assertEquals(LATITUDE_UHN, result.getLatitude(), 0.0001);
assertEquals(LONGITUDE_UHN, result.getLatitude(), 0.0001);
}
}

View File

@ -315,7 +315,7 @@ public abstract class BaseSearchParamExtractor implements ISearchParamExtractor
IExtractor<BaseResourceIndexedSearchParam> extractor = (params, searchParam, value, path) -> {
if ("Location.position".equals(path)) {
addCoords_Position(resourceTypeName, params, searchParam, value);
addCoords_Position(resourceTypeName, params, searchParam, value);
}
};
@ -741,6 +741,10 @@ public abstract class BaseSearchParamExtractor implements ISearchParamExtractor
org.hl7.fhir.r4.model.Location.LocationPositionComponent value = (org.hl7.fhir.r4.model.Location.LocationPositionComponent) theValue;
latitude = value.getLatitude();
longitude = value.getLongitude();
} else if (theValue instanceof org.hl7.fhir.r5.model.Location.LocationPositionComponent) {
org.hl7.fhir.r5.model.Location.LocationPositionComponent value = (org.hl7.fhir.r5.model.Location.LocationPositionComponent) theValue;
latitude = value.getLatitude();
longitude = value.getLongitude();
}
// We only accept coordinates when both are present
if (latitude != null && longitude != null) {