From e4dfda29a3cc37171fcc85c1e6a3dcc34c5c6201 Mon Sep 17 00:00:00 2001 From: Thomas Darimont Date: Wed, 26 Feb 2014 01:17:26 +0100 Subject: [PATCH] DATACMNS-437 - Introduce reusable value objects for geo concepts. Moved and retro-fitted the basic geo structures from SD MongoDB into SD Commons. This will serve as the foundation for more reusable geo spatial functionality in other SD modules. Adjusted equals methods to support subtypes in other SD modules. Added convenience constructor to Polygon. Renamed spanning points of Box to be first and second because some stores could have a different meaning of the spanning points. Changed radius field of circle to be a Distance (value + metric). Added additional mitigation constructor to Circle. Original pull request: #68. --- .../org/springframework/data/geo/Box.java | 99 ++++++++++++ .../org/springframework/data/geo/Circle.java | 131 ++++++++++++++++ .../data/geo/CustomMetric.java | 45 ++++++ .../springframework/data/geo/Distance.java | 147 +++++++++++++++++ .../org/springframework/data/geo/GeoPage.java | 65 ++++++++ .../springframework/data/geo/GeoResult.java | 106 +++++++++++++ .../springframework/data/geo/GeoResults.java | 148 ++++++++++++++++++ .../org/springframework/data/geo/Metric.java | 33 ++++ .../org/springframework/data/geo/Metrics.java | 45 ++++++ .../org/springframework/data/geo/Point.java | 104 ++++++++++++ .../org/springframework/data/geo/Polygon.java | 120 ++++++++++++++ .../org/springframework/data/geo/Shape.java | 25 +++ .../data/geo/package-info.java | 7 + .../data/geo/BoxUnitTests.java | 55 +++++++ .../data/geo/CircleUnitTests.java | 64 ++++++++ .../data/geo/DistanceUnitTests.java | 65 ++++++++ .../data/geo/GeoResultUnitTests.java | 67 ++++++++ .../data/geo/GeoResultsUnitTests.java | 46 ++++++ .../data/geo/PointUnitTests.java | 57 +++++++ .../data/geo/PolygonUnitTests.java | 66 ++++++++ 20 files changed, 1495 insertions(+) create mode 100644 src/main/java/org/springframework/data/geo/Box.java create mode 100644 src/main/java/org/springframework/data/geo/Circle.java create mode 100644 src/main/java/org/springframework/data/geo/CustomMetric.java create mode 100644 src/main/java/org/springframework/data/geo/Distance.java create mode 100644 src/main/java/org/springframework/data/geo/GeoPage.java create mode 100644 src/main/java/org/springframework/data/geo/GeoResult.java create mode 100644 src/main/java/org/springframework/data/geo/GeoResults.java create mode 100644 src/main/java/org/springframework/data/geo/Metric.java create mode 100644 src/main/java/org/springframework/data/geo/Metrics.java create mode 100644 src/main/java/org/springframework/data/geo/Point.java create mode 100644 src/main/java/org/springframework/data/geo/Polygon.java create mode 100644 src/main/java/org/springframework/data/geo/Shape.java create mode 100644 src/main/java/org/springframework/data/geo/package-info.java create mode 100644 src/test/java/org/springframework/data/geo/BoxUnitTests.java create mode 100644 src/test/java/org/springframework/data/geo/CircleUnitTests.java create mode 100644 src/test/java/org/springframework/data/geo/DistanceUnitTests.java create mode 100644 src/test/java/org/springframework/data/geo/GeoResultUnitTests.java create mode 100644 src/test/java/org/springframework/data/geo/GeoResultsUnitTests.java create mode 100644 src/test/java/org/springframework/data/geo/PointUnitTests.java create mode 100644 src/test/java/org/springframework/data/geo/PolygonUnitTests.java diff --git a/src/main/java/org/springframework/data/geo/Box.java b/src/main/java/org/springframework/data/geo/Box.java new file mode 100644 index 000000000..f3b67d34d --- /dev/null +++ b/src/main/java/org/springframework/data/geo/Box.java @@ -0,0 +1,99 @@ +/* + * Copyright 2010-2014 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 + * + * http://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.geo; + +import org.springframework.util.Assert; + +/** + * Represents a geospatial box value + * + * @author Mark Pollack + * @author Oliver Gierke + * @author Thomas Darimont + * @since 1.8 + */ +public class Box implements Shape { + + private final Point first; + private final Point second; + + /** + * Creates a new Box spanning from the given first to the second {@link Point}. + * + * @param first must not be {@literal null}. + * @param second must not be {@literal null}. + */ + public Box(Point first, Point second) { + + Assert.notNull(first); + Assert.notNull(second); + + this.first = first; + this.second = second; + } + + /** + * Creates a new Box from the given {@code first} to the {@code second} point represented as the {@literal double[]}. + * + * @param first must not be {@literal null} and contain exactly 2 doubles. + * @param second must not be {@literal null} and contain exactly 2 doubles. + */ + public Box(double[] first, double[] second) { + + Assert.isTrue(first.length == 2, "Point array has to have 2 elements!"); + Assert.isTrue(second.length == 2, "Point array has to have 2 elements!"); + + this.first = new Point(first[0], first[1]); + this.second = new Point(second[0], second[1]); + } + + public Point getFirst() { + return first; + } + + public Point getSecond() { + return second; + } + + @Override + public String toString() { + return String.format("Box [%s, %s]", first, second); + } + + @Override + public int hashCode() { + + int result = 31; + result += 17 * first.hashCode(); + result += 17 * second.hashCode(); + return result; + } + + @Override + public boolean equals(Object obj) { + + if (this == obj) { + return true; + } + + if (obj == null || !(obj instanceof Box)) { + return false; + } + + Box that = (Box) obj; + return this.first.equals(that.first) && this.second.equals(that.second); + } +} diff --git a/src/main/java/org/springframework/data/geo/Circle.java b/src/main/java/org/springframework/data/geo/Circle.java new file mode 100644 index 000000000..d4051528b --- /dev/null +++ b/src/main/java/org/springframework/data/geo/Circle.java @@ -0,0 +1,131 @@ +/* + * Copyright 2010-2014 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 + * + * http://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.geo; + +import org.springframework.data.annotation.PersistenceConstructor; +import org.springframework.util.Assert; + +/** + * Represents a geospatial circle value + * + * @author Mark Pollack + * @author Oliver Gierke + * @author Thomas Darimont + * @since 1.8 + */ +public class Circle implements Shape { + + private final Point center; + private final Distance radius; + + /** + * Creates a new {@link Circle} from the given {@link Point} and radius. + * + * @param center must not be {@literal null}. + * @param radius must not be {@literal null} and it's value greater or equal to zero. + */ + @PersistenceConstructor + public Circle(Point center, Distance radius) { + + Assert.notNull(center); + Assert.notNull(radius); + Assert.isTrue(radius.getValue() >= 0, "Radius must not be negative!"); + + this.center = center; + this.radius = radius; + } + + /** + * Creates a new {@link Circle} from the given {@link Point} and radius. + * + * @param center must not be {@literal null}. + * @param radius's value must be greater or equal to zero. + */ + @PersistenceConstructor + public Circle(Point center, double radius) { + this(center, new Distance(radius)); + } + + /** + * Creates a new {@link Circle} from the given coordinates and radius as {@link Distance} with a + * {@link Metrics#NEUTRAL}. + * + * @param centerX + * @param centerY + * @param radius must be greater or equal to zero. + */ + public Circle(double centerX, double centerY, double radius) { + this(new Point(centerX, centerY), new Distance(radius)); + } + + /** + * Returns the center of the {@link Circle}. + * + * @return will never be {@literal null}. + */ + public Point getCenter() { + return center; + } + + /** + * Returns the radius of the {@link Circle}. + * + * @return + */ + public Distance getRadius() { + return radius; + } + + /* + * (non-Javadoc) + * @see java.lang.Object#toString() + */ + @Override + public String toString() { + return String.format("Circle [center=%s, radius=%f]", center, radius); + } + + /* (non-Javadoc) + * @see java.lang.Object#equals(java.lang.Object) + */ + @Override + public boolean equals(Object obj) { + + if (this == obj) { + return true; + } + + if (obj == null || !(obj instanceof Circle)) { + return false; + } + + Circle that = (Circle) obj; + + return this.center.equals(that.center) && this.radius.equals(that.radius); + } + + /* + * (non-Javadoc) + * @see java.lang.Object#hashCode() + */ + @Override + public int hashCode() { + int result = 17; + result += 31 * center.hashCode(); + result += 31 * radius.hashCode(); + return result; + } +} diff --git a/src/main/java/org/springframework/data/geo/CustomMetric.java b/src/main/java/org/springframework/data/geo/CustomMetric.java new file mode 100644 index 000000000..c77c66ca8 --- /dev/null +++ b/src/main/java/org/springframework/data/geo/CustomMetric.java @@ -0,0 +1,45 @@ +/* + * Copyright 2013-2014 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 + * + * http://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.geo; + +/** + * Value object to create custom {@link Metric}s on the fly. + * + * @author Oliver Gierke + * @author Thomas Darimont + * @since 1.8 + */ +public class CustomMetric implements Metric { + + private final double multiplier; + + /** + * Creates a custom {@link Metric} using the given multiplier. + * + * @param multiplier + */ + public CustomMetric(double multiplier) { + this.multiplier = multiplier; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.geo.Metric#getMultiplier() + */ + public double getMultiplier() { + return multiplier; + } +} diff --git a/src/main/java/org/springframework/data/geo/Distance.java b/src/main/java/org/springframework/data/geo/Distance.java new file mode 100644 index 000000000..e671c3b03 --- /dev/null +++ b/src/main/java/org/springframework/data/geo/Distance.java @@ -0,0 +1,147 @@ +/* + * Copyright 2010-2014 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 + * + * http://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.geo; + +import org.springframework.util.ObjectUtils; + +/** + * Value object to represent distances in a given metric. + * + * @author Oliver Gierke + * @author Thomas Darimont + * @since 1.8 + */ +public class Distance { + + private final double value; + private final Metric metric; + + /** + * Creates a new {@link Distance}. + * + * @param value + */ + public Distance(double value) { + this(value, Metrics.NEUTRAL); + } + + /** + * Creates a new {@link Distance} with the given {@link Metric}. + * + * @param value + * @param metric + */ + public Distance(double value, Metric metric) { + this.value = value; + this.metric = metric == null ? Metrics.NEUTRAL : metric; + } + + /** + * @return the value + */ + public double getValue() { + return value; + } + + /** + * Returns the normalized value regarding the underlying {@link Metric}. + * + * @return + */ + public double getNormalizedValue() { + return value / metric.getMultiplier(); + } + + /** + * @return the metric + */ + public Metric getMetric() { + return metric; + } + + /** + * Adds the given distance to the current one. The resulting {@link Distance} will be in the same metric as the + * current one. + * + * @param other + * @return + */ + public Distance add(Distance other) { + double newNormalizedValue = getNormalizedValue() + other.getNormalizedValue(); + return new Distance(newNormalizedValue * metric.getMultiplier(), metric); + } + + /** + * Adds the given {@link Distance} to the current one and forces the result to be in a given {@link Metric}. + * + * @param other + * @param metric + * @return + */ + public Distance add(Distance other, Metric metric) { + double newLeft = getNormalizedValue() * metric.getMultiplier(); + double newRight = other.getNormalizedValue() * metric.getMultiplier(); + return new Distance(newLeft + newRight, metric); + } + + /* + * (non-Javadoc) + * @see java.lang.Object#equals(java.lang.Object) + */ + @Override + public boolean equals(Object obj) { + + if (this == obj) { + return true; + } + + if (obj == null || !getClass().isInstance(obj)) { + return false; + } + + Distance that = (Distance) obj; + + return this.value == that.value && ObjectUtils.nullSafeEquals(this.metric, that.metric); + } + + /* + * (non-Javadoc) + * @see java.lang.Object#hashCode() + */ + @Override + public int hashCode() { + int result = 17; + result += 31 * Double.doubleToLongBits(value); + result += 31 * ObjectUtils.nullSafeHashCode(metric); + return result; + } + + /* (non-Javadoc) + * @see java.lang.Object#toString() + */ + @Override + public String toString() { + + StringBuilder builder = new StringBuilder(); + builder.append(value); + + if (metric != Metrics.NEUTRAL) { + builder.append(" ").append(metric.toString()); + } + + return builder.toString(); + } +} diff --git a/src/main/java/org/springframework/data/geo/GeoPage.java b/src/main/java/org/springframework/data/geo/GeoPage.java new file mode 100644 index 000000000..f716307ee --- /dev/null +++ b/src/main/java/org/springframework/data/geo/GeoPage.java @@ -0,0 +1,65 @@ +/* + * Copyright 2011-2014 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 + * + * http://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.geo; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.Pageable; + +/** + * Custom {@link Page} to carry the average distance retrieved from the {@link GeoResults} the {@link GeoPage} is set up + * from. + * + * @author Oliver Gierke + * @author Thomas Darimont + * @since 1.8 + */ +public class GeoPage extends PageImpl> { + + private static final long serialVersionUID = -5655267379242128600L; + private final Distance averageDistance; + + /** + * Creates a new {@link GeoPage} from the given {@link GeoResults}. + * + * @param content must not be {@literal null}. + */ + public GeoPage(GeoResults results) { + super(results.getContent()); + this.averageDistance = results.getAverageDistance(); + } + + /** + * Creates a new {@link GeoPage} from the given {@link GeoResults}, {@link Pageable} and total. + * + * @param results must not be {@literal null}. + * @param pageable must not be {@literal null}. + * @param total + */ + public GeoPage(GeoResults results, Pageable pageable, long total) { + super(results.getContent(), pageable, total); + this.averageDistance = results.getAverageDistance(); + } + + /** + * Returns the average distance of the underlying results. + * + * @return the averageDistance + */ + public Distance getAverageDistance() { + return averageDistance; + } +} diff --git a/src/main/java/org/springframework/data/geo/GeoResult.java b/src/main/java/org/springframework/data/geo/GeoResult.java new file mode 100644 index 000000000..e269a8695 --- /dev/null +++ b/src/main/java/org/springframework/data/geo/GeoResult.java @@ -0,0 +1,106 @@ +/* + * Copyright 2011-2014 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 + * + * http://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.geo; + +import org.springframework.util.Assert; + +/** + * Calue object capturing some arbitrary object plus a distance. + * + * @author Oliver Gierke + * @author Thomas Darimont + * @since 1.8 + */ +public class GeoResult { + + private final T content; + private final Distance distance; + + /** + * Creates a new {@link GeoResult} for the given content and distance. + * + * @param content must not be {@literal null}. + * @param distance must not be {@literal null}. + */ + public GeoResult(T content, Distance distance) { + + Assert.notNull(content); + Assert.notNull(distance); + + this.content = content; + this.distance = distance; + } + + /** + * Returns the actual content object. + * + * @return the content + */ + public T getContent() { + return content; + } + + /** + * Returns the distance the actual content object has from the origin. + * + * @return the distance + */ + public Distance getDistance() { + return distance; + } + + /* + * (non-Javadoc) + * @see java.lang.Object#equals(java.lang.Object) + */ + @Override + public boolean equals(Object obj) { + + if (this == obj) { + return true; + } + + if (obj == null || !getClass().isInstance(obj)) { + return false; + } + + GeoResult that = (GeoResult) obj; + + return this.content.equals(that.content) && this.distance.equals(that.distance); + } + + /* + * (non-Javadoc) + * @see java.lang.Object#hashCode() + */ + @Override + public int hashCode() { + + int result = 17; + result += 31 * distance.hashCode(); + result += 31 * content.hashCode(); + return result; + } + + /* + * (non-Javadoc) + * @see java.lang.Object#toString() + */ + @Override + public String toString() { + return String.format("GeoResult [content: %s, distance: %s, ]", content.toString(), distance.toString()); + } +} diff --git a/src/main/java/org/springframework/data/geo/GeoResults.java b/src/main/java/org/springframework/data/geo/GeoResults.java new file mode 100644 index 000000000..4c9d8d7a1 --- /dev/null +++ b/src/main/java/org/springframework/data/geo/GeoResults.java @@ -0,0 +1,148 @@ +/* + * Copyright 2011-2014 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 + * + * http://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.geo; + +import java.util.Collections; +import java.util.Iterator; +import java.util.List; + +import org.springframework.data.annotation.PersistenceConstructor; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +/** + * Value object to capture {@link GeoResult}s as well as the average distance they have. + * + * @author Oliver Gierke + * @author Thomas Darimont + * @since 1.8 + */ +public class GeoResults implements Iterable> { + + private final List> results; + private final Distance averageDistance; + + /** + * Creates a new {@link GeoResults} instance manually calculating the average distance from the distance values of the + * given {@link GeoResult}s. + * + * @param results must not be {@literal null}. + */ + public GeoResults(List> results) { + this(results, (Metric) null); + } + + public GeoResults(List> results, Metric metric) { + this(results, calculateAverageDistance(results, metric)); + } + + /** + * Creates a new {@link GeoResults} instance from the given {@link GeoResult}s and average distance. + * + * @param results must not be {@literal null}. + * @param averageDistance + */ + @PersistenceConstructor + public GeoResults(List> results, Distance averageDistance) { + Assert.notNull(results); + this.results = results; + this.averageDistance = averageDistance; + } + + /** + * Returns the average distance of all {@link GeoResult}s in this list. + * + * @return the averageDistance + */ + public Distance getAverageDistance() { + return averageDistance; + } + + /* + * (non-Javadoc) + * @see java.lang.Iterable#iterator() + */ + @SuppressWarnings("unchecked") + public Iterator> iterator() { + return (Iterator>) results.iterator(); + } + + /** + * Returns the actual + * + * @return + */ + public List> getContent() { + return Collections.unmodifiableList(results); + } + + /* + * (non-Javadoc) + * @see java.lang.Object#equals(java.lang.Object) + */ + @Override + public boolean equals(Object obj) { + + if (this == obj) { + return true; + } + + if (obj == null || !getClass().isInstance(obj)) { + return false; + } + + GeoResults that = (GeoResults) obj; + + return this.results.equals(that.results) && this.averageDistance == that.averageDistance; + } + + /* + * (non-Javadoc) + * @see java.lang.Object#hashCode() + */ + @Override + public int hashCode() { + int result = 17; + result += 31 * results.hashCode(); + result += 31 * averageDistance.hashCode(); + return result; + } + + /* + * (non-Javadoc) + * @see java.lang.Object#toString() + */ + @Override + public String toString() { + return String.format("GeoResults: [averageDistance: %s, results: %s]", averageDistance.toString(), + StringUtils.collectionToCommaDelimitedString(results)); + } + + private static Distance calculateAverageDistance(List> results, Metric metric) { + + if (results.isEmpty()) { + return new Distance(0, metric); + } + + double averageDistance = 0; + + for (GeoResult result : results) { + averageDistance += result.getDistance().getValue(); + } + + return new Distance(averageDistance / results.size(), metric); + } +} diff --git a/src/main/java/org/springframework/data/geo/Metric.java b/src/main/java/org/springframework/data/geo/Metric.java new file mode 100644 index 000000000..e4f457e74 --- /dev/null +++ b/src/main/java/org/springframework/data/geo/Metric.java @@ -0,0 +1,33 @@ +/* + * Copyright 2011-2014 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 + * + * http://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.geo; + +/** + * Interface for {@link Metric}s that can be applied to a base scale. + * + * @author Oliver Gierke + * @author Thomas Darimont + * @since 1.8 + */ +public interface Metric { + + /** + * Returns the multiplier to calculate metrics values from a base scale. + * + * @return + */ + double getMultiplier(); +} diff --git a/src/main/java/org/springframework/data/geo/Metrics.java b/src/main/java/org/springframework/data/geo/Metrics.java new file mode 100644 index 000000000..0fb4345d0 --- /dev/null +++ b/src/main/java/org/springframework/data/geo/Metrics.java @@ -0,0 +1,45 @@ +/* + * Copyright 2011-2014 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 + * + * http://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.geo; + +/** + * Commonly used {@link Metric}s. + * + * @author Oliver Gierke + * @author Thomas Darimont + * @since 1.8 + */ +public enum Metrics implements Metric { + + KILOMETERS(6378.137), MILES(3963.191), NEUTRAL(1); + + private final double multiplier; + + /** + * @param multiplier the earth radius at equator. + */ + private Metrics(double multiplier) { + this.multiplier = multiplier; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.geo.Metric#getMultiplier() + */ + public double getMultiplier() { + return multiplier; + } +} diff --git a/src/main/java/org/springframework/data/geo/Point.java b/src/main/java/org/springframework/data/geo/Point.java new file mode 100644 index 000000000..e5f21fb93 --- /dev/null +++ b/src/main/java/org/springframework/data/geo/Point.java @@ -0,0 +1,104 @@ +/* + * Copyright 2010-2014 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 + * + * http://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.geo; + +import org.springframework.data.annotation.PersistenceConstructor; +import org.springframework.util.Assert; + +/** + * Represents a geospatial point value. + * + * @author Mark Pollack + * @author Oliver Gierke + * @author Thomas Darimont + * @since 1.8 + */ +public class Point { + + private final double x; + private final double y; + + /** + * Creates a {@link Point} from the given {@code x}, {@code y} coordinate. + * + * @param x + * @param y + */ + @PersistenceConstructor + public Point(double x, double y) { + this.x = x; + this.y = y; + } + + /** + * Creates a {@link Point} from the given {@link Point} coordinate. + * + * @param point must not be {@literal null}. + */ + public Point(Point point) { + + Assert.notNull(point); + + this.x = point.x; + this.y = point.y; + } + + public double getX() { + return x; + } + + public double getY() { + return y; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + long temp; + temp = Double.doubleToLongBits(x); + result = prime * result + (int) (temp ^ (temp >>> 32)); + temp = Double.doubleToLongBits(y); + result = prime * result + (int) (temp ^ (temp >>> 32)); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (!(obj instanceof Point)) { + return false; + } + Point other = (Point) obj; + if (Double.doubleToLongBits(x) != Double.doubleToLongBits(other.x)) { + return false; + } + if (Double.doubleToLongBits(y) != Double.doubleToLongBits(other.y)) { + return false; + } + return true; + } + + @Override + public String toString() { + return String.format("Point [latitude=%f, longitude=%f]", x, y); + } +} diff --git a/src/main/java/org/springframework/data/geo/Polygon.java b/src/main/java/org/springframework/data/geo/Polygon.java new file mode 100644 index 000000000..7cc3b0094 --- /dev/null +++ b/src/main/java/org/springframework/data/geo/Polygon.java @@ -0,0 +1,120 @@ +/* + * Copyright 2011-2014 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 + * + * http://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.geo; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Iterator; +import java.util.List; + +import org.springframework.util.Assert; + +/** + * Simple value object to represent a {@link Polygon}. + * + * @author Oliver Gierke + * @author Thomas Darimont + * @since 1.8 + */ +public class Polygon implements Iterable, Shape { + + private final List points; + + /** + * Creates a new {@link Polygon} for the given Points. + * + * @param x + * @param y + * @param z + * @param others + */ + public

