diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..b5eecee
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,5 @@
+target
+.idea
+*.iml
+*.jar
+*.class
diff --git a/README.md b/README.md
index 5c56261..a6dc113 100644
--- a/README.md
+++ b/README.md
@@ -1,2 +1,96 @@
gorgeo
======
+
+Provides a facet for grouping documents containing geolocation data based on
+the geohash cells. It groups the geographical points by using
+[geohash](http://en.wikipedia.org/wiki/Geohash) cells, according to the
+level provided by user.
+
+### Installation
+
+TODO
+
+### Example usage
+
+Query example:
+
+```json
+ "geohashcell": {
+ "field": "location",
+ "level" : 3,
+ "top_left": "11.21,51.33",
+ "bottom_right": "-20.21,80.65"
+}
+```
+
+
+
+Result example:
+
+```json
+ "facets": {
+ "f": {
+ "_type": "Z2VvaGFzaGNlbGw=",
+ "field": "location",
+ "top_left": "[11.21, 51.33]",
+ "bottom_right": "[-20.21, 80.65]",
+ "level": 4,
+ "base_map_level": 1,
+ "counts": {
+ "[8.876953125, 76.46484375]": "1",
+ "[6.240234375, 79.98046875]": "1",
+ "[6.943359375, 79.98046875]": "13",
+ "[9.755859375, 77.87109375]": "2",
+ "[2.197265625, 72.94921875]": "1",
+ "[9.931640625, 77.51953125]": "3",
+ ...
+ }
+ }
+```
+
+### Parameters
+
+| Parameter name | Optional | Default value | Description
+| ---------------- | :--------: | :---------------: | -------------
+| __field__ | _optional_ | "location" | Name of the document field containing geographical location.
+| __top_left__ | _optional_ | "90,-180" | Geographical coordinates (lattitude, longitude) of the top left point of the current map viewport.
+| __bottom_right__ | _optional_ | "-90,180" | Geographical coordinates (lattitude, longitude) of the bottom right point of the current map viewport.
+| __level__ | _optional_ | 3 | The grouping level (see [Algorithm](#Algorithm)).
+
+_Note_: the map viewport defaults to a full map.
+
+### Results
+
+ * __field__ - equal to the __field__ parameter
+ * __top_left__ - equal to the __top_left__ parameter
+ * __bottom_right__ - equal to the __bottom_right__ parameter
+ * __level__ - actual grouping level (see [Algorithm](#Algorithm))
+ * __base_map_level__ - base grouping level for the current map viewport
+ * __counts__ - grouping results, contain cell center and corresponding
+ document count
+
+### Algorithm
+
+The facet counts the documents based on geohash prefixes. For more information
+on how geohashing works, see the following links:
+
+ * [Wikipedia on Geohash](http://en.wikipedia.org/wiki/Geohash)
+ * [Visualizing Geohash](http://www.bigdatamodeling.org/2013/01/intuitive-geohash.html)
+
+The algorithm groups documents based on geohash prefixes of the stored
+geographical data. The __level__ parameter provided to the facet is used as
+a lenght of the geohash prefix for grouping, i.e. increasing the level by one,
+divides the map into 32 cells.
+
+Additionally the facet adapts to the current map viewport by comparing the
+current viewport size with sizes of the geohash cells and calculates the
+__base_map_level__. This level is than added to the level parameter and used
+as a geohash prefix. The result is always in [1,6] range.
+
+### To do
+ * better parameter checking
+ * more tests
+
+Credits
+=======
+Thanks to [Mahesh Paolini-Subramanya](https://github.com/dieswaytoofast) for his help.
diff --git a/assembly/release.xml b/assembly/release.xml
new file mode 100644
index 0000000..87aff4a
--- /dev/null
+++ b/assembly/release.xml
@@ -0,0 +1,29 @@
+
+
+ bin
+
+ zip
+
+ false
+
+
+ false
+ /
+ false
+ true
+
+ org.elasticsearch:elasticsearch
+ junit:junit
+
+
+
+
+
+ ${project.build.directory}/
+ /
+
+ elasticsearch-${project.name}-${elasticsearch.version}.jar
+
+
+
+
\ No newline at end of file
diff --git a/pom.xml b/pom.xml
new file mode 100644
index 0000000..382571b
--- /dev/null
+++ b/pom.xml
@@ -0,0 +1,122 @@
+
+ 4.0.0
+ org.elasticsearch.plugin.geohashcellfacet
+ GeoHashCellFacetPlugin
+ jar
+ 1.0
+ GeoHashCellFacetPlugin
+ http://maven.apache.org
+
+
+
+
+ org.apache.maven.plugins
+ maven-jar-plugin
+ 2.3
+
+
+ elasticsearch-${project.name}-${elasticsearch.version}
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-dependency-plugin
+ 2.2
+
+
+ copy-dependencies
+ package
+
+ copy-dependencies
+
+
+ ${project.build.directory}/lib
+
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-surefire-plugin
+ 2.8
+
+ true
+
+
+ ${project.build.directory}/classes/conf
+
+
+ ${project.build.directory}/lib
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-assembly-plugin
+ 2.2.1
+
+
+ elasticsearch-${project.name}-${elasticsearch.version}
+
+ false
+ ${project.build.directory}/release/
+
+
+ assembly/release.xml
+
+
+
+
+ generate-release-plugin
+ package
+
+ single
+
+
+
+
+
+
+
+ src/main/resources
+ true
+
+ **/*.properties
+
+
+
+
+
+
+ 0.90.5
+
+
+
+
+ junit
+ junit
+ 4.8.2
+
+
+ org.elasticsearch
+ elasticsearch
+ ${elasticsearch.version}
+
+
+
+
+ sonatype
+ Sonatype Groups
+ https://oss.sonatype.org/content/groups/public/
+
+
+
diff --git a/src/main/java/org/elasticsearch/plugin/geohashcellfacet/GeoHashCell.java b/src/main/java/org/elasticsearch/plugin/geohashcellfacet/GeoHashCell.java
new file mode 100644
index 0000000..fd856d5
--- /dev/null
+++ b/src/main/java/org/elasticsearch/plugin/geohashcellfacet/GeoHashCell.java
@@ -0,0 +1,82 @@
+package org.elasticsearch.plugin.geohashcellfacet;
+
+import org.elasticsearch.common.geo.GeoHashUtils;
+import org.elasticsearch.common.geo.GeoPoint;
+
+/**
+ * Represents a cell established by the geo hash prefix.
+ */
+public class GeoHashCell {
+ public final static int MIN_GEOHASH_LEVEL = 1;
+ public final static int MAX_GEOHASH_LEVEL = 6;
+
+ private final String geoHashPrefix;
+ private final GeoPoint topLeft = new GeoPoint();
+ private final GeoPoint bottomRight = new GeoPoint();
+
+ /**
+ * Creates a geo hash cell of the provided level which includes the given point.
+ *
+ * @param point Point which should be included in this cell.
+ * @param level Level for the cell, i.e. the length of the geo hash.
+ */
+ public GeoHashCell(GeoPoint point, int level) {
+ this(GeoHashUtils.encode(point.lat(), point.lon()).substring(0, level));
+ }
+
+ /**
+ * Creates a geo hash cell instance based on the given geo hash prefix.
+ * @param geoHashPrefix Prefix for the geo hash cell.
+ */
+ public GeoHashCell(String geoHashPrefix) {
+ if (geoHashPrefix == null || geoHashPrefix.isEmpty())
+ throw new IllegalArgumentException("GeoHash value is required");
+ this.geoHashPrefix = geoHashPrefix;
+
+ GeoHashUtils.decodeCell(geoHashPrefix, topLeft, bottomRight);
+ }
+
+ /**
+ * Gets the point which is a center of this geo hash cell.
+ * @return Center of this cell.
+ */
+ public GeoPoint getCenter() {
+ return new GeoPoint(
+ (topLeft.lat() + bottomRight.lat()) / 2D,
+ (topLeft.lon() + bottomRight.lon()) / 2D
+ );
+ }
+
+ /**
+ * Compare if this object equals another object.
+ * @param o Object to compare.
+ * @return True if objects are equal, otherwise false.
+ */
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+
+ GeoHashCell that = (GeoHashCell) o;
+
+ return geoHashPrefix.equals(that.geoHashPrefix);
+ }
+
+ /**
+ * Gets a hash code of this instance.
+ * @return Hash code.
+ */
+ @Override
+ public int hashCode() {
+ return geoHashPrefix.hashCode();
+ }
+
+ /**
+ * Gets a string representation of this instance.
+ * @return String representation.
+ */
+ @Override
+ public String toString() {
+ return getCenter().toString();
+ }
+}
diff --git a/src/main/java/org/elasticsearch/plugin/geohashcellfacet/GeoHashCellFacet.java b/src/main/java/org/elasticsearch/plugin/geohashcellfacet/GeoHashCellFacet.java
new file mode 100644
index 0000000..3170af6
--- /dev/null
+++ b/src/main/java/org/elasticsearch/plugin/geohashcellfacet/GeoHashCellFacet.java
@@ -0,0 +1,127 @@
+package org.elasticsearch.plugin.geohashcellfacet;
+
+import org.elasticsearch.common.bytes.BytesArray;
+import org.elasticsearch.common.bytes.BytesReference;
+import org.elasticsearch.common.collect.Maps;
+import org.elasticsearch.common.io.stream.StreamInput;
+import org.elasticsearch.common.xcontent.XContentBuilder;
+import org.elasticsearch.common.xcontent.XContentBuilderString;
+import org.elasticsearch.search.facet.Facet;
+import org.elasticsearch.search.facet.InternalFacet;
+
+import java.io.IOException;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicLong;
+
+/**
+ * An ElasticSearch facet which allows grouping geo points by cells established
+ * by geo hash.
+ * Example usage:
+ * "geohashcell": {
+ * "level" : 3,
+ * "top_left": "11.21,51.33",
+ * "bottom_right": "-20.21,80.65"
+ * }
+ */
+public class GeoHashCellFacet extends InternalFacet {
+
+ public static final String TYPE = "geohashcell";
+ private static final BytesReference STREAM_TYPE = new BytesArray(TYPE);
+ private final MapBox mapBox;
+
+ private Map counts = Maps.newHashMap();
+ private final String fieldName;
+ private final int userLevel;
+
+ /**
+ * Creates the facet instance.
+ * @param facetName Name of the facet.
+ * @param counts Map of the group counts.
+ * @param fieldName Name of the field used for grouping.
+ * @param mapBox Current map viewport.
+ * @param userLevel Level for geo hash grouping.
+ */
+ public GeoHashCellFacet(String facetName, Map counts,
+ String fieldName, MapBox mapBox,
+ int userLevel) {
+ super(facetName);
+ this.counts = counts;
+ this.fieldName = fieldName;
+ this.mapBox = mapBox;
+ this.userLevel = userLevel;
+ }
+
+ private GeoHashCellFacet() {
+ this.fieldName = null;
+ this.mapBox = null;
+ this.userLevel = 0;
+ }
+
+
+ public static void registerStreams() {
+ Streams.registerStream(STREAM, STREAM_TYPE);
+ }
+
+ static InternalFacet.Stream STREAM = new InternalFacet.Stream() {
+ @Override
+ public Facet readFacet(StreamInput in) throws IOException {
+ GeoHashCellFacet facet = new GeoHashCellFacet();
+ facet.readFrom(in);
+ return facet;
+ }
+ };
+
+ @Override
+ public BytesReference streamType() {
+ return STREAM_TYPE;
+ }
+
+ @Override
+ public Facet reduce(ReduceContext reduceContext) {
+ List facets = reduceContext.facets();
+ GeoHashCellFacet geoHashCellFacet = (GeoHashCellFacet) facets.get(0);
+
+ for (int i = 1; i < facets.size(); i++) {
+ Facet facet = facets.get(i);
+
+ if (facet instanceof GeoHashCellFacet) {
+ GeoHashCellFacet hashCellFacet = (GeoHashCellFacet) facet;
+
+ for (Map.Entry entry : hashCellFacet.counts.entrySet()) {
+ if (geoHashCellFacet.counts.containsKey(entry.getKey())) {
+ geoHashCellFacet.counts.get(entry.getKey()).addAndGet(entry.getValue().longValue());
+ }
+ else {
+ geoHashCellFacet.counts.put(entry.getKey(), entry.getValue());
+ }
+ }
+ }
+ }
+
+ return geoHashCellFacet;
+ }
+
+ @Override
+ public String getType() {
+ return TYPE;
+ }
+
+ @Override
+ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
+ builder.startObject(getName());
+ builder.field(Fields._TYPE, STREAM_TYPE);
+ builder.field(GeoHashCellFacetParser.ParamName.FIELD, fieldName);
+ builder.field(GeoHashCellFacetParser.ParamName.TOP_LEFT, mapBox.getTopLeft());
+ builder.field(GeoHashCellFacetParser.ParamName.BOTTOM_RIGHT, mapBox.getBottomRight());
+ builder.field(GeoHashCellFacetParser.ParamName.LEVEL, mapBox.getLevel(userLevel));
+ builder.field("base_map_level", mapBox.getBaseLevel());
+ builder.field("counts", counts);
+ builder.endObject();
+ return builder;
+ }
+
+ static final class Fields {
+ static final XContentBuilderString _TYPE = new XContentBuilderString("_type");
+ }
+}
diff --git a/src/main/java/org/elasticsearch/plugin/geohashcellfacet/GeoHashCellFacetExecutor.java b/src/main/java/org/elasticsearch/plugin/geohashcellfacet/GeoHashCellFacetExecutor.java
new file mode 100644
index 0000000..e0bd828
--- /dev/null
+++ b/src/main/java/org/elasticsearch/plugin/geohashcellfacet/GeoHashCellFacetExecutor.java
@@ -0,0 +1,90 @@
+package org.elasticsearch.plugin.geohashcellfacet;
+
+import org.apache.lucene.index.AtomicReaderContext;
+import org.elasticsearch.common.collect.Maps;
+import org.elasticsearch.common.geo.GeoPoint;
+import org.elasticsearch.index.fielddata.GeoPointValues;
+import org.elasticsearch.index.fielddata.IndexGeoPointFieldData;
+import org.elasticsearch.search.facet.FacetExecutor;
+import org.elasticsearch.search.facet.InternalFacet;
+import org.elasticsearch.search.internal.SearchContext;
+
+import java.io.IOException;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicLong;
+
+public class GeoHashCellFacetExecutor extends FacetExecutor {
+ private final String fieldName;
+ private final int userLevel;
+ private Map counts = Maps.newHashMap();
+ private final IndexGeoPointFieldData indexFieldData;
+ private final MapBox mapBox;
+
+ public GeoHashCellFacetExecutor(String fieldName,
+ SearchContext searchContext,
+ MapBox mapBox,
+ int userLevel) {
+ this.fieldName = fieldName;
+ this.mapBox = mapBox;
+ this.userLevel = userLevel;
+ this.indexFieldData = searchContext
+ .fieldData()
+ .getForField(searchContext.smartNameFieldMapper(fieldName));
+ }
+
+ @Override
+ public InternalFacet buildFacet(String facetName) {
+ return new GeoHashCellFacet(
+ facetName, counts, fieldName,
+ mapBox, userLevel);
+ }
+
+ @Override
+ public Collector collector() {
+ return new Collector();
+ }
+
+ final class Collector extends FacetExecutor.Collector {
+
+ protected GeoPointValues values;
+
+ @Override
+ public void setNextReader(AtomicReaderContext context)
+ throws IOException {
+ values = indexFieldData.load(context).getGeoPointValues();
+ }
+
+ @Override
+ public void collect(int docId) throws IOException {
+ final GeoPointValues.Iter iterator = values.getIter(docId);
+
+ if (iterator == null || !iterator.hasNext()) {
+ increment("_missing", 1L);
+ return;
+ }
+
+ while (iterator.hasNext()) {
+ GeoPoint point = iterator.next();
+
+ if (!mapBox.includes(point))
+ continue;
+
+ GeoHashCell cell = new GeoHashCell(point, mapBox.getLevel(userLevel));
+ increment(cell.toString(), 1L);
+ }
+ }
+
+ @Override
+ public void postCollection() {
+ // do nothing
+ }
+
+ private void increment(String name, Long value) {
+ if (counts.containsKey(name)) {
+ counts.get(name).addAndGet(value);
+ } else {
+ counts.put(name, new AtomicLong(value));
+ }
+ }
+ }
+}
diff --git a/src/main/java/org/elasticsearch/plugin/geohashcellfacet/GeoHashCellFacetParser.java b/src/main/java/org/elasticsearch/plugin/geohashcellfacet/GeoHashCellFacetParser.java
new file mode 100644
index 0000000..a753fb7
--- /dev/null
+++ b/src/main/java/org/elasticsearch/plugin/geohashcellfacet/GeoHashCellFacetParser.java
@@ -0,0 +1,91 @@
+package org.elasticsearch.plugin.geohashcellfacet;
+
+
+import org.elasticsearch.common.component.AbstractComponent;
+import org.elasticsearch.common.geo.GeoPoint;
+import org.elasticsearch.common.inject.Inject;
+import org.elasticsearch.common.settings.Settings;
+import org.elasticsearch.common.xcontent.XContentParser;
+import org.elasticsearch.search.facet.FacetExecutor;
+import org.elasticsearch.search.facet.FacetParser;
+import org.elasticsearch.search.internal.SearchContext;
+
+import java.io.IOException;
+
+public class GeoHashCellFacetParser extends AbstractComponent implements FacetParser {
+
+ static final class ParamName {
+ final static String FIELD = "field";
+ final static String TOP_LEFT = "top_left";
+ final static String BOTTOM_RIGHT = "bottom_right";
+ final static String LEVEL = "level";
+ }
+
+ static final class Default {
+ final static String FIELD = "location";
+ final static GeoPoint TOP_LEFT = new GeoPoint(90L, -180L);
+ final static GeoPoint BOTTOM_RIGHT = new GeoPoint(-90L, 180L);
+ final static int LEVEL = 3;
+ }
+
+ @Inject
+ public GeoHashCellFacetParser(Settings settings) {
+ super(settings);
+ GeoHashCellFacet.registerStreams();
+ }
+
+ @Override
+ public String[] types() {
+ return new String[] {GeoHashCellFacet.TYPE};
+ }
+
+ @Override
+ public FacetExecutor.Mode defaultMainMode() {
+ return FacetExecutor.Mode.COLLECTOR;
+ }
+
+ @Override
+ public FacetExecutor.Mode defaultGlobalMode() {
+ return FacetExecutor.Mode.COLLECTOR;
+ }
+
+ @Override
+ public FacetExecutor parse(String facetName, XContentParser parser,
+ SearchContext searchContext) throws IOException {
+ String field = Default.FIELD;
+ GeoPoint topLeft = new GeoPoint(
+ Default.TOP_LEFT.lat(), Default.TOP_LEFT.lon());
+ GeoPoint bottomRight = new GeoPoint(
+ Default.BOTTOM_RIGHT.lat(), Default.BOTTOM_RIGHT.lon());
+
+ int level = Default.LEVEL;
+
+ String currentFieldName = null;
+ XContentParser.Token token;
+
+ while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) {
+ if (token == XContentParser.Token.FIELD_NAME) {
+ currentFieldName = parser.currentName();
+ }
+ else if (token.isValue()) {
+ if (currentFieldName.equals(ParamName.FIELD)) {
+ field = parser.text();
+ }
+ else if (currentFieldName.equals(ParamName.TOP_LEFT)) {
+ GeoPoint.parse(parser, topLeft);
+ }
+ else if (currentFieldName.equals(ParamName.BOTTOM_RIGHT)) {
+ GeoPoint.parse(parser, bottomRight);
+ }
+ else if (currentFieldName.equals(ParamName.LEVEL)) {
+ level = parser.intValue();
+ }
+ }
+ }
+
+ MapBox mapBox = new MapBox(topLeft, bottomRight);
+
+ return new GeoHashCellFacetExecutor(field, searchContext,
+ mapBox, level);
+ }
+}
diff --git a/src/main/java/org/elasticsearch/plugin/geohashcellfacet/GeoHashCellFacetPlugin.java b/src/main/java/org/elasticsearch/plugin/geohashcellfacet/GeoHashCellFacetPlugin.java
new file mode 100644
index 0000000..91948b8
--- /dev/null
+++ b/src/main/java/org/elasticsearch/plugin/geohashcellfacet/GeoHashCellFacetPlugin.java
@@ -0,0 +1,40 @@
+package org.elasticsearch.plugin.geohashcellfacet;
+
+
+import org.elasticsearch.plugins.AbstractPlugin;
+import org.elasticsearch.search.facet.FacetModule;
+
+/**
+ * Plugin which extends ElasticSearch with a Geohash Cell facet functionality.
+ */
+public class GeoHashCellFacetPlugin extends AbstractPlugin {
+ /**
+ * Name of the plugin.
+ *
+ * @return Name of the plugin.
+ */
+ @Override
+ public String name() {
+ return "geohashcell-facet";
+ }
+
+ /**
+ * Description of the plugin.
+ *
+ * @return Description of the plugin.
+ */
+ @Override
+ public String description() {
+ return "Geohash cell facet support";
+ }
+
+ /**
+ * Hooks up to the FacetModule initialization and adds a new facet parser
+ * {@link GeoHashCellFacetParser} which enables using {@link GeoHashCellFacet}.
+ *
+ * @param facetModule {@link FacetModule} instance
+ */
+ public void onModule(FacetModule facetModule) {
+ facetModule.addFacetProcessor(GeoHashCellFacetParser.class);
+ }
+}
diff --git a/src/main/java/org/elasticsearch/plugin/geohashcellfacet/MapBox.java b/src/main/java/org/elasticsearch/plugin/geohashcellfacet/MapBox.java
new file mode 100644
index 0000000..a2d16de
--- /dev/null
+++ b/src/main/java/org/elasticsearch/plugin/geohashcellfacet/MapBox.java
@@ -0,0 +1,113 @@
+package org.elasticsearch.plugin.geohashcellfacet;
+
+
+import org.elasticsearch.common.geo.GeoDistance;
+import org.elasticsearch.common.geo.GeoPoint;
+import org.elasticsearch.common.geo.GeoUtils;
+import org.elasticsearch.common.unit.DistanceUnit;
+
+/**
+ * A box representing the current map viewport.
+ */
+public class MapBox {
+ private final GeoPoint topLeft;
+ private final GeoPoint bottomRight;
+ private final double width;
+ private final double height;
+ private final int baseLevel;
+
+ /**
+ * Creates the map box based on the provided coordinates.
+ * @param topLeft Top-left point of the map.
+ * @param bottomRight Bottom-right point of the map.
+ */
+ public MapBox(GeoPoint topLeft, GeoPoint bottomRight) {
+ // TODO check if correct
+ this.topLeft = topLeft;
+ this.bottomRight = bottomRight;
+
+ this.width = calculateWidth();
+ this.height = calculateHeight();
+ this.baseLevel = calculateBaseLevel();
+ }
+
+ /**
+ * Top-left point of the map.
+ * @return Top-left point of the map.
+ */
+ public GeoPoint getTopLeft() {
+ return topLeft;
+ }
+
+ /**
+ * Bottom-right point of the map.
+ * @return Bottom-right point of the map.
+ */
+ public GeoPoint getBottomRight() {
+ return bottomRight;
+ }
+
+ /**
+ * Checks if the given point is included inside this box.
+ * @param point Point to check.
+ * @return True if point is within boundaries of the box.
+ */
+ public boolean includes(GeoPoint point) {
+ // TODO what about crossing date change line
+
+ return topLeft.lat() >= point.lat() &&
+ topLeft.lon() <= point.lon() &&
+ point.lat() >= bottomRight.lat() &&
+ point.lon() <= bottomRight.lon();
+ }
+
+ /**
+ * Gets the base grouping level (geohash length) for the current viewport.
+ * @return Base grouping level.
+ */
+ public int getBaseLevel() {
+ return baseLevel;
+ }
+
+ /**
+ * Gets the actual grouping level (geohash length), based on the base level
+ * for the current map box and the level provided by the user.
+ * @param userLevel Grouping level provided by the "level" parameter.
+ * @return Grouping level in [1,6] range.
+ */
+ public int getLevel(int userLevel) {
+ return Math.max(GeoHashCell.MIN_GEOHASH_LEVEL,
+ Math.min(GeoHashCell.MAX_GEOHASH_LEVEL, baseLevel + userLevel));
+ }
+
+ private int calculateBaseLevel() {
+
+ for (int level = 1; level <= GeoHashCell.MAX_GEOHASH_LEVEL; level++) {
+ double cellWidth = GeoUtils.geoHashCellWidth(level);
+ double cellHeight = GeoUtils.geoHashCellHeight(level);
+
+ if (geoHashCellSmallerThanMapBox(cellWidth, cellHeight))
+ return level - 1;
+ }
+
+ return GeoHashCell.MAX_GEOHASH_LEVEL - 1;
+ }
+
+ private boolean geoHashCellSmallerThanMapBox(double cellWidth, double cellHeight) {
+ return cellWidth < width && cellHeight < height;
+ }
+
+ private double calculateWidth() {
+ return GeoDistance.PLANE.calculate(
+ topLeft.lat(), topLeft.lon(),
+ topLeft.lat(), bottomRight.lon(),
+ DistanceUnit.METERS);
+ }
+
+ private double calculateHeight() {
+ return GeoDistance.PLANE.calculate(
+ topLeft.lat(), topLeft.lon(),
+ bottomRight.lat(), topLeft.lon(),
+ DistanceUnit.METERS);
+ }
+}
diff --git a/src/main/resources/es-plugin.properties b/src/main/resources/es-plugin.properties
new file mode 100644
index 0000000..a8b0908
--- /dev/null
+++ b/src/main/resources/es-plugin.properties
@@ -0,0 +1 @@
+plugin=org.elasticsearch.plugin.geohashcellfacet.GeoHashCellFacetPlugin
\ No newline at end of file
diff --git a/src/test/java/org/elasticsearch/plugin/tests/MapBoxTests.java b/src/test/java/org/elasticsearch/plugin/tests/MapBoxTests.java
new file mode 100644
index 0000000..925179d
--- /dev/null
+++ b/src/test/java/org/elasticsearch/plugin/tests/MapBoxTests.java
@@ -0,0 +1,68 @@
+package org.elasticsearch.plugin.tests;
+
+
+import org.elasticsearch.plugin.geohashcellfacet.MapBox;
+import org.elasticsearch.common.geo.GeoPoint;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+@RunWith(JUnit4.class)
+public class MapBoxTests {
+
+ @Test
+ public void mapBoxIncludesCorners() {
+ MapBox mapBox = new MapBox(new GeoPoint(0L, 0L), new GeoPoint(-10L, 10L));
+ assertTrue(mapBox.includes(new GeoPoint(0L, 0L)));
+ assertTrue(mapBox.includes(new GeoPoint(0L, 10L)));
+ assertTrue(mapBox.includes(new GeoPoint(-10L, 0L)));
+ assertTrue(mapBox.includes(new GeoPoint(-10L, 10L)));
+ }
+
+ @Test
+ public void mapBoxIncludesPointInside() {
+ MapBox mapBox = new MapBox(new GeoPoint(0L, 0L), new GeoPoint(-10L, 10L));
+ assertTrue(mapBox.includes(new GeoPoint(-5L, 5L)));
+ }
+
+ @Test
+ public void mapBoxIncludesBoundaryPoints() {
+ MapBox mapBox = new MapBox(new GeoPoint(0L, 0L), new GeoPoint(-10L, 10L));
+ assertTrue(mapBox.includes(new GeoPoint(0L, 5L)));
+ assertTrue(mapBox.includes(new GeoPoint(-5L, 0L)));
+ assertTrue(mapBox.includes(new GeoPoint(-10L, 5L)));
+ }
+
+ @Test
+ public void mapBoxDoesNotIncludePointOutside() {
+ MapBox mapBox = new MapBox(new GeoPoint(0L, 0L), new GeoPoint(-10L, 10L));
+ assertFalse(mapBox.includes(new GeoPoint(-15L, 15L)));
+ assertFalse(mapBox.includes(new GeoPoint(0L, 15L)));
+ }
+
+ @Test
+ public void mapBoxForFullMapHasBaseLevel0() {
+ MapBox mapBox = new MapBox(new GeoPoint(90L, -180L), new GeoPoint(-90L, 180L));
+ assertEquals(0, mapBox.getBaseLevel());
+ }
+
+ @Test
+ public void mapBoxForOneGeoHashLevelDownHasBaseLevel1() {
+ MapBox mapBox = new MapBox(
+ new GeoPoint(90L/4L, -180L/8L),
+ new GeoPoint(-90L/4L, 180L/8L));
+ assertEquals(1, mapBox.getBaseLevel());
+ }
+
+ @Test
+ public void mapBoxForTwoGeoHashLevelsDownHasBaseLevel2() {
+ MapBox mapBox = new MapBox(
+ new GeoPoint(90L/32L, -180L/32L),
+ new GeoPoint(-90L/32L, 180L/32L));
+ assertEquals(2, mapBox.getBaseLevel());
+ }
+}