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.search.builder.predicate;
021
022import ca.uhn.fhir.context.RuntimeSearchParam;
023import ca.uhn.fhir.i18n.Msg;
024import ca.uhn.fhir.interceptor.model.RequestPartitionId;
025import ca.uhn.fhir.jpa.search.builder.sql.SearchQueryBuilder;
026import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
027import ca.uhn.fhir.jpa.util.CoordCalculator;
028import ca.uhn.fhir.model.api.IQueryParameterType;
029import ca.uhn.fhir.model.dstu2.resource.Location;
030import com.healthmarketscience.sqlbuilder.BinaryCondition;
031import com.healthmarketscience.sqlbuilder.ComboCondition;
032import com.healthmarketscience.sqlbuilder.Condition;
033import com.healthmarketscience.sqlbuilder.dbspec.basic.DbColumn;
034import org.hibernate.search.engine.spatial.GeoBoundingBox;
035
036public class CoordsPredicateBuilder extends BaseSearchParamPredicateBuilder {
037
038        private final DbColumn myColumnLatitude;
039        private final DbColumn myColumnLongitude;
040
041        /**
042         * Constructor
043         */
044        public CoordsPredicateBuilder(SearchQueryBuilder theSearchSqlBuilder) {
045                super(theSearchSqlBuilder, theSearchSqlBuilder.addTable("HFJ_SPIDX_COORDS"));
046
047                myColumnLatitude = getTable().addColumn("SP_LATITUDE");
048                myColumnLongitude = getTable().addColumn("SP_LONGITUDE");
049        }
050
051        public DbColumn getColumnLatitude() {
052                return myColumnLatitude;
053        }
054
055        public DbColumn getColumnLongitude() {
056                return myColumnLongitude;
057        }
058
059        public Condition createPredicateCoords(
060                        SearchParameterMap theParams,
061                        IQueryParameterType theParam,
062                        String theResourceName,
063                        RuntimeSearchParam theSearchParam,
064                        CoordsPredicateBuilder theFrom,
065                        RequestPartitionId theRequestPartitionId) {
066
067                ParsedLocationParam params = ParsedLocationParam.from(theParams, theParam);
068                double distanceKm = params.getDistanceKm();
069                double latitudeValue = params.getLatitudeValue();
070                double longitudeValue = params.getLongitudeValue();
071
072                Condition latitudePredicate;
073                Condition longitudePredicate;
074                if (distanceKm == 0.0) {
075                        latitudePredicate = theFrom.createPredicateLatitudeExact(latitudeValue);
076                        longitudePredicate = theFrom.createPredicateLongitudeExact(longitudeValue);
077                } else if (distanceKm < 0.0) {
078                        throw new IllegalArgumentException(Msg.code(1233) + "Invalid " + Location.SP_NEAR_DISTANCE + " parameter '"
079                                        + distanceKm + "' must be >= 0.0");
080                } else if (distanceKm > CoordCalculator.MAX_SUPPORTED_DISTANCE_KM) {
081                        throw new IllegalArgumentException(Msg.code(1234) + "Invalid " + Location.SP_NEAR_DISTANCE + " parameter '"
082                                        + distanceKm + "' must be <= " + CoordCalculator.MAX_SUPPORTED_DISTANCE_KM);
083                } else {
084                        GeoBoundingBox box = CoordCalculator.getBox(latitudeValue, longitudeValue, distanceKm);
085                        latitudePredicate = theFrom.createLatitudePredicateFromBox(box);
086                        longitudePredicate = theFrom.createLongitudePredicateFromBox(box);
087                }
088                ComboCondition singleCode = ComboCondition.and(latitudePredicate, longitudePredicate);
089                return combineWithHashIdentityPredicate(theResourceName, theSearchParam.getName(), singleCode);
090        }
091
092        public Condition createPredicateLatitudeExact(double theLatitudeValue) {
093                return BinaryCondition.equalTo(myColumnLatitude, generatePlaceholder(theLatitudeValue));
094        }
095
096        public Condition createPredicateLongitudeExact(double theLongitudeValue) {
097                return BinaryCondition.equalTo(myColumnLongitude, generatePlaceholder(theLongitudeValue));
098        }
099
100        public Condition createLatitudePredicateFromBox(GeoBoundingBox theBox) {
101                return ComboCondition.and(
102                                BinaryCondition.greaterThanOrEq(
103                                                myColumnLatitude,
104                                                generatePlaceholder(theBox.bottomRight().latitude())),
105                                BinaryCondition.lessThanOrEq(
106                                                myColumnLatitude, generatePlaceholder(theBox.topLeft().latitude())));
107        }
108
109        public Condition createLongitudePredicateFromBox(GeoBoundingBox theBox) {
110                if (theBox.bottomRight().longitude() < theBox.topLeft().longitude()) {
111                        return ComboCondition.or(
112                                        BinaryCondition.greaterThanOrEq(
113                                                        myColumnLongitude,
114                                                        generatePlaceholder(theBox.bottomRight().longitude())),
115                                        BinaryCondition.lessThanOrEq(
116                                                        myColumnLongitude,
117                                                        generatePlaceholder(theBox.topLeft().longitude())));
118                } else {
119                        return ComboCondition.and(
120                                        BinaryCondition.greaterThanOrEq(
121                                                        myColumnLongitude,
122                                                        generatePlaceholder(theBox.topLeft().longitude())),
123                                        BinaryCondition.lessThanOrEq(
124                                                        myColumnLongitude,
125                                                        generatePlaceholder(theBox.bottomRight().longitude())));
126                }
127        }
128}