001/*-
002 * #%L
003 * HAPI FHIR JPA Server
004 * %%
005 * Copyright (C) 2014 - 2024 Smile CDR, Inc.
006 * %%
007 * Licensed under the Apache License, Version 2.0 (the "License");
008 * you may not use this file except in compliance with the License.
009 * You may obtain a copy of the License at
010 *
011 *      http://www.apache.org/licenses/LICENSE-2.0
012 *
013 * Unless required by applicable law or agreed to in writing, software
014 * distributed under the License is distributed on an "AS IS" BASIS,
015 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
016 * See the License for the specific language governing permissions and
017 * limitations under the License.
018 * #L%
019 */
020package ca.uhn.fhir.jpa.util;
021
022import org.hibernate.search.engine.spatial.GeoBoundingBox;
023import org.hibernate.search.engine.spatial.GeoPoint;
024import org.slf4j.Logger;
025
026import static ca.uhn.fhir.jpa.searchparam.extractor.GeopointNormalizer.normalizeLatitude;
027import static ca.uhn.fhir.jpa.searchparam.extractor.GeopointNormalizer.normalizeLongitude;
028import static org.slf4j.LoggerFactory.getLogger;
029
030public class CoordCalculator {
031        private static final Logger ourLog = getLogger(CoordCalculator.class);
032        public static final double MAX_SUPPORTED_DISTANCE_KM =
033                        10000.0; // Slightly less than a quarter of the earth's circumference
034        private static final double RADIUS_EARTH_KM = 6378.1;
035
036        // Source: https://stackoverflow.com/questions/7222382/get-lat-long-given-current-point-distance-and-bearing
037        static GeoPoint findTarget(
038                        double theLatitudeDegrees, double theLongitudeDegrees, double theBearingDegrees, double theDistanceKm) {
039
040                double latitudeRadians = Math.toRadians(normalizeLatitude(theLatitudeDegrees));
041                double longitudeRadians = Math.toRadians(normalizeLongitude(theLongitudeDegrees));
042                double bearingRadians = Math.toRadians(theBearingDegrees);
043                double distanceRadians = theDistanceKm / RADIUS_EARTH_KM;
044
045                double targetLatitude = Math.asin(Math.sin(latitudeRadians) * Math.cos(distanceRadians)
046                                + Math.cos(latitudeRadians) * Math.sin(distanceRadians) * Math.cos(bearingRadians));
047
048                double targetLongitude = longitudeRadians
049                                + Math.atan2(
050                                                Math.sin(bearingRadians) * Math.sin(distanceRadians) * Math.cos(latitudeRadians),
051                                                Math.cos(distanceRadians) - Math.sin(latitudeRadians) * Math.sin(targetLatitude));
052
053                double latitude = Math.toDegrees(targetLatitude);
054                double longitude = Math.toDegrees(targetLongitude);
055
056                GeoPoint of = GeoPoint.of(normalizeLatitude(latitude), normalizeLongitude(longitude));
057                return of;
058        }
059
060        /**
061         * Find a box around my coordinates such that the closest distance to each edge is the provided distance
062         * @return
063         */
064        public static GeoBoundingBox getBox(double theLatitudeDegrees, double theLongitudeDegrees, Double theDistanceKm) {
065                double diagonalDistanceKm = theDistanceKm * Math.sqrt(2.0);
066
067                GeoPoint topLeft =
068                                CoordCalculator.findTarget(theLatitudeDegrees, theLongitudeDegrees, 315.0, diagonalDistanceKm);
069                GeoPoint bottomRight =
070                                CoordCalculator.findTarget(theLatitudeDegrees, theLongitudeDegrees, 135.0, diagonalDistanceKm);
071
072                return GeoBoundingBox.of(topLeft, bottomRight);
073        }
074}