From dcf8e913c8fd10434b97870e828c54a0229edd66 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Wed, 1 Jul 2015 20:49:47 +0200 Subject: [PATCH] Polishing Geo-API #86 - Use GeoWithin type for georadius - Use GeoEncoded type for geoencode/geodecode - Rename GeoTuple to GeoCoordinates to reflect the purpose - API documentation - Test against redis unstable branch Motivation: The interface containing List of Objects or List of Lists of Objects makes it hard to use the Geo API, especially when the coordinates are provided as strings. GeoWithin and GeoEncoded reflect the data structures returned by Redis and allow operations in a convenient way. --- Makefile | 4 +- .../java/com/lambdaworks/redis/GeoArgs.java | 74 ++++++++++- .../com/lambdaworks/redis/GeoCoordinates.java | 45 +++++++ .../com/lambdaworks/redis/GeoEncoded.java | 67 ++++++++++ .../java/com/lambdaworks/redis/GeoTuple.java | 31 ----- .../java/com/lambdaworks/redis/GeoWithin.java | 67 ++++++++++ .../redis/RedisAsyncConnectionImpl.java | 15 +-- .../redis/RedisCommandBuilder.java | 38 +++--- .../redis/RedisGeoAsyncConnection.java | 118 +++++++++--------- .../lambdaworks/redis/RedisGeoConnection.java | 117 ++++++++--------- .../output/GeoCoordinatesListOutput.java | 47 +++++++ .../redis/output/GeoEncodedOutput.java | 73 +++++++++++ .../redis/output/GeoWithinListOutput.java | 95 ++++++++++++++ .../output/NumericAndGeoTupleListOutput.java | 76 ----------- .../com/lambdaworks/redis/GeoCommandTest.java | 91 +++++++++----- 15 files changed, 674 insertions(+), 284 deletions(-) create mode 100644 src/main/java/com/lambdaworks/redis/GeoCoordinates.java create mode 100644 src/main/java/com/lambdaworks/redis/GeoEncoded.java delete mode 100644 src/main/java/com/lambdaworks/redis/GeoTuple.java create mode 100644 src/main/java/com/lambdaworks/redis/GeoWithin.java create mode 100644 src/main/java/com/lambdaworks/redis/output/GeoCoordinatesListOutput.java create mode 100644 src/main/java/com/lambdaworks/redis/output/GeoEncodedOutput.java create mode 100644 src/main/java/com/lambdaworks/redis/output/GeoWithinListOutput.java delete mode 100644 src/main/java/com/lambdaworks/redis/output/NumericAndGeoTupleListOutput.java diff --git a/Makefile b/Makefile index 809c2f2f18..a2e5138e91 100644 --- a/Makefile +++ b/Makefile @@ -377,8 +377,8 @@ endif endif endif - [ ! -e work/redis-git ] && git clone https://github.com/antirez/redis.git --branch geo --single-branch work/redis-git && cd work/redis-git|| true - [ -e work/redis-git ] && cd work/redis-git && git reset --hard && git pull && git checkout geo || true + [ ! -e work/redis-git ] && git clone https://github.com/antirez/redis.git --branch unstable --single-branch work/redis-git && cd work/redis-git|| true + [ -e work/redis-git ] && cd work/redis-git && git reset --hard && git pull && git checkout unstable || true make -C work/redis-git clean make -C work/redis-git -j4 diff --git a/src/main/java/com/lambdaworks/redis/GeoArgs.java b/src/main/java/com/lambdaworks/redis/GeoArgs.java index 0fda2de987..b751843dd3 100644 --- a/src/main/java/com/lambdaworks/redis/GeoArgs.java +++ b/src/main/java/com/lambdaworks/redis/GeoArgs.java @@ -1,5 +1,7 @@ package com.lambdaworks.redis; +import static com.google.common.base.Preconditions.checkArgument; + import com.lambdaworks.redis.protocol.CommandArgs; import com.lambdaworks.redis.protocol.CommandKeyword; @@ -16,30 +18,76 @@ public class GeoArgs { private Long count; private Sort sort = Sort.none; + /** + * Request distance for results. + * + * @return {@code this} + */ public GeoArgs withDistance() { withdistance = true; return this; } + /** + * Request coordinates for results. + * + * @return {@code this} + */ public GeoArgs withCoordinates() { withcoordinates = true; return this; } + /** + * Request geohash for results. + * + * @return {@code this} + */ public GeoArgs withHash() { withhash = true; return this; } + /** + * Limit results to {@code count} entries. + * + * @param count number greater 0 + * @return {@code this} + */ public GeoArgs withCount(long count) { + checkArgument(count > 0, "count must be greater 0"); this.count = count; return this; } + /** + * + * @return {@literal true} if distance is requested. + */ + public boolean isWithDistance() { + return withdistance; + } + + /** + * + * @return {@literal true} if coordinates are requested. + */ + public boolean isWithCoordinates() { + return withcoordinates; + } + + /** + * + * @return {@literal true} if geohash is requested. + */ + public boolean isWithHash() { + return withhash; + } + /** * Sort results ascending. * - * @return the current geo args. + * @return {@code this} */ public GeoArgs asc() { return sort(Sort.asc); @@ -48,7 +96,7 @@ public GeoArgs asc() { /** * Sort results descending. * - * @return the current geo args. + * @return {@code this} */ public GeoArgs desc() { return sort(Sort.desc); @@ -57,10 +105,12 @@ public GeoArgs desc() { /** * Sort results. * - * @param sort - * @return the current geo args. + * @param sort sort order, must not be {@literal null} + * @return {@code this} */ public GeoArgs sort(Sort sort) { + checkArgument(sort != null, "sort must not be null"); + this.sort = sort; return this; } @@ -69,7 +119,20 @@ public GeoArgs sort(Sort sort) { * Sort order. */ public enum Sort { - asc, desc, none; + /** + * ascending. + */ + asc, + + /** + * descending. + */ + desc, + + /** + * no sort order. + */ + none; } /** @@ -95,6 +158,7 @@ public enum Unit { mi; } + public void build(CommandArgs args) { if (withdistance) { args.add("withdist"); diff --git a/src/main/java/com/lambdaworks/redis/GeoCoordinates.java b/src/main/java/com/lambdaworks/redis/GeoCoordinates.java new file mode 100644 index 0000000000..ef3971de85 --- /dev/null +++ b/src/main/java/com/lambdaworks/redis/GeoCoordinates.java @@ -0,0 +1,45 @@ +package com.lambdaworks.redis; + +/** + * A tuple consisting of numerical geo data points to describe geo coordinates. + * + * @author Mark Paluch + */ +public class GeoCoordinates { + + public final Number x; + public final Number y; + + public GeoCoordinates(Number x, Number y) { + + this.x = x; + this.y = y; + } + + @Override + public boolean equals(Object o) { + if (this == o) + return true; + if (!(o instanceof GeoCoordinates)) + return false; + + GeoCoordinates geoCoords = (GeoCoordinates) o; + + if (x != null ? !x.equals(geoCoords.x) : geoCoords.x != null) + return false; + return !(y != null ? !y.equals(geoCoords.y) : geoCoords.y != null); + } + + @Override + public int hashCode() { + int result = x != null ? x.hashCode() : 0; + result = 31 * result + (y != null ? y.hashCode() : 0); + return result; + } + + @Override + public String toString() { + return String.format("(%f, %f)", x, y); + } + +} diff --git a/src/main/java/com/lambdaworks/redis/GeoEncoded.java b/src/main/java/com/lambdaworks/redis/GeoEncoded.java new file mode 100644 index 0000000000..ad9e7f0cb7 --- /dev/null +++ b/src/main/java/com/lambdaworks/redis/GeoEncoded.java @@ -0,0 +1,67 @@ +package com.lambdaworks.redis; + +/** + * Geo-encoded object. Contains: + * + *
    + *
  • the 52-bit geohash integer for your longitude/latitude
  • + *
  • the minimum corner of your geohash {@link GeoCoordinates}
  • + *
  • the maximum corner of your geohash {@link GeoCoordinates}
  • + *
  • 4: The averaged center of your geohash {@link GeoCoordinates}
  • + *