Polygon(P x, P y, P z, P... others) { + + Assert.notNull(x); + Assert.notNull(y); + Assert.notNull(z); + Assert.notNull(others); + + this.points = new ArrayList(3 + others.length); + this.points.addAll(Arrays.asList(x, y, z)); + this.points.addAll(Arrays.asList(others)); + } + + /** + * Creates a new {@link Polygon} for the given Points. + * + * @param points + */ + public

Polygon(List

points) { + + Assert.notNull(points); + + this.points = new ArrayList(points.size()); + + for (Point point : points) { + Assert.notNull(point); + this.points.add(point); + } + } + + public List getPoints() { + + List result = new ArrayList(); + + for (Point point : points) { + result.add(point); + } + + return result; + } + + /* + * (non-Javadoc) + * @see java.lang.Iterable#iterator() + */ + public Iterator iterator() { + return this.points.iterator(); + } + + /* + * (non-Javadoc) + * @see java.lang.Object#equals(java.lang.Object) + */ + @Override + public boolean equals(Object obj) { + + if (this == obj) { + return true; + } + + if (obj == null || !(obj instanceof Polygon)) { + return false; + } + + Polygon that = (Polygon) obj; + + return this.points.equals(that.points); + } + + /* + * (non-Javadoc) + * @see java.lang.Object#hashCode() + */ + @Override + public int hashCode() { + return points.hashCode(); + } +} diff --git a/src/main/java/org/springframework/data/geo/Shape.java b/src/main/java/org/springframework/data/geo/Shape.java new file mode 100644 index 000000000..560afdc96 --- /dev/null +++ b/src/main/java/org/springframework/data/geo/Shape.java @@ -0,0 +1,25 @@ +/* + * Copyright 2011-2014 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 + * + * http://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.geo; + + +/** + * Common interface for all shapes. Allows building external representations of them. + * + * @author Oliver Gierke + * @author Thomas Darimont + */ +public interface Shape {} diff --git a/src/main/java/org/springframework/data/geo/package-info.java b/src/main/java/org/springframework/data/geo/package-info.java new file mode 100644 index 000000000..d9ce1acdb --- /dev/null +++ b/src/main/java/org/springframework/data/geo/package-info.java @@ -0,0 +1,7 @@ +/** + * Support for reusable geospatial structures. + * + * @author Thomas Darimont + * @since 1.8 + */ +package org.springframework.data.geo; \ No newline at end of file diff --git a/src/test/java/org/springframework/data/geo/BoxUnitTests.java b/src/test/java/org/springframework/data/geo/BoxUnitTests.java new file mode 100644 index 000000000..5343aefea --- /dev/null +++ b/src/test/java/org/springframework/data/geo/BoxUnitTests.java @@ -0,0 +1,55 @@ +/* + * Copyright 2011-2014 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 + * + * http://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.geo; + +import static org.hamcrest.CoreMatchers.*; +import static org.junit.Assert.*; + +import org.junit.Test; + +/** + * Unit tests for {@link Box}. + * + * @author Oliver Gierke + * @author Thomas Darimont + */ +public class BoxUnitTests { + + Box first = new Box(new Point(1d, 1d), new Point(2d, 2d)); + Box second = new Box(new Point(1d, 1d), new Point(2d, 2d)); + Box third = new Box(new Point(3d, 3d), new Point(1d, 1d)); + + /** + * @see DATACMNS-437 + */ + @Test + public void equalsWorksCorrectly() { + + assertThat(first.equals(second), is(true)); + assertThat(second.equals(first), is(true)); + assertThat(first.equals(third), is(false)); + } + + /** + * @see DATACMNS-437 + */ + @Test + public void hashCodeWorksCorrectly() { + + assertThat(first.hashCode(), is(second.hashCode())); + assertThat(first.hashCode(), is(not(third.hashCode()))); + } +} diff --git a/src/test/java/org/springframework/data/geo/CircleUnitTests.java b/src/test/java/org/springframework/data/geo/CircleUnitTests.java new file mode 100644 index 000000000..c9e42ba98 --- /dev/null +++ b/src/test/java/org/springframework/data/geo/CircleUnitTests.java @@ -0,0 +1,64 @@ +/* + * Copyright 2011-2014 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 + * + * http://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.geo; + +import static org.hamcrest.CoreMatchers.*; +import static org.junit.Assert.*; + +import org.junit.Test; + +/** + * Unit tests for {@link Circle}. + * + * @author Oliver Gierke + * @author Thomas Darimont + */ +public class CircleUnitTests { + + /** + * @see DATACMNS-437 + */ + @Test(expected = IllegalArgumentException.class) + public void rejectsNullOrigin() { + new Circle(null, new Distance(0)); + } + + /** + * @see DATACMNS-437 + */ + @Test(expected = IllegalArgumentException.class) + public void rejectsNegativeRadius() { + new Circle(1, 1, -1); + } + + /** + * @see DATACMNS-437 + */ + @Test + public void considersTwoCirclesEqualCorrectly() { + + Circle left = new Circle(1, 1, 1); + Circle right = new Circle(1, 1, 1); + + assertThat(left, is(right)); + assertThat(right, is(left)); + + right = new Circle(new Point(1, 1), new Distance(1)); + + assertThat(left, is(right)); + assertThat(right, is(left)); + } +} diff --git a/src/test/java/org/springframework/data/geo/DistanceUnitTests.java b/src/test/java/org/springframework/data/geo/DistanceUnitTests.java new file mode 100644 index 000000000..684e1da29 --- /dev/null +++ b/src/test/java/org/springframework/data/geo/DistanceUnitTests.java @@ -0,0 +1,65 @@ +/* + * Copyright 2011-2014 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 + * + * http://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.geo; + +import static org.hamcrest.CoreMatchers.*; +import static org.junit.Assert.*; +import static org.springframework.data.geo.Metrics.*; + +import org.junit.Test; + +/** + * Unit tests for {@link Distance}. + * + * @author Oliver Gierke + * @author Thomas Darimont + */ +public class DistanceUnitTests { + + /** + * @see DATACMNS-437 + */ + @Test + public void defaultsMetricToNeutralOne() { + + assertThat(new Distance(2.5).getMetric(), is((Metric) Metrics.NEUTRAL)); + assertThat(new Distance(2.5, null).getMetric(), is((Metric) Metrics.NEUTRAL)); + } + + /** + * @see DATACMNS-437 + */ + @Test + public void addsDistancesWithoutExplicitMetric() { + + Distance left = new Distance(2.5, KILOMETERS); + Distance right = new Distance(2.5, KILOMETERS); + + assertThat(left.add(right), is(new Distance(5.0, KILOMETERS))); + } + + /** + * @see DATACMNS-437 + */ + @Test + public void addsDistancesWithExplicitMetric() { + + Distance left = new Distance(2.5, KILOMETERS); + Distance right = new Distance(2.5, KILOMETERS); + + assertThat(left.add(right, MILES), is(new Distance(3.106856281073925, MILES))); + } +} diff --git a/src/test/java/org/springframework/data/geo/GeoResultUnitTests.java b/src/test/java/org/springframework/data/geo/GeoResultUnitTests.java new file mode 100644 index 000000000..e16843615 --- /dev/null +++ b/src/test/java/org/springframework/data/geo/GeoResultUnitTests.java @@ -0,0 +1,67 @@ +/* + * Copyright 2011-2014 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 + * + * http://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.geo; + +import static org.hamcrest.CoreMatchers.*; +import static org.junit.Assert.*; + +import org.junit.Test; + +/** + * Unit tests for {@link GeoResult}. + * + * @author Oliver Gierke + * @author Thomas Darimont + */ +public class GeoResultUnitTests { + + GeoResult first = new GeoResult("Foo", new Distance(2.5)); + GeoResult second = new GeoResult("Foo", new Distance(2.5)); + GeoResult third = new GeoResult("Bar", new Distance(2.5)); + GeoResult fourth = new GeoResult("Foo", new Distance(5.2)); + + /** + * @see DATACMNS-437 + */ + @Test + public void considersSameInstanceEqual() { + + assertThat(first.equals(first), is(true)); + } + + /** + * @see DATACMNS-437 + */ + @Test + public void considersSameValuesAsEqual() { + + assertThat(first.equals(second), is(true)); + assertThat(second.equals(first), is(true)); + assertThat(first.equals(third), is(false)); + assertThat(third.equals(first), is(false)); + assertThat(first.equals(fourth), is(false)); + assertThat(fourth.equals(first), is(false)); + } + + /** + * @see DATACMNS-437 + */ + @SuppressWarnings({ "rawtypes", "unchecked" }) + @Test(expected = IllegalArgumentException.class) + public void rejectsNullContent() { + new GeoResult(null, new Distance(2.5)); + } +} diff --git a/src/test/java/org/springframework/data/geo/GeoResultsUnitTests.java b/src/test/java/org/springframework/data/geo/GeoResultsUnitTests.java new file mode 100644 index 000000000..0f97c8b8e --- /dev/null +++ b/src/test/java/org/springframework/data/geo/GeoResultsUnitTests.java @@ -0,0 +1,46 @@ +/* + * Copyright 2011-2014 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 + * + * http://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.geo; + +import static org.hamcrest.CoreMatchers.*; +import static org.junit.Assert.*; + +import java.util.Arrays; + +import org.junit.Test; + +/** + * Unit tests for {@link GeoResults}. + * + * @author Oliver Gierke + * @author Thomas Darimont + */ +public class GeoResultsUnitTests { + + /** + * @see DATACMNS-437 + */ + @Test + @SuppressWarnings("unchecked") + public void calculatesAverageForGivenGeoResults() { + + GeoResult first = new GeoResult(new Object(), new Distance(2)); + GeoResult second = new GeoResult(new Object(), new Distance(5)); + GeoResults geoResults = new GeoResults(Arrays.asList(first, second)); + + assertThat(geoResults.getAverageDistance(), is(new Distance(3.5))); + } +} diff --git a/src/test/java/org/springframework/data/geo/PointUnitTests.java b/src/test/java/org/springframework/data/geo/PointUnitTests.java new file mode 100644 index 000000000..daad11596 --- /dev/null +++ b/src/test/java/org/springframework/data/geo/PointUnitTests.java @@ -0,0 +1,57 @@ +/* + * Copyright 2011-2014 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 + * + * http://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.geo; + +import static org.hamcrest.CoreMatchers.*; +import static org.junit.Assert.*; + +import org.junit.Test; + +/** + * Unit tests for {@link Point}. + * + * @author Oliver Gierke + * @author Thomas Darimont + */ +public class PointUnitTests { + + /** + * @see DATACMNS-437 + */ + @Test(expected = IllegalArgumentException.class) + public void rejectsNullforCopyConstructor() { + new Point(null); + } + + /** + * @see DATACMNS-437 + */ + @Test + public void equalsIsImplementedCorrectly() { + + assertThat(new Point(1.5, 1.5), is(equalTo(new Point(1.5, 1.5)))); + assertThat(new Point(1.5, 1.5), is(not(equalTo(new Point(2.0, 2.0))))); + assertThat(new Point(2.0, 2.0), is(not(equalTo(new Point(1.5, 1.5))))); + } + + /** + * @see DATACMNS-437 + */ + @Test + public void invokingToStringWorksCorrectly() { + new Point(1.5, 1.5).toString(); + } +} diff --git a/src/test/java/org/springframework/data/geo/PolygonUnitTests.java b/src/test/java/org/springframework/data/geo/PolygonUnitTests.java new file mode 100644 index 000000000..68d2677ba --- /dev/null +++ b/src/test/java/org/springframework/data/geo/PolygonUnitTests.java @@ -0,0 +1,66 @@ +/* + * Copyright 2011-2014 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 + * + * http://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.geo; + +import static org.hamcrest.CoreMatchers.*; +import static org.junit.Assert.*; + +import org.junit.Test; + +/** + * Unit tests for {@link Polygon}. + * + * @author Oliver Gierke + * @author Thomas Darimont + */ +public class PolygonUnitTests { + + Point first = new Point(1, 1); + Point second = new Point(2, 2); + Point third = new Point(3, 3); + + /** + * @see DATACMNS-437 + */ + @Test(expected = IllegalArgumentException.class) + public void rejectsNullPoints() { + new Polygon(null, null, null); + } + + /** + * @see DATACMNS-437 + */ + @Test + public void createsSimplePolygon() { + + Polygon polygon = new Polygon(third, second, first); + + assertThat(polygon, is(notNullValue())); + } + + /** + * @see DATACMNS-437 + */ + @Test + public void isEqualForSamePoints() { + + Polygon left = new Polygon(third, second, first); + Polygon right = new Polygon(third, second, first); + + assertThat(left, is(right)); + assertThat(right, is(left)); + } +}