001/*-
002 * #%L
003 * HAPI FHIR JPA Server
004 * %%
005 * Copyright (C) 2014 - 2025 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
030/**
031 * Utility for calculating a symmetric GeoBoundingBox around a center point,
032 * ensuring each edge is at least the given distance in kilometers away.
033 */
034public class CoordCalculator {
035        private static final Logger ourLog = getLogger(CoordCalculator.class);
036        public static final double MAX_SUPPORTED_DISTANCE_KM =
037                        10000.0; // Slightly less than a quarter of the earth's circumference
038        private static final double RADIUS_EARTH_KM = 6378.1;
039
040        /**
041         * Computes a symmetric bounding box around the given latitude/longitude center,
042         * with each edge being at least the given distance in km away.
043         *
044         * @param lat center latitude
045         * @param lon center longitude
046         * @param distanceKm distance in kilometers from center to each edge
047         * @return GeoBoundingBox centered around the given coordinates
048         */
049        public static GeoBoundingBox getBox(double lat, double lon, double distanceKm) {
050                // Approximate 1 degree latitude as 111 km
051                double deltaLat = distanceKm / 111.0;
052
053                // Longitude varies with latitude
054                double deltaLon = distanceKm / (111.320 * Math.cos(Math.toRadians(lat)));
055
056                double minLat = normalizeLatitude(lat - deltaLat);
057                double maxLat = normalizeLatitude(lat + deltaLat);
058                double minLon = normalizeLongitude(lon - deltaLon);
059                double maxLon = normalizeLongitude(lon + deltaLon);
060
061                GeoPoint topLeft = GeoPoint.of(maxLat, minLon);
062                GeoPoint bottomRight = GeoPoint.of(minLat, maxLon);
063
064                return GeoBoundingBox.of(topLeft, bottomRight);
065        }
066}