Java tutorial
/* * Copyright 2011-2019 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.springframework.data.mongodb.core.query; import java.util.Arrays; import org.bson.Document; import org.springframework.data.domain.Pageable; import org.springframework.data.geo.CustomMetric; import org.springframework.data.geo.Distance; import org.springframework.data.geo.Metric; import org.springframework.data.geo.Metrics; import org.springframework.data.geo.Point; import org.springframework.data.mongodb.core.geo.GeoJsonPoint; import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; /** * Builder class to build near-queries. <br /> * MongoDB {@code $geoNear} operator allows usage of a {@literal GeoJSON Point} or legacy coordinate pair. Though * syntactically different, there's no difference between {@code near: [-73.99171, 40.738868]} and {@code near: { type: * "Point", coordinates: [-73.99171, 40.738868] } } for the MongoDB server<br /> * <br /> * Please note that there is a huge difference in the distance calculation. Using the legacy format (for near) operates * upon {@literal Radians} on an Earth like sphere, whereas the {@literal GeoJSON} format uses {@literal Meters}. The * actual type within the document is of no concern at this point.<br /> * To avoid a serious headache make sure to set the {@link Metric} to the desired unit of measure which ensures the * distance to be calculated correctly.<br /> * <p /> * In other words: <br /> * Assume you've got 5 Documents like the ones below <br /> * * <pre> * <code> * { * "_id" : ObjectId("5c10f3735d38908db52796a5"), * "name" : "Penn Station", * "location" : { "type" : "Point", "coordinates" : [ -73.99408, 40.75057 ] } * } * { * "_id" : ObjectId("5c10f3735d38908db52796a6"), * "name" : "10gen Office", * "location" : { "type" : "Point", "coordinates" : [ -73.99171, 40.738868 ] } * } * { * "_id" : ObjectId("5c10f3735d38908db52796a9"), * "name" : "City Bakery ", * "location" : { "type" : "Point", "coordinates" : [ -73.992491, 40.738673 ] } * } * { * "_id" : ObjectId("5c10f3735d38908db52796aa"), * "name" : "Splash Bar", * "location" : { "type" : "Point", "coordinates" : [ -73.992491, 40.738673 ] } * } * { * "_id" : ObjectId("5c10f3735d38908db52796ab"), * "name" : "Momofuku Milk Bar", * "location" : { "type" : "Point", "coordinates" : [ -73.985839, 40.731698 ] } * } * </code> * </pre> * * Fetching all Documents within a 400 Meter radius from {@code [-73.99171, 40.738868] } would look like this using * {@literal GeoJSON}: * * <pre> * <code> * { * $geoNear: { * maxDistance: 400, * num: 10, * near: { type: "Point", coordinates: [-73.99171, 40.738868] }, * spherical:true, * key: "location", * distanceField: "distance" * } * } * * </code> * </pre> * * resulting in the following 3 Documents. * * <pre> * <code> * { * "_id" : ObjectId("5c10f3735d38908db52796a6"), * "name" : "10gen Office", * "location" : { "type" : "Point", "coordinates" : [ -73.99171, 40.738868 ] } * "distance" : 0.0 // Meters * } * { * "_id" : ObjectId("5c10f3735d38908db52796a9"), * "name" : "City Bakery ", * "location" : { "type" : "Point", "coordinates" : [ -73.992491, 40.738673 ] } * "distance" : 69.3582262492474 // Meters * } * { * "_id" : ObjectId("5c10f3735d38908db52796aa"), * "name" : "Splash Bar", * "location" : { "type" : "Point", "coordinates" : [ -73.992491, 40.738673 ] } * "distance" : 69.3582262492474 // Meters * } * </code> * </pre> * * Using legacy coordinate pairs one operates upon radians as discussed before. Assume we use {@link Metrics#KILOMETERS} * when constructing the geoNear command. The {@link Metric} will make sure the distance multiplier is set correctly, so * the command is rendered like * * <pre> * <code> * { * $geoNear: { * maxDistance: 0.0000627142377, // 400 Meters * distanceMultiplier: 6378.137, * num: 10, * near: [-73.99171, 40.738868], * spherical:true, * key: "location", * distanceField: "distance" * } * } * </code> * </pre> * * Please note the calculated distance now uses {@literal Kilometers} instead of {@literal Meters} as unit of measure, * so we need to take it times 1000 to match up to {@literal Meters} as in the {@literal GeoJSON} variant. <br /> * Still as we've been requesting the {@link Distance} in {@link Metrics#KILOMETERS} the {@link Distance#getValue()} * reflects exactly this. * * <pre> * <code> * { * "_id" : ObjectId("5c10f3735d38908db52796a6"), * "name" : "10gen Office", * "location" : { "type" : "Point", "coordinates" : [ -73.99171, 40.738868 ] } * "distance" : 0.0 // Kilometers * } * { * "_id" : ObjectId("5c10f3735d38908db52796a9"), * "name" : "City Bakery ", * "location" : { "type" : "Point", "coordinates" : [ -73.992491, 40.738673 ] } * "distance" : 0.0693586286032982 // Kilometers * } * { * "_id" : ObjectId("5c10f3735d38908db52796aa"), * "name" : "Splash Bar", * "location" : { "type" : "Point", "coordinates" : [ -73.992491, 40.738673 ] } * "distance" : 0.0693586286032982 // Kilometers * } * </code> * </pre> * * @author Oliver Gierke * @author Thomas Darimont * @author Christoph Strobl * @author Mark Paluch */ public final class NearQuery { private final Point point; private @Nullable Query query; private @Nullable Distance maxDistance; private @Nullable Distance minDistance; private Metric metric; private boolean spherical; private @Nullable Long limit; private @Nullable Long skip; /** * Creates a new {@link NearQuery}. * * @param point must not be {@literal null}. * @param metric must not be {@literal null}. */ private NearQuery(Point point, Metric metric) { Assert.notNull(point, "Point must not be null!"); Assert.notNull(metric, "Metric must not be null!"); this.point = point; this.spherical = false; this.metric = metric; } /** * Creates a new {@link NearQuery} starting near the given coordinates. * * @param x * @param y * @return */ public static NearQuery near(double x, double y) { return near(x, y, Metrics.NEUTRAL); } /** * Creates a new {@link NearQuery} starting at the given coordinates using the given {@link Metric} to adapt given * values to further configuration. E.g. setting a {@link #maxDistance(double)} will be interpreted as a value of the * initially set {@link Metric}. * * @param x * @param y * @param metric must not be {@literal null}. * @return */ public static NearQuery near(double x, double y, Metric metric) { return near(new Point(x, y), metric); } /** * Creates a new {@link NearQuery} starting at the given {@link Point}. <br /> * <strong>NOTE</strong> There is a difference in using {@link Point} versus {@link GeoJsonPoint}. {@link Point} * values are rendered as coordinate pairs in the legacy format and operate upon radians, whereas the * {@link GeoJsonPoint} uses according to its specification {@literal meters} as unit of measure. This may lead to * different results when using a {@link Metrics#NEUTRAL neutral Metric}. * * @param point must not be {@literal null}. * @return new instance of {@link NearQuery}. */ public static NearQuery near(Point point) { return near(point, Metrics.NEUTRAL); } /** * Creates a {@link NearQuery} starting near the given {@link Point} using the given {@link Metric} to adapt given * values to further configuration. E.g. setting a {@link #maxDistance(double)} will be interpreted as a value of the * initially set {@link Metric}. <br /> * <strong>NOTE</strong> There is a difference in using {@link Point} versus {@link GeoJsonPoint}. {@link Point} * values are rendered as coordinate pairs in the legacy format and operate upon radians, whereas the * {@link GeoJsonPoint} uses according to its specification {@literal meters} as unit of measure. This may lead to * different results when using a {@link Metrics#NEUTRAL neutral Metric}. * * @param point must not be {@literal null}. * @param metric must not be {@literal null}. * @return new instance of {@link NearQuery}. */ public static NearQuery near(Point point, Metric metric) { return new NearQuery(point, metric); } /** * Returns the {@link Metric} underlying the actual query. If no metric was set explicitly {@link Metrics#NEUTRAL} * will be returned. * * @return will never be {@literal null}. */ public Metric getMetric() { return metric; } /** * Configures the maximum number of results to return. * * @param num * @return * @deprecated since 2.2. Please use {@link #limit(long)} instead. */ @Deprecated public NearQuery num(long num) { return limit(num); } /** * Configures the maximum number of results to return. * * @param limit * @return * @since 2.2 */ public NearQuery limit(long limit) { this.limit = limit; return this; } /** * Configures the number of results to skip. * * @param skip * @return */ public NearQuery skip(long skip) { this.skip = skip; return this; } /** * Configures the {@link Pageable} to use. * * @param pageable must not be {@literal null} * @return */ public NearQuery with(Pageable pageable) { Assert.notNull(pageable, "Pageable must not be 'null'."); if (pageable.isPaged()) { this.skip = pageable.getOffset(); this.limit = (long) pageable.getPageSize(); } return this; } /** * Sets the max distance results shall have from the configured origin. If a {@link Metric} was set before the given * value will be interpreted as being a value in that metric. E.g. * * <pre> * NearQuery query = near(10.0, 20.0, Metrics.KILOMETERS).maxDistance(150); * </pre> * * Will set the maximum distance to 150 kilometers. * * @param maxDistance * @return */ public NearQuery maxDistance(double maxDistance) { return maxDistance(new Distance(maxDistance, getMetric())); } /** * Sets the maximum distance supplied in a given metric. Will normalize the distance but not reconfigure the query's * result {@link Metric} if one was configured before. * * @param maxDistance * @param metric must not be {@literal null}. * @return */ public NearQuery maxDistance(double maxDistance, Metric metric) { Assert.notNull(metric, "Metric must not be null!"); return maxDistance(new Distance(maxDistance, metric)); } /** * Sets the maximum distance to the given {@link Distance}. Will set the returned {@link Metric} to be the one of the * given {@link Distance} if {@link Metric} was {@link Metrics#NEUTRAL} before. * * @param distance must not be {@literal null}. * @return */ public NearQuery maxDistance(Distance distance) { Assert.notNull(distance, "Distance must not be null!"); if (distance.getMetric() != Metrics.NEUTRAL) { this.spherical(true); } if (ObjectUtils.nullSafeEquals(Metrics.NEUTRAL, this.metric)) { in(distance.getMetric()); } this.maxDistance = distance; return this; } /** * Sets the minimum distance results shall have from the configured origin. If a {@link Metric} was set before the * given value will be interpreted as being a value in that metric. E.g. * * <pre> * NearQuery query = near(10.0, 20.0, Metrics.KILOMETERS).minDistance(150); * </pre> * * Will set the minimum distance to 150 kilometers. * * @param minDistance * @return * @since 1.7 */ public NearQuery minDistance(double minDistance) { return minDistance(new Distance(minDistance, getMetric())); } /** * Sets the minimum distance supplied in a given metric. Will normalize the distance but not reconfigure the query's * result {@link Metric} if one was configured before. * * @param minDistance * @param metric must not be {@literal null}. * @return * @since 1.7 */ public NearQuery minDistance(double minDistance, Metric metric) { Assert.notNull(metric, "Metric must not be null!"); return minDistance(new Distance(minDistance, metric)); } /** * Sets the minimum distance to the given {@link Distance}. Will set the returned {@link Metric} to be the one of the * given {@link Distance} if no {@link Metric} was set before. * * @param distance must not be {@literal null}. * @return * @since 1.7 */ public NearQuery minDistance(Distance distance) { Assert.notNull(distance, "Distance must not be null!"); if (distance.getMetric() != Metrics.NEUTRAL) { this.spherical(true); } if (this.metric == null) { in(distance.getMetric()); } this.minDistance = distance; return this; } /** * Returns the maximum {@link Distance}. * * @return */ @Nullable public Distance getMaxDistance() { return this.maxDistance; } /** * Returns the maximum {@link Distance}. * * @return * @since 1.7 */ @Nullable public Distance getMinDistance() { return this.minDistance; } /** * Configures a {@link CustomMetric} with the given multiplier. * * @param distanceMultiplier * @return */ public NearQuery distanceMultiplier(double distanceMultiplier) { this.metric = new CustomMetric(distanceMultiplier); return this; } /** * Configures whether to return spherical values for the actual distance. * * @param spherical * @return */ public NearQuery spherical(boolean spherical) { this.spherical = spherical; return this; } /** * Returns whether spharical values will be returned. * * @return */ public boolean isSpherical() { return this.spherical; } /** * Will cause the results' distances being returned in kilometers. Sets {@link #distanceMultiplier(double)} and * {@link #spherical(boolean)} accordingly. * * @return */ public NearQuery inKilometers() { return adaptMetric(Metrics.KILOMETERS); } /** * Will cause the results' distances being returned in miles. Sets {@link #distanceMultiplier(double)} and * {@link #spherical(boolean)} accordingly. * * @return */ public NearQuery inMiles() { return adaptMetric(Metrics.MILES); } /** * Will cause the results' distances being returned in the given metric. Sets {@link #distanceMultiplier(double)} * accordingly as well as {@link #spherical(boolean)} if the given {@link Metric} is not {@link Metrics#NEUTRAL}. * * @param metric the metric the results shall be returned in. Uses {@link Metrics#NEUTRAL} if {@literal null} is * passed. * @return */ public NearQuery in(@Nullable Metric metric) { return adaptMetric(metric == null ? Metrics.NEUTRAL : metric); } /** * Configures the given {@link Metric} to be used as base on for this query and recalculate the maximum distance if no * metric was set before. * * @param metric */ private NearQuery adaptMetric(Metric metric) { if (metric != Metrics.NEUTRAL) { spherical(true); } this.metric = metric; return this; } /** * Adds an actual query to the {@link NearQuery} to restrict the objects considered for the actual near operation. * * @param query must not be {@literal null}. * @return */ public NearQuery query(Query query) { Assert.notNull(query, "Cannot apply 'null' query on NearQuery."); this.query = query; this.skip = query.getSkip(); if (query.getLimit() != 0) { this.limit = (long) query.getLimit(); } return this; } /** * @return the number of elements to skip. */ @Nullable public Long getSkip() { return skip; } /** * Get the {@link Collation} to use along with the {@link #query(Query)}. * * @return the {@link Collation} if set. {@literal null} otherwise. * @since 2.2 */ @Nullable public Collation getCollation() { return query != null ? query.getCollation().orElse(null) : null; } /** * Returns the {@link Document} built by the {@link NearQuery}. * * @return */ public Document toDocument() { Document document = new Document(); if (query != null) { document.put("query", query.getQueryObject()); query.getCollation().ifPresent(collation -> document.append("collation", collation.toDocument())); } if (maxDistance != null) { document.put("maxDistance", getDistanceValueInRadiantsOrMeters(maxDistance)); } if (minDistance != null) { document.put("minDistance", getDistanceValueInRadiantsOrMeters(minDistance)); } if (metric != null) { document.put("distanceMultiplier", getDistanceMultiplier()); } if (limit != null) { document.put("num", limit); } if (usesGeoJson()) { document.put("near", point); } else { document.put("near", Arrays.asList(point.getX(), point.getY())); } document.put("spherical", spherical ? spherical : usesGeoJson()); return document; } private double getDistanceMultiplier() { return usesMetricSystem() ? MetricConversion.getMetersToMetricMultiplier(metric) : metric.getMultiplier(); } private double getDistanceValueInRadiantsOrMeters(Distance distance) { return usesMetricSystem() ? MetricConversion.getDistanceInMeters(distance) : distance.getNormalizedValue(); } private boolean usesMetricSystem() { return usesGeoJson(); } private boolean usesGeoJson() { return point instanceof GeoJsonPoint; } }