001package ca.uhn.fhir.jpa.search.builder.predicate;
002
003/*-
004 * #%L
005 * HAPI FHIR JPA Server
006 * %%
007 * Copyright (C) 2014 - 2021 Smile CDR, Inc.
008 * %%
009 * Licensed under the Apache License, Version 2.0 (the "License");
010 * you may not use this file except in compliance with the License.
011 * You may obtain a copy of the License at
012 *
013 *      http://www.apache.org/licenses/LICENSE-2.0
014 *
015 * Unless required by applicable law or agreed to in writing, software
016 * distributed under the License is distributed on an "AS IS" BASIS,
017 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
018 * See the License for the specific language governing permissions and
019 * limitations under the License.
020 * #L%
021 */
022
023import ca.uhn.fhir.context.RuntimeSearchParam;
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 ca.uhn.fhir.rest.param.QuantityParam;
031import ca.uhn.fhir.rest.param.SpecialParam;
032import ca.uhn.fhir.rest.param.TokenParam;
033import com.healthmarketscience.sqlbuilder.BinaryCondition;
034import com.healthmarketscience.sqlbuilder.ComboCondition;
035import com.healthmarketscience.sqlbuilder.Condition;
036import com.healthmarketscience.sqlbuilder.dbspec.basic.DbColumn;
037import org.hibernate.search.engine.spatial.GeoBoundingBox;
038
039import static org.apache.commons.lang3.StringUtils.isBlank;
040
041public class CoordsPredicateBuilder extends BaseSearchParamPredicateBuilder {
042
043        private final DbColumn myColumnLatitude;
044        private final DbColumn myColumnLongitude;
045
046        /**
047         * Constructor
048         */
049        public CoordsPredicateBuilder(SearchQueryBuilder theSearchSqlBuilder) {
050                super(theSearchSqlBuilder, theSearchSqlBuilder.addTable("HFJ_SPIDX_COORDS"));
051
052                myColumnLatitude = getTable().addColumn("SP_LATITUDE");
053                myColumnLongitude = getTable().addColumn("SP_LONGITUDE");
054        }
055
056
057        public Condition createPredicateCoords(SearchParameterMap theParams,
058                                                                                                                IQueryParameterType theParam,
059                                                                                                                String theResourceName,
060                                                                                                                RuntimeSearchParam theSearchParam,
061                                                                                                                CoordsPredicateBuilder theFrom,
062                                                                                                                RequestPartitionId theRequestPartitionId) {
063                String latitudeValue;
064                String longitudeValue;
065                double distanceKm = 0.0;
066
067                if (theParam instanceof TokenParam) { // DSTU3
068                        TokenParam param = (TokenParam) theParam;
069                        String value = param.getValue();
070                        String[] parts = value.split(":");
071                        if (parts.length != 2) {
072                                throw new IllegalArgumentException("Invalid position format '" + value + "'.  Required format is 'latitude:longitude'");
073                        }
074                        latitudeValue = parts[0];
075                        longitudeValue = parts[1];
076                        if (isBlank(latitudeValue) || isBlank(longitudeValue)) {
077                                throw new IllegalArgumentException("Invalid position format '" + value + "'.  Both latitude and longitude must be provided.");
078                        }
079                        QuantityParam distanceParam = theParams.getNearDistanceParam();
080                        if (distanceParam != null) {
081                                distanceKm = distanceParam.getValue().doubleValue();
082                        }
083                } else if (theParam instanceof SpecialParam) { // R4
084                        SpecialParam param = (SpecialParam) theParam;
085                        String value = param.getValue();
086                        String[] parts = value.split("\\|");
087                        if (parts.length < 2 || parts.length > 4) {
088                                throw new IllegalArgumentException("Invalid position format '" + value + "'.  Required format is 'latitude|longitude' or 'latitude|longitude|distance' or 'latitude|longitude|distance|units'");
089                        }
090                        latitudeValue = parts[0];
091                        longitudeValue = parts[1];
092                        if (isBlank(latitudeValue) || isBlank(longitudeValue)) {
093                                throw new IllegalArgumentException("Invalid position format '" + value + "'.  Both latitude and longitude must be provided.");
094                        }
095                        if (parts.length >= 3) {
096                                String distanceString = parts[2];
097                                if (!isBlank(distanceString)) {
098                                        distanceKm = Double.parseDouble(distanceString);
099                                }
100                        }
101                } else {
102                        throw new IllegalArgumentException("Invalid position type: " + theParam.getClass());
103                }
104
105                Condition latitudePredicate;
106                Condition longitudePredicate;
107                if (distanceKm == 0.0) {
108                        latitudePredicate = theFrom.createPredicateLatitudeExact(latitudeValue);
109                        longitudePredicate = theFrom.createPredicateLongitudeExact(longitudeValue);
110                } else if (distanceKm < 0.0) {
111                        throw new IllegalArgumentException("Invalid " + Location.SP_NEAR_DISTANCE + " parameter '" + distanceKm + "' must be >= 0.0");
112                } else if (distanceKm > CoordCalculator.MAX_SUPPORTED_DISTANCE_KM) {
113                        throw new IllegalArgumentException("Invalid " + Location.SP_NEAR_DISTANCE + " parameter '" + distanceKm + "' must be <= " + CoordCalculator.MAX_SUPPORTED_DISTANCE_KM);
114                } else {
115                        double latitudeDegrees = Double.parseDouble(latitudeValue);
116                        double longitudeDegrees = Double.parseDouble(longitudeValue);
117
118                        GeoBoundingBox box = CoordCalculator.getBox(latitudeDegrees, longitudeDegrees, distanceKm);
119                        latitudePredicate = theFrom.createLatitudePredicateFromBox(box);
120                        longitudePredicate = theFrom.createLongitudePredicateFromBox(box);
121                }
122                ComboCondition singleCode = ComboCondition.and(latitudePredicate, longitudePredicate);
123                return combineWithHashIdentityPredicate(theResourceName, theSearchParam.getName(), singleCode);
124        }
125
126
127        public Condition createPredicateLatitudeExact(String theLatitudeValue) {
128                return BinaryCondition.equalTo(myColumnLatitude, generatePlaceholder(theLatitudeValue));
129        }
130
131        public Condition createPredicateLongitudeExact(String theLongitudeValue) {
132                return BinaryCondition.equalTo(myColumnLongitude, generatePlaceholder(theLongitudeValue));
133        }
134
135        public Condition createLatitudePredicateFromBox(GeoBoundingBox theBox) {
136                return ComboCondition.and(
137                        BinaryCondition.greaterThanOrEq(myColumnLatitude, generatePlaceholder(theBox.bottomRight().latitude())),
138                        BinaryCondition.lessThanOrEq(myColumnLatitude, generatePlaceholder(theBox.topLeft().latitude()))
139                );
140        }
141
142        public Condition createLongitudePredicateFromBox(GeoBoundingBox theBox) {
143                if (theBox.bottomRight().longitude() < theBox.topLeft().longitude()) {
144                        return ComboCondition.or(
145                                BinaryCondition.greaterThanOrEq(myColumnLongitude, generatePlaceholder(theBox.bottomRight().longitude())),
146                                BinaryCondition.lessThanOrEq(myColumnLongitude, generatePlaceholder(theBox.topLeft().longitude()))
147                        );
148                }
149                return ComboCondition.and(
150                        BinaryCondition.greaterThanOrEq(myColumnLongitude, generatePlaceholder(theBox.topLeft().longitude())),
151                        BinaryCondition.lessThanOrEq(myColumnLongitude, generatePlaceholder(theBox.bottomRight().longitude()))
152                );
153        }
154}