+ * + * @author Mark Paluch + */ +public class GeoEncoded { + + public final long geohash; + public final GeoCoordinates min; + public final GeoCoordinates max; + public final GeoCoordinates avg; + + public GeoEncoded(long geohash, GeoCoordinates min, GeoCoordinates max, GeoCoordinates avg) { + this.geohash = geohash; + this.min = min; + this.max = max; + this.avg = avg; + } + + @Override + public boolean equals(Object o) { + if (this == o) + return true; + if (!(o instanceof GeoEncoded)) + return false; + + GeoEncoded that = (GeoEncoded) o; + + if (geohash != that.geohash) + return false; + if (min != null ? !min.equals(that.min) : that.min != null) + return false; + if (max != null ? !max.equals(that.max) : that.max != null) + return false; + return !(avg != null ? !avg.equals(that.avg) : that.avg != null); + } + + @Override + public int hashCode() { + int result = (int) (geohash ^ (geohash >>> 32)); + result = 31 * result + (min != null ? min.hashCode() : 0); + result = 31 * result + (max != null ? max.hashCode() : 0); + result = 31 * result + (avg != null ? avg.hashCode() : 0); + return result; + } + + @Override + public String toString() { + final StringBuffer sb = new StringBuffer(); + sb.append(getClass().getSimpleName()); + sb.append(" [geohash=").append(geohash); + sb.append(", min=").append(min); + sb.append(", max=").append(max); + sb.append(", avg=").append(avg); + sb.append(']'); + return sb.toString(); + } +} diff --git a/src/main/java/com/lambdaworks/redis/GeoTuple.java b/src/main/java/com/lambdaworks/redis/GeoTuple.java deleted file mode 100644 index 2cba6549cb..0000000000 --- a/src/main/java/com/lambdaworks/redis/GeoTuple.java +++ /dev/null @@ -1,31 +0,0 @@ -package com.lambdaworks.redis; - -/** - * A tuple consisting of two numerical geo data points. - * - * @author Mark Paluch - */ -public class GeoTuple { - - private Number x; - private Number y; - - public GeoTuple() { - } - - public Number getX() { - return x; - } - - public void setX(Number x) { - this.x = x; - } - - public Number getY() { - return y; - } - - public void setY(Number y) { - this.y = y; - } -} diff --git a/src/main/java/com/lambdaworks/redis/GeoWithin.java b/src/main/java/com/lambdaworks/redis/GeoWithin.java new file mode 100644 index 0000000000..ecbb0cc378 --- /dev/null +++ b/src/main/java/com/lambdaworks/redis/GeoWithin.java @@ -0,0 +1,67 @@ +package com.lambdaworks.redis; + +/** + * Geo element within a certain radius. Contains: + *
    + *
  • the member
  • + *
  • the distance from the reference point (if requested)
  • + *
  • the geohash (if requested)
  • + *
  • the coordinates (if requested)
  • + *
+ * + * @param Value type. + * @author Mark Paluch + */ +public class GeoWithin { + + public final V member; + public final Double distance; + public final Long geohash; + public final GeoCoordinates coordinates; + + public GeoWithin(V member, Double distance, Long geohash, GeoCoordinates coordinates) { + this.member = member; + this.distance = distance; + this.geohash = geohash; + this.coordinates = coordinates; + } + + @Override + public boolean equals(Object o) { + if (this == o) + return true; + if (!(o instanceof GeoWithin)) + return false; + + GeoWithin geoWithin = (GeoWithin) o; + + if (member != null ? !member.equals(geoWithin.member) : geoWithin.member != null) + return false; + if (distance != null ? !distance.equals(geoWithin.distance) : geoWithin.distance != null) + return false; + if (geohash != null ? !geohash.equals(geoWithin.geohash) : geoWithin.geohash != null) + return false; + return !(coordinates != null ? !coordinates.equals(geoWithin.coordinates) : geoWithin.coordinates != null); + } + + @Override + public int hashCode() { + int result = member != null ? member.hashCode() : 0; + result = 31 * result + (distance != null ? distance.hashCode() : 0); + result = 31 * result + (geohash != null ? geohash.hashCode() : 0); + result = 31 * result + (coordinates != null ? coordinates.hashCode() : 0); + return result; + } + + @Override + public String toString() { + final StringBuffer sb = new StringBuffer(); + sb.append(getClass().getSimpleName()); + sb.append(" [member=").append(member); + sb.append(", distance=").append(distance); + sb.append(", geohash=").append(geohash); + sb.append(", coordinates=").append(coordinates); + sb.append(']'); + return sb.toString(); + } +} diff --git a/src/main/java/com/lambdaworks/redis/RedisAsyncConnectionImpl.java b/src/main/java/com/lambdaworks/redis/RedisAsyncConnectionImpl.java index 9a75940d6f..973a085bf6 100644 --- a/src/main/java/com/lambdaworks/redis/RedisAsyncConnectionImpl.java +++ b/src/main/java/com/lambdaworks/redis/RedisAsyncConnectionImpl.java @@ -1616,8 +1616,8 @@ public RedisFuture> georadius(K key, double longitude, double latitude, d } @Override - public RedisFuture> georadius(K key, double longitude, double latitude, double distance, GeoArgs.Unit unit, - GeoArgs geoArgs) { + public RedisFuture>> georadius(K key, double longitude, double latitude, double distance, + GeoArgs.Unit unit, GeoArgs geoArgs) { return dispatch(commandBuilder.georadius(key, longitude, latitude, distance, unit.name(), geoArgs)); } @@ -1627,12 +1627,13 @@ public RedisFuture> georadiusbymember(K key, V member, double distance, G } @Override - public RedisFuture> georadiusbymember(K key, V member, double distance, GeoArgs.Unit unit, GeoArgs geoArgs) { + public RedisFuture>> georadiusbymember(K key, V member, double distance, GeoArgs.Unit unit, + GeoArgs geoArgs) { return dispatch(commandBuilder.georadiusbymember(key, member, distance, unit.name(), geoArgs)); } @Override - public RedisFuture> geopos(K key, V... members) { + public RedisFuture> geopos(K key, V... members) { return dispatch(commandBuilder.geopos(key, members)); } @@ -1642,17 +1643,17 @@ public RedisFuture geodist(K key, V from, V to, GeoArgs.Unit unit) { } @Override - public RedisFuture> geoencode(double longitude, double latitude) { + public RedisFuture geoencode(double longitude, double latitude) { return dispatch(commandBuilder.geoencode(longitude, latitude, null, null)); } @Override - public RedisFuture> geoencode(double longitude, double latitude, double distance, GeoArgs.Unit unit) { + public RedisFuture geoencode(double longitude, double latitude, double distance, GeoArgs.Unit unit) { return dispatch(commandBuilder.geoencode(longitude, latitude, distance, unit.name())); } @Override - public RedisFuture> geodecode(long geohash) { + public RedisFuture geodecode(long geohash) { return dispatch(commandBuilder.geodecode(geohash)); } diff --git a/src/main/java/com/lambdaworks/redis/RedisCommandBuilder.java b/src/main/java/com/lambdaworks/redis/RedisCommandBuilder.java index 3fa809e08c..501ab15764 100644 --- a/src/main/java/com/lambdaworks/redis/RedisCommandBuilder.java +++ b/src/main/java/com/lambdaworks/redis/RedisCommandBuilder.java @@ -62,7 +62,9 @@ import com.lambdaworks.redis.output.ByteArrayOutput; import com.lambdaworks.redis.output.DateOutput; import com.lambdaworks.redis.output.DoubleOutput; -import com.lambdaworks.redis.output.NumericAndGeoTupleListOutput; +import com.lambdaworks.redis.output.GeoCoordinatesListOutput; +import com.lambdaworks.redis.output.GeoEncodedOutput; +import com.lambdaworks.redis.output.GeoWithinListOutput; import com.lambdaworks.redis.output.IntegerOutput; import com.lambdaworks.redis.output.KeyListOutput; import com.lambdaworks.redis.output.KeyOutput; @@ -1751,16 +1753,16 @@ public Command> georadius(K key, double longitude, double latitude, return createCommand(GEORADIUS, new ValueSetOutput(codec), args); } - public Command> georadius(K key, double longitude, double latitude, double distance, String unit, + public Command>> georadius(K key, double longitude, double latitude, double distance, String unit, GeoArgs geoArgs) { + assertNotNull(geoArgs, "geoArgs must not be null"); CommandArgs args = new CommandArgs(codec).addKey(key).add(longitude).add(latitude).add(distance).add(unit); - if (geoArgs != null) { - geoArgs.build(args); - } + geoArgs.build(args); - return createCommand(GEORADIUS, new NestedMultiOutput(codec), args); + return createCommand(GEORADIUS, new GeoWithinListOutput(codec, geoArgs.isWithDistance(), geoArgs.isWithHash(), + geoArgs.isWithCoordinates()), args); } public Command> georadiusbymember(K key, V member, double distance, String unit) { @@ -1768,21 +1770,21 @@ public Command> georadiusbymember(K key, V member, double distance, return createCommand(GEORADIUSBYMEMBER, new ValueSetOutput(codec), args); } - public Command> georadiusbymember(K key, V member, double distance, String unit, GeoArgs geoArgs) { + public Command>> georadiusbymember(K key, V member, double distance, String unit, GeoArgs geoArgs) { CommandArgs args = new CommandArgs(codec).addKey(key).addValue(member).add(distance).add(unit); + assertNotNull(geoArgs, "geoArgs must not be null"); - if (geoArgs != null) { - geoArgs.build(args); - } - - return createCommand(GEORADIUSBYMEMBER, new NestedMultiOutput(codec), args); + return createCommand( + GEORADIUSBYMEMBER, + new GeoWithinListOutput(codec, geoArgs.isWithDistance(), geoArgs.isWithHash(), geoArgs + .isWithCoordinates()), args); } @SuppressWarnings({ "unchecked", "rawtypes" }) - public Command> geopos(K key, V[] members) { + public Command> geopos(K key, V[] members) { CommandArgs args = new CommandArgs(codec).addKey(key).addValues(members); - return (Command) createCommand(GEOPOS, new NumericAndGeoTupleListOutput(codec), args); + return (Command) createCommand(GEOPOS, new GeoCoordinatesListOutput(codec), args); } public Command geodist(K key, V from, V to, GeoArgs.Unit unit) { @@ -1797,21 +1799,21 @@ public Command geodist(K key, V from, V to, GeoArgs.Unit unit) { } @SuppressWarnings({ "unchecked", "rawtypes" }) - public Command> geoencode(double longitude, double latitude, Double distance, String unit) { + public Command geoencode(double longitude, double latitude, Double distance, String unit) { CommandArgs args = new CommandArgs(codec).add(longitude).add(latitude); if (distance != null && unit != null) { args.add(distance).add(unit); } - return (Command) createCommand(GEOENCODE, new NumericAndGeoTupleListOutput(codec), args); + return (Command) createCommand(GEOENCODE, new GeoEncodedOutput(codec, null), args); } @SuppressWarnings({ "unchecked", "rawtypes" }) - public Command> geodecode(long geohash) { + public Command geodecode(long geohash) { CommandArgs args = new CommandArgs(codec).add(geohash); - return (Command) createCommand(GEODECODE, new NumericAndGeoTupleListOutput(codec), args); + return (Command) createCommand(GEODECODE, new GeoEncodedOutput(codec, geohash), args); } /** diff --git a/src/main/java/com/lambdaworks/redis/RedisGeoAsyncConnection.java b/src/main/java/com/lambdaworks/redis/RedisGeoAsyncConnection.java index 36a07318ce..96f2dbf843 100644 --- a/src/main/java/com/lambdaworks/redis/RedisGeoAsyncConnection.java +++ b/src/main/java/com/lambdaworks/redis/RedisGeoAsyncConnection.java @@ -14,10 +14,10 @@ public interface RedisGeoAsyncConnection { /** * Single geo add. * - * @param key - * @param longitude - * @param latitude - * @param member + * @param key the key of the geo set + * @param longitude the longitude coordinate according to WGS84 + * @param latitude the latitude coordinate according to WGS84 + * @param member the member to add * @return Long integer-reply the number of elements that were added to the set */ RedisFuture geoadd(K key, double longitude, double latitude, V member); @@ -25,7 +25,7 @@ public interface RedisGeoAsyncConnection { /** * Multi geo add. * - * @param key + * @param key the key of the geo set * @param lngLatMember triplets of double longitude, double latitude and V member * @return Long integer-reply the number of elements that were added to the set */ @@ -34,11 +34,11 @@ public interface RedisGeoAsyncConnection { /** * Retrieve members selected by distance with the center of {@code longitude} and {@code latitude}. * - * @param key - * @param longitude - * @param latitude - * @param distance - * @param unit + * @param key the key of the geo set + * @param longitude the longitude coordinate according to WGS84 + * @param latitude the latitude coordinate according to WGS84 + * @param distance radius distance + * @param unit distance unit * @return bulk reply */ RedisFuture> georadius(K key, double longitude, double latitude, double distance, GeoArgs.Unit unit); @@ -46,58 +46,63 @@ public interface RedisGeoAsyncConnection { /** * Retrieve members selected by distance with the center of {@code longitude} and {@code latitude}. * - * @param key - * @param longitude - * @param latitude - * @param distance - * @param unit - * @return nested multi-bulk reply + * @param key the key of the geo set + * @param longitude the longitude coordinate according to WGS84 + * @param latitude the latitude coordinate according to WGS84 + * @param distance radius distance + * @param unit distance unit + * @param geoArgs args to control the result + * @return nested multi-bulk reply. The {@link GeoWithin} contains only fields which were requested by {@link GeoArgs} */ - RedisFuture> georadius(K key, double longitude, double latitude, double distance, GeoArgs.Unit unit, + RedisFuture>> georadius(K key, double longitude, double latitude, double distance, GeoArgs.Unit unit, GeoArgs geoArgs); /** - * Retrieve members selected by distance with the center of {@code member}. + * Retrieve members selected by distance with the center of {@code member}. The member itself is always contained in the + * results. * - * @param key - * @param member - * @param distance - * @param unit - * @return bulk reply + * @param key the key of the geo set + * @param member reference member + * @param distance radius distance + * @param unit distance unit + * @return set of members */ RedisFuture> georadiusbymember(K key, V member, double distance, GeoArgs.Unit unit); /** * - * Retrieve members selected by distance with the center of {@code member}. + * Retrieve members selected by distance with the center of {@code member}. The member itself is always contained in the + * results. * - * @param key - * @param member - * @param distance - * @param unit - * @return nested multi-bulk reply + * @param key the key of the geo set + * @param member reference member + * @param distance radius distance + * @param unit distance unit + * @param geoArgs args to control the result + * @return nested multi-bulk reply. The {@link GeoWithin} contains only fields which were requested by {@link GeoArgs} */ - RedisFuture> georadiusbymember(K key, V member, double distance, GeoArgs.Unit unit, GeoArgs geoArgs); + RedisFuture>> georadiusbymember(K key, V member, double distance, GeoArgs.Unit unit, GeoArgs geoArgs); /** * Get geo coordinates for the {@code members}. * - * @param key - * @param members + * @param key the key of the geo set + * @param members the members * - * @return a list of {@link GeoTuple}s representing the x,y position of each element specified in the arguments. For missing - * elements {@literal null} is returned. + * @return a list of {@link GeoCoordinates}s representing the x,y position of each element specified in the arguments. For + * missing elements {@literal null} is returned. */ - RedisFuture> geopos(K key, V... members); + RedisFuture> geopos(K key, V... members); /** * * Retrieve distance between points {@code from} and {@code to}. If one or more elements are missing {@literal null} is * returned. Default in meters by , otherwise according to {@code unit} * - * @param key - * @param from - * @param to + * @param key the key of the geo set + * @param from from member + * @param to to member + * @param unit distance unit * * @return distance between points {@code from} and {@code to}. If one or more elements are missing {@literal null} is * returned. @@ -108,36 +113,35 @@ RedisFuture> georadius(K key, double longitude, double latitude, do * * Encode {@code longitude} and {@code latitude} to highest geohash accuracy. * - * @param longitude - * @param latitude - * @return multi-bulk reply with 4 elements 1: the 52-bit geohash integer for your latitude longitude, 2: The minimum corner - * of your geohash {@link GeoTuple}, 3: The maximum corner of your geohash {@link GeoTuple}, 4: The averaged center - * of your geohash {@link GeoTuple}. + * @param longitude the longitude coordinate according to WGS84 + * @param latitude the latitude coordinate according to WGS84 + * @return multi-bulk reply with 4 elements 1: the 52-bit geohash integer for your longitude/latitude, 2: The minimum corner + * of your geohash {@link GeoCoordinates}, 3: The maximum corner of your geohash {@link GeoCoordinates}, 4: The + * averaged center of your geohash {@link GeoCoordinates}. */ - RedisFuture> geoencode(double longitude, double latitude); + RedisFuture geoencode(double longitude, double latitude); /** * * Encode {@code longitude} and {@code latitude} to highest geohash accuracy. * - * @param longitude - * @param latitude - * @param distance - * @param unit - * @return multi-bulk reply with four components 1: the 52-bit geohash integer for your latitude longitude, 2: The minimum - * corner of your geohash {@link GeoTuple}, 3: The maximum corner of your geohash {@link GeoTuple}, 4: The averaged - * center of your geohash {@link GeoTuple}. + * @param longitude the longitude coordinate according to WGS84 + * @param latitude the latitude coordinate according to WGS84 + * @param distance distance for geohash accuracy + * @param unit the distance unit + * @return multi-bulk reply with four components 1: the 52-bit geohash integer for your longitude/latitude, 2: The minimum + * corner of your geohash {@link GeoCoordinates}, 3: The maximum corner of your geohash {@link GeoCoordinates}, 4: + * The averaged center of your geohash {@link GeoCoordinates}. */ - RedisFuture> geoencode(double longitude, double latitude, double distance, GeoArgs.Unit unit); + RedisFuture geoencode(double longitude, double latitude, double distance, GeoArgs.Unit unit); /** * * Decode geohash. * - * @param geohash - * @return a list of {@link GeoTuple}s (nested multi-bulk) with 3 elements 1: minimum decoded corner, 2: maximum decoded - * corner, 3: averaged center of bounding box. + * @param geohash geohash containing your longitude/latitude + * @return a list of {@link GeoCoordinates}s (nested multi-bulk) with 3 elements 1: minimum decoded corner, 2: maximum + * decoded corner, 3: averaged center of bounding box. */ - RedisFuture> geodecode(long geohash); - + RedisFuture geodecode(long geohash); } diff --git a/src/main/java/com/lambdaworks/redis/RedisGeoConnection.java b/src/main/java/com/lambdaworks/redis/RedisGeoConnection.java index f8d852d292..f63c7726db 100644 --- a/src/main/java/com/lambdaworks/redis/RedisGeoConnection.java +++ b/src/main/java/com/lambdaworks/redis/RedisGeoConnection.java @@ -14,10 +14,10 @@ public interface RedisGeoConnection { /** * Single geo add. * - * @param key - * @param longitude - * @param latitude - * @param member + * @param key the key of the geo set + * @param longitude the longitude coordinate according to WGS84 + * @param latitude the latitude coordinate according to WGS84 + * @param member the member to add * @return Long integer-reply the number of elements that were added to the set */ Long geoadd(K key, double longitude, double latitude, V member); @@ -25,7 +25,7 @@ public interface RedisGeoConnection { /** * Multi geo add. * - * @param key + * @param key the key of the geo set * @param lngLatMember triplets of double longitude, double latitude and V member * @return Long integer-reply the number of elements that were added to the set */ @@ -34,11 +34,11 @@ public interface RedisGeoConnection { /** * Retrieve members selected by distance with the center of {@code longitude} and {@code latitude}. * - * @param key - * @param longitude - * @param latitude - * @param distance - * @param unit + * @param key the key of the geo set + * @param longitude the longitude coordinate according to WGS84 + * @param latitude the latitude coordinate according to WGS84 + * @param distance radius distance + * @param unit distance unit * @return bulk reply */ Set georadius(K key, double longitude, double latitude, double distance, GeoArgs.Unit unit); @@ -46,57 +46,62 @@ public interface RedisGeoConnection { /** * Retrieve members selected by distance with the center of {@code longitude} and {@code latitude}. * - * @param key - * @param longitude - * @param latitude - * @param distance - * @param unit - * @return nested multi-bulk reply + * @param key the key of the geo set + * @param longitude the longitude coordinate according to WGS84 + * @param latitude the latitude coordinate according to WGS84 + * @param distance radius distance + * @param unit distance unit + * @param geoArgs args to control the result + * @return nested multi-bulk reply. The {@link GeoWithin} contains only fields which were requested by {@link GeoArgs} */ - List georadius(K key, double longitude, double latitude, double distance, GeoArgs.Unit unit, GeoArgs geoArgs); + List> georadius(K key, double longitude, double latitude, double distance, GeoArgs.Unit unit, GeoArgs geoArgs); /** - * Retrieve members selected by distance with the center of {@code member}. + * Retrieve members selected by distance with the center of {@code member}. The member itself is always contained in the + * results. * - * @param key - * @param member - * @param distance - * @param unit - * @return bulk reply + * @param key the key of the geo set + * @param member reference member + * @param distance radius distance + * @param unit distance unit + * @return set of members */ Set georadiusbymember(K key, V member, double distance, GeoArgs.Unit unit); /** * - * Retrieve members selected by distance with the center of {@code member}. + * Retrieve members selected by distance with the center of {@code member}. The member itself is always contained in the + * results. * - * @param key - * @param member - * @param distance - * @param unit - * @return nested multi-bulk reply + * @param key the key of the geo set + * @param member reference member + * @param distance radius distance + * @param unit distance unit + * @param geoArgs args to control the result + * @return nested multi-bulk reply. The {@link GeoWithin} contains only fields which were requested by {@link GeoArgs} */ - List georadiusbymember(K key, V member, double distance, GeoArgs.Unit unit, GeoArgs geoArgs); + List> georadiusbymember(K key, V member, double distance, GeoArgs.Unit unit, GeoArgs geoArgs); /** * Get geo coordinates for the {@code members}. * - * @param key - * @param members + * @param key the key of the geo set + * @param members the members * - * @return a list of {@link GeoTuple}s representing the x,y position of each element specified in the arguments. For missing - * elements {@literal null} is returned. + * @return a list of {@link GeoCoordinates}s representing the x,y position of each element specified in the arguments. For + * missing elements {@literal null} is returned. */ - List geopos(K key, V... members); + List geopos(K key, V... members); /** * * Retrieve distance between points {@code from} and {@code to}. If one or more elements are missing {@literal null} is * returned. Default in meters by, otherwise according to {@code unit} * - * @param key - * @param from - * @param to + * @param key the key of the geo set + * @param from from member + * @param to to member + * @param unit distance unit * * @return distance between points {@code from} and {@code to}. If one or more elements are missing {@literal null} is * returned. @@ -107,35 +112,35 @@ public interface RedisGeoConnection { * * Encode {@code longitude} and {@code latitude} to highest geohash accuracy. * - * @param longitude - * @param latitude - * @return multi-bulk reply with 4 elements 1: the 52-bit geohash integer for your latitude longitude, 2: The minimum corner - * of your geohash {@link GeoTuple}, 3: The maximum corner of your geohash {@link GeoTuple}, 4: The averaged center - * of your geohash {@link GeoTuple}. + * @param longitude the longitude coordinate according to WGS84 + * @param latitude the latitude coordinate according to WGS84 + * @return multi-bulk reply with 4 elements 1: the 52-bit geohash integer for your longitude/latitude, 2: The minimum corner + * of your geohash {@link GeoCoordinates}, 3: The maximum corner of your geohash {@link GeoCoordinates}, 4: The + * averaged center of your geohash {@link GeoCoordinates}. */ - List geoencode(double longitude, double latitude); + GeoEncoded geoencode(double longitude, double latitude); /** * * Encode {@code longitude} and {@code latitude} to highest geohash accuracy. * - * @param longitude - * @param latitude - * @param distance - * @param unit - * @return multi-bulk reply with four components 1: the 52-bit geohash integer for your latitude longitude, 2: The minimum - * corner of your geohash {@link GeoTuple}, 3: The maximum corner of your geohash {@link GeoTuple}, 4: The averaged - * center of your geohash {@link GeoTuple}. + * @param longitude the longitude coordinate according to WGS84 + * @param latitude the latitude coordinate according to WGS84 + * @param distance distance for geohash accuracy + * @param unit the distance unit + * @return multi-bulk reply with four components 1: the 52-bit geohash integer for your longitude/latitude, 2: The minimum + * corner of your geohash {@link GeoCoordinates}, 3: The maximum corner of your geohash {@link GeoCoordinates}, 4: + * The averaged center of your geohash {@link GeoCoordinates}. */ - List geoencode(double longitude, double latitude, double distance, GeoArgs.Unit unit); + GeoEncoded geoencode(double longitude, double latitude, double distance, GeoArgs.Unit unit); /** * * Decode geohash. * - * @param geohash - * @return a list of {@link GeoTuple}s (nested multi-bulk) with 3 elements 1: minimum decoded corner, 2: maximum decoded - * corner, 3: averaged center of bounding box. + * @param geohash geohash containing your longitude/latitude + * @return a list of {@link GeoCoordinates}s (nested multi-bulk) with 3 elements 1: minimum decoded corner, 2: maximum + * decoded corner, 3: averaged center of bounding box. */ - List geodecode(long geohash); + GeoEncoded geodecode(long geohash); } diff --git a/src/main/java/com/lambdaworks/redis/output/GeoCoordinatesListOutput.java b/src/main/java/com/lambdaworks/redis/output/GeoCoordinatesListOutput.java new file mode 100644 index 0000000000..8f0843df37 --- /dev/null +++ b/src/main/java/com/lambdaworks/redis/output/GeoCoordinatesListOutput.java @@ -0,0 +1,47 @@ +package com.lambdaworks.redis.output; + +import static java.lang.Double.parseDouble; + +import java.nio.ByteBuffer; +import java.util.List; + +import com.google.common.collect.Lists; +import com.lambdaworks.redis.GeoCoordinates; +import com.lambdaworks.redis.codec.RedisCodec; +import com.lambdaworks.redis.protocol.CommandOutput; + +/** + * A list output that creates a list with {@link GeoCoordinates}'s. + * + * @author Mark Paluch + */ +public class GeoCoordinatesListOutput extends CommandOutput> { + + private Double x; + + public GeoCoordinatesListOutput(RedisCodec codec) { + super(codec, null); + } + + @Override + public void set(ByteBuffer bytes) { + Double value = (bytes == null) ? 0 : parseDouble(decodeAscii(bytes)); + + if (x == null) { + x = value; + return; + } + + output.add(new GeoCoordinates(x, value)); + x = null; + } + + @Override + public void multi(int count) { + if (output == null) { + output = Lists.newArrayListWithCapacity(count); + } else if (count == -1) { + output.add(null); + } + } +} diff --git a/src/main/java/com/lambdaworks/redis/output/GeoEncodedOutput.java b/src/main/java/com/lambdaworks/redis/output/GeoEncodedOutput.java new file mode 100644 index 0000000000..f029796066 --- /dev/null +++ b/src/main/java/com/lambdaworks/redis/output/GeoEncodedOutput.java @@ -0,0 +1,73 @@ +package com.lambdaworks.redis.output; + +import static java.lang.Double.parseDouble; + +import java.nio.ByteBuffer; + +import com.lambdaworks.redis.GeoEncoded; +import com.lambdaworks.redis.GeoCoordinates; +import com.lambdaworks.redis.codec.RedisCodec; +import com.lambdaworks.redis.protocol.CommandOutput; + +/** + * Encodes the command output to retrieve a {@link GeoEncoded} object. + * + * @author Mark Paluch + */ +public class GeoEncodedOutput extends CommandOutput { + + private Long geohash; + private GeoCoordinates min; + private GeoCoordinates max; + private GeoCoordinates avg; + + private Double x; + + public GeoEncodedOutput(RedisCodec codec, Long geohash) { + super(codec, null); + this.geohash = geohash; + } + + @Override + public void set(long integer) { + if (geohash == null) { + geohash = integer; + } + } + + @Override + public void set(ByteBuffer bytes) { + Double value = (bytes == null) ? 0 : parseDouble(decodeAscii(bytes)); + + if (x == null) { + x = value; + return; + } + + try { + if (min == null) { + min = new GeoCoordinates(x, value); + return; + } + + if (max == null) { + max = new GeoCoordinates(x, value); + return; + } + + if (avg == null) { + avg = new GeoCoordinates(x, value); + return; + } + } finally { + x = null; + } + } + + @Override + public void complete(int depth) { + if (depth == 0) { + output = new GeoEncoded(geohash, min, max, avg); + } + } +} diff --git a/src/main/java/com/lambdaworks/redis/output/GeoWithinListOutput.java b/src/main/java/com/lambdaworks/redis/output/GeoWithinListOutput.java new file mode 100644 index 0000000000..860eb67d90 --- /dev/null +++ b/src/main/java/com/lambdaworks/redis/output/GeoWithinListOutput.java @@ -0,0 +1,95 @@ +package com.lambdaworks.redis.output; + +import static java.lang.Double.parseDouble; + +import java.nio.ByteBuffer; +import java.util.List; + +import com.google.common.collect.Lists; +import com.lambdaworks.redis.GeoCoordinates; +import com.lambdaworks.redis.GeoWithin; +import com.lambdaworks.redis.codec.RedisCodec; +import com.lambdaworks.redis.protocol.CommandOutput; + +/** + * A list output that creates a list with either double/long or {@link GeoCoordinates}'s. + * + * @author Mark Paluch + */ +public class GeoWithinListOutput extends CommandOutput>> { + + private V member; + private Double distance; + private Long geohash; + private GeoCoordinates coordinates; + + private Double x; + + private boolean withDistance; + private boolean withHash; + private boolean withCoordinates; + + public GeoWithinListOutput(RedisCodec codec, boolean withDistance, boolean withHash, boolean withCoordinates) { + super(codec, null); + this.withDistance = withDistance; + this.withHash = withHash; + this.withCoordinates = withCoordinates; + } + + @Override + public void set(long integer) { + if (member == null) { + member = (V) (Long) integer; + return; + } + + if (withHash) { + geohash = integer; + } + } + + @Override + public void set(ByteBuffer bytes) { + + if (member == null) { + member = codec.decodeValue(bytes); + return; + } + + Double value = (bytes == null) ? 0 : parseDouble(decodeAscii(bytes)); + if (withDistance) { + if (distance == null) { + distance = value; + return; + } + } + if (withCoordinates) { + if (x == null) { + x = value; + return; + } + + coordinates = new GeoCoordinates(x, value); + return; + } + } + + @Override + public void multi(int count) { + if (output == null) { + output = Lists.newArrayListWithCapacity(count); + } + } + + @Override + public void complete(int depth) { + if (depth == 1) { + output.add(new GeoWithin(member, distance, geohash, coordinates)); + + member = null; + distance = null; + geohash = null; + coordinates = null; + } + } +} diff --git a/src/main/java/com/lambdaworks/redis/output/NumericAndGeoTupleListOutput.java b/src/main/java/com/lambdaworks/redis/output/NumericAndGeoTupleListOutput.java deleted file mode 100644 index 61157ac9c5..0000000000 --- a/src/main/java/com/lambdaworks/redis/output/NumericAndGeoTupleListOutput.java +++ /dev/null @@ -1,76 +0,0 @@ -package com.lambdaworks.redis.output; - -import static java.lang.Double.parseDouble; - -import java.nio.ByteBuffer; -import java.util.List; - -import com.google.common.collect.Lists; -import com.lambdaworks.redis.GeoTuple; -import com.lambdaworks.redis.codec.RedisCodec; -import com.lambdaworks.redis.protocol.CommandOutput; - -/** - * A list output that creates a list with either double/long or {@link GeoTuple}'s. - * - * @author Mark Paluch - */ -public class NumericAndGeoTupleListOutput extends CommandOutput> { - - private GeoTuple geoTuple; - private Integer elementCount; - - public NumericAndGeoTupleListOutput(RedisCodec codec) { - super(codec, null); - } - - @Override - public void set(long integer) { - - if (geoTuple == null) { - output.add(integer); - return; - } - - if (geoTuple.getX() == null) { - geoTuple.setX(integer); - return; - } - - geoTuple.setY(integer); - output.add(geoTuple); - geoTuple = null; - } - - @Override - public void set(ByteBuffer bytes) { - Double value = (bytes == null) ? null : parseDouble(decodeAscii(bytes)); - - if (geoTuple == null) { - output.add(value); - return; - } - - if (geoTuple.getX() == null) { - geoTuple.setX(value); - return; - } - - geoTuple.setY(value); - output.add(geoTuple); - geoTuple = null; - } - - @Override - public void multi(int count) { - geoTuple = null; - if (elementCount == null) { - elementCount = count; - output = Lists.newArrayListWithCapacity(count); - } else if (count == 2) { - geoTuple = new GeoTuple(); - } else if (count == -1) { - output.add(null); - } - } -} diff --git a/src/test/java/com/lambdaworks/redis/GeoCommandTest.java b/src/test/java/com/lambdaworks/redis/GeoCommandTest.java index 448fd1922f..22ccc29de9 100644 --- a/src/test/java/com/lambdaworks/redis/GeoCommandTest.java +++ b/src/test/java/com/lambdaworks/redis/GeoCommandTest.java @@ -71,10 +71,10 @@ public void geopos() throws Exception { prepareGeo(); - List geopos = redis.geopos(key, "Weinheim", "foobar", "Bahn"); + List geopos = redis.geopos(key, "Weinheim", "foobar", "Bahn"); assertThat(geopos).hasSize(3); - assertThat(geopos.get(0).getX().doubleValue()).isEqualTo(8.6638, offset(0.001)); + assertThat(geopos.get(0).x.doubleValue()).isEqualTo(8.6638, offset(0.001)); assertThat(geopos.get(1)).isNull(); assertThat(geopos.get(2)).isNotNull(); } @@ -84,16 +84,35 @@ public void georadiusWithArgs() throws Exception { prepareGeo(); - GeoArgs geoArgs = new GeoArgs().withHash().withCoordinates().withDistance().withCount(1).asc(); + GeoArgs geoArgs = new GeoArgs().withHash().withCoordinates().withDistance().withCount(1).desc(); - List result = redis.georadius(key, 8.6582861, 49.5285695, 1, GeoArgs.Unit.km, geoArgs); + List> result = redis.georadius(key, 8.665351, 49.553302, 5, GeoArgs.Unit.km, geoArgs); assertThat(result).hasSize(1); - List response = (List) result.get(0); - assertThat(response).hasSize(4); + GeoWithin weinheim = result.get(0); - result = redis.georadius(key, 8.6582861, 49.5285695, 1, GeoArgs.Unit.km, null); + assertThat(weinheim.member).isEqualTo("Weinheim"); + assertThat(weinheim.geohash).isEqualTo(3666615932941099L); + + assertThat(weinheim.distance).isEqualTo(2.7882, offset(0.5)); + assertThat(weinheim.coordinates.x.doubleValue()).isEqualTo(8.663875, offset(0.5)); + assertThat(weinheim.coordinates.y.doubleValue()).isEqualTo(49.52825, offset(0.5)); + + result = redis.georadius(key, 8.665351, 49.553302, 1, GeoArgs.Unit.km, new GeoArgs()); assertThat(result).hasSize(1); + + GeoWithin bahn = result.get(0); + + assertThat(bahn.member).isEqualTo("Bahn"); + assertThat(bahn.geohash).isNull(); + + assertThat(bahn.distance).isNull(); + assertThat(bahn.coordinates).isNull(); + } + + @Test(expected = IllegalArgumentException.class) + public void georadiusWithNullArgs() throws Exception { + redis.georadius(key, 8.665351, 49.553302, 5, GeoArgs.Unit.km, null); } @Test @@ -115,55 +134,63 @@ public void georadiusbymemberWithArgs() throws Exception { GeoArgs geoArgs = new GeoArgs().withHash().withCoordinates().withDistance().desc(); - List empty = redis.georadiusbymember(key, "Bahn", 1, GeoArgs.Unit.km, geoArgs); + List> empty = redis.georadiusbymember(key, "Bahn", 1, GeoArgs.Unit.km, geoArgs); assertThat(empty).isNotEmpty(); - List georadiusbymember = redis.georadiusbymember(key, "Bahn", 5, GeoArgs.Unit.km, geoArgs); + List> georadiusbymember = redis.georadiusbymember(key, "Bahn", 5, GeoArgs.Unit.km, geoArgs); assertThat(georadiusbymember).hasSize(2); - List response = (List) georadiusbymember.get(0); - assertThat(response).hasSize(4); + GeoWithin weinheim = georadiusbymember.get(0); + assertThat(weinheim.member).isEqualTo("Weinheim"); + } + + @Test(expected = IllegalArgumentException.class) + public void georadiusbymemberWithNullArgs() throws Exception { + redis.georadiusbymember(key, "Bahn", 1, GeoArgs.Unit.km, null); } @Test public void geoencode() throws Exception { - List geoencode = redis.geoencode(8.6638775, 49.5282537); + GeoEncoded geoencoded = redis.geoencode(8.6638775, 49.5282537); + + assertThat(geoencoded.geohash).isEqualTo(3666615932941099L); + assertThat(geoencoded.min.x.doubleValue()).isEqualTo(8.6638730764389038, offset(1d)); + assertThat(geoencoded.min.y.doubleValue()).isEqualTo(49.528251210511513, offset(1d)); + + assertThat(geoencoded.max.x.doubleValue()).isEqualTo(8.6638784408569336, offset(1d)); + assertThat(geoencoded.max.y.doubleValue()).isEqualTo(49.528253745232675, offset(1d)); - assertThat(geoencode).hasSize(5); - assertThat(geoencode.get(0)).isEqualTo(3666615932941099L); - assertThat(geoencode.get(1)).isInstanceOf(GeoTuple.class); - assertThat(geoencode.get(2)).isInstanceOf(GeoTuple.class); - assertThat(geoencode.get(3)).isInstanceOf(GeoTuple.class); + assertThat(geoencoded.avg.x.doubleValue()).isEqualTo(8.6638757586479187, offset(1d)); + assertThat(geoencoded.avg.y.doubleValue()).isEqualTo(49.528252477872094, offset(1d)); } @Test public void geoencodeWithDistance() throws Exception { - List result = redis.geoencode(8.6638775, 49.5282537, 1, GeoArgs.Unit.km); + GeoEncoded geoencoded = redis.geoencode(8.6638775, 49.5282537, 1, GeoArgs.Unit.km); - assertThat(result).hasSize(5); - assertThat(result.get(0)).isEqualTo(3666615929405440L); - assertThat(result.get(1)).isInstanceOf(GeoTuple.class); - assertThat(result.get(2)).isInstanceOf(GeoTuple.class); - assertThat(result.get(3)).isInstanceOf(GeoTuple.class); + assertThat(geoencoded.geohash).isEqualTo(3666615929405440L); + assertThat(geoencoded.max).isNotNull(); + assertThat(geoencoded.min).isNotNull(); + assertThat(geoencoded.avg).isNotNull(); } @Test - public void geodecode() throws Exception { + public void geoencoded() throws Exception { - List result = redis.geodecode(3666615932941099L); + GeoEncoded geoencoded = redis.geodecode(3666615932941099L); - assertThat(result).hasSize(3); - assertThat(result.get(0).getX().doubleValue()).isEqualTo(8.6638730764389038, offset(1d)); - assertThat(result.get(0).getY().doubleValue()).isEqualTo(49.528251210511513, offset(1d)); + assertThat(geoencoded.geohash).isEqualTo(3666615932941099L); + assertThat(geoencoded.min.x.doubleValue()).isEqualTo(8.6638730764389038, offset(1d)); + assertThat(geoencoded.min.y.doubleValue()).isEqualTo(49.528251210511513, offset(1d)); - assertThat(result.get(1).getX().doubleValue()).isEqualTo(8.6638784408569336, offset(1d)); - assertThat(result.get(1).getY().doubleValue()).isEqualTo(49.528253745232675, offset(1d)); + assertThat(geoencoded.max.x.doubleValue()).isEqualTo(8.6638784408569336, offset(1d)); + assertThat(geoencoded.max.y.doubleValue()).isEqualTo(49.528253745232675, offset(1d)); - assertThat(result.get(2).getX().doubleValue()).isEqualTo(8.6638757586479187, offset(1d)); - assertThat(result.get(2).getY().doubleValue()).isEqualTo(49.528252477872094, offset(1d)); + assertThat(geoencoded.avg.x.doubleValue()).isEqualTo(8.6638757586479187, offset(1d)); + assertThat(geoencoded.avg.y.doubleValue()).isEqualTo(49.528252477872094, offset(1d)); } }