# GeoShape Precision

The `geo_shape` precision could be only set via `tree_levels` so far. A new option `precision` now allows to set the levels of the underlying tree structures to be set by distances like `50m`. The def

## Example
```json
curl -XPUT 'http://127.0.0.1:9200/myindex/' -d '{
  "mappings" : {
      "type1": {
          "dynamic": "false",
          "properties": {
              "location" : {
                  "type" : "geo_shape",
                  "geohash" : "true",
                  "store" : "yes",
                  "precision":"50m"
              }
          }
      }
  }
}'
```

## Changes
- GeoUtils defines the [WGS84](http://en.wikipedia.org/wiki/WGS84) reference ellipsoid of earth
- DistanceUnits refer to a more precise definition of earth circumference
- DistanceUnits for inch, yard and meter have been defined
- Set default levels in GeoShapeFieldMapper to 50m precision

Closes #2803
This commit is contained in:
Florian Schilling 2013-03-14 15:32:46 +01:00 committed by Simon Willnauer
parent 4705eb2959
commit f08d458545
6 changed files with 687 additions and 121 deletions

View File

@ -19,10 +19,157 @@
package org.elasticsearch.common.geo;
import org.apache.lucene.spatial.prefix.tree.GeohashPrefixTree;
import org.apache.lucene.spatial.prefix.tree.QuadPrefixTree;
import org.elasticsearch.common.unit.DistanceUnit;
/**
*/
public class GeoUtils {
/** Earth ellipsoid major axis defined by WGS 84 in meters */
public static final double EARTH_SEMI_MAJOR_AXIS = 6378137.0; // meters (WGS 84)
/** Earth ellipsoid minor axis defined by WGS 84 in meters */
public static final double EARTH_SEMI_MINOR_AXIS = 6356752.314245; // meters (WGS 84)
/** Earth ellipsoid equator length in meters */
public static final double EARTH_EQUATOR = 2*Math.PI * EARTH_SEMI_MAJOR_AXIS;
/** Earth ellipsoid polar distance in meters */
public static final double EARTH_POLAR_DISTANCE = Math.PI * EARTH_SEMI_MINOR_AXIS;
/**
* Calculate the width (in meters) of geohash cells at a specific level
* @param level geohash level must be greater or equal to zero
* @return the width of cells at level in meters
*/
public static double geoHashCellWidth(int level) {
assert level>=0;
// Geohash cells are split into 32 cells at each level. the grid
// alternates at each level between a 8x4 and a 4x8 grid
return EARTH_EQUATOR / (1L<<((((level+1)/2)*3) + ((level/2)*2)));
}
/**
* Calculate the width (in meters) of quadtree cells at a specific level
* @param level quadtree level must be greater or equal to zero
* @return the width of cells at level in meters
*/
public static double quadTreeCellWidth(int level) {
assert level >=0;
return EARTH_EQUATOR / (1L<<level);
}
/**
* Calculate the height (in meters) of geohash cells at a specific level
* @param level geohash level must be greater or equal to zero
* @return the height of cells at level in meters
*/
public static double geoHashCellHeight(int level) {
assert level>=0;
// Geohash cells are split into 32 cells at each level. the grid
// alternates at each level between a 8x4 and a 4x8 grid
return EARTH_POLAR_DISTANCE / (1L<<((((level+1)/2)*2) + ((level/2)*3)));
}
/**
* Calculate the height (in meters) of quadtree cells at a specific level
* @param level quadtree level must be greater or equal to zero
* @return the height of cells at level in meters
*/
public static double quadTreeCellHeight(int level) {
assert level>=0;
return EARTH_POLAR_DISTANCE / (1L<<level);
}
/**
* Calculate the size (in meters) of geohash cells at a specific level
* @param level geohash level must be greater or equal to zero
* @return the size of cells at level in meters
*/
public static double geoHashCellSize(int level) {
assert level>=0;
final double w = geoHashCellWidth(level);
final double h = geoHashCellHeight(level);
return Math.sqrt(w*w + h*h);
}
/**
* Calculate the size (in meters) of quadtree cells at a specific level
* @param level quadtree level must be greater or equal to zero
* @return the size of cells at level in meters
*/
public static double quadTreeCellSize(int level) {
assert level>=0;
return Math.sqrt(EARTH_POLAR_DISTANCE*EARTH_POLAR_DISTANCE + EARTH_EQUATOR*EARTH_EQUATOR) / (1L<<level);
}
/**
* Calculate the number of levels needed for a specific precision. Quadtree
* cells will not exceed the specified size (diagonal) of the precision.
* @param meters Maximum size of cells in meters (must greater than zero)
* @return levels need to achieve precision
*/
public static int quadTreeLevelsForPrecision(double meters) {
assert meters >= 0;
if(meters == 0) {
return QuadPrefixTree.MAX_LEVELS_POSSIBLE;
} else {
final double ratio = 1+(EARTH_POLAR_DISTANCE / EARTH_EQUATOR); // cell ratio
final double width = Math.sqrt((meters*meters)/(ratio*ratio)); // convert to cell width
final long part = Math.round(Math.ceil(EARTH_EQUATOR / width));
final int level = Long.SIZE - Long.numberOfLeadingZeros(part)-1; // (log_2)
return (part<=(1l<<level)) ?level :(level+1); // adjust level
}
}
/**
* Calculate the number of levels needed for a specific precision. QuadTree
* cells will not exceed the specified size (diagonal) of the precision.
* @param distance Maximum size of cells as unit string (must greater or equal to zero)
* @return levels need to achieve precision
*/
public static int quadTreeLevelsForPrecision(String distance) {
return quadTreeLevelsForPrecision(DistanceUnit.parse(distance, DistanceUnit.METERS, DistanceUnit.METERS));
}
/**
* Calculate the number of levels needed for a specific precision. GeoHash
* cells will not exceed the specified size (diagonal) of the precision.
* @param meters Maximum size of cells in meters (must greater or equal to zero)
* @return levels need to achieve precision
*/
public static int geoHashLevelsForPrecision(double meters) {
assert meters >= 0;
if(meters == 0) {
return GeohashPrefixTree.getMaxLevelsPossible();
} else {
final double ratio = 1+(EARTH_POLAR_DISTANCE / EARTH_EQUATOR); // cell ratio
final double width = Math.sqrt((meters*meters)/(ratio*ratio)); // convert to cell width
final double part = Math.ceil(EARTH_EQUATOR / width);
if(part == 1)
return 1;
final int bits = (int)Math.round(Math.ceil(Math.log(part) / Math.log(2)));
final int full = bits / 5; // number of 5 bit subdivisions
final int left = bits - full*5; // bit representing the last level
final int even = full + (left>0?1:0); // number of even levels
final int odd = full + (left>3?1:0); // number of odd levels
return even+odd;
}
}
/**
* Calculate the number of levels needed for a specific precision. GeoHash
* cells will not exceed the specified size (diagonal) of the precision.
* @param distance Maximum size of cells as unit string (must greater or equal to zero)
* @return levels need to achieve precision
*/
public static int geoHashLevelsForPrecision(String distance) {
return geoHashLevelsForPrecision(DistanceUnit.parse(distance, DistanceUnit.METERS, DistanceUnit.METERS));
}
/**
* Normalize longitude to lie within the -180 (exclusive) to 180 (inclusive) range.
*

View File

@ -20,59 +20,132 @@
package org.elasticsearch.common.unit;
import org.elasticsearch.ElasticSearchIllegalArgumentException;
import org.elasticsearch.common.geo.GeoUtils;
import org.elasticsearch.common.io.stream.StreamInput;
import org.elasticsearch.common.io.stream.StreamOutput;
import java.io.IOException;
/**
*
* The DistanceUnit enumerates several units for measuring distances. These units
* provide methods for converting strings and methods to convert units among each
* others. Some methods like {@link DistanceUnit#getEarthCircumference} refer to
* the earth ellipsoid defined in {@link GeoUtils}.
*/
public enum DistanceUnit {
MILES(3959, 24902) {
@Override
public String toString() {
return "miles";
}
INCH(0.0254, "in", "inch"),
YARD(0.9144, "yd", "yards"),
MILES(1609.344, "mi", "miles"),
KILOMETERS(1000.0, "km", "kilometers"),
MILLIMETERS(0.001, "mm", "millimeters"),
CENTIMETERS(0.01, "cm", "centimeters"),
// since 'm' is suffix of other unit
// it must be the last entry of unit
// names ending with 'm'. otherwise
// parsing would fail
METERS(1, "m", "meters");
@Override
public double toMiles(double distance) {
return distance;
}
private double meters;
private final String[] names;
DistanceUnit(double meters, String...names) {
this.meters = meters;
this.names = names;
}
@Override
public double toKilometers(double distance) {
return distance * MILES_KILOMETRES_RATIO;
}
/**
* Measures the circumference of earth in this unit
*
* @return length of earth circumference in this unit
*/
public double getEarthCircumference() {
return GeoUtils.EARTH_EQUATOR / meters;
}
@Override
public String toString(double distance) {
return distance + "mi";
}
},
KILOMETERS(6371, 40076) {
@Override
public String toString() {
return "km";
}
/**
* Measures the radius of earth in this unit
*
* @return length of earth radius in this unit
*/
public double getEarthRadius() {
return GeoUtils.EARTH_SEMI_MAJOR_AXIS / meters;
}
@Override
public double toMiles(double distance) {
return distance / MILES_KILOMETRES_RATIO;
}
/**
* Measures a longitude in this unit
*
* @return length of a longitude degree in this unit
*/
public double getDistancePerDegree() {
return GeoUtils.EARTH_EQUATOR / (360.0 * meters);
}
@Override
public double toKilometers(double distance) {
return distance;
}
/**
* Convert a value into miles
*
* @param distance distance in this unit
* @return value in miles
*/
public double toMiles(double distance) {
return convert(distance, this, DistanceUnit.MILES);
}
@Override
public String toString(double distance) {
return distance + "km";
}
};
/**
* Convert a value into kilometers
*
* @param distance distance in this unit
* @return value in kilometers
*/
public double toKilometers(double distance) {
return convert(distance, this, DistanceUnit.KILOMETERS);
}
static final double MILES_KILOMETRES_RATIO = 1.609344;
/**
* Convert a value into meters
*
* @param distance distance in this unit
* @return value in meters
*/
public double toMeters(double distance) {
return convert(distance, this, DistanceUnit.METERS);
}
/**
* Convert a value given in meters to a value of this unit
*
* @param distance distance in meters
* @return value in this unit
*/
public double fromMeters(double distance) {
return convert(distance, DistanceUnit.METERS, this);
}
/**
* Convert a given value into another unit
*
* @param distance value in this unit
* @param unit target unit
* @return value of the target unit
*/
public double convert(double distance, DistanceUnit unit) {
return convert(distance, this, unit);
}
/**
* Convert a value to a distance string
*
* @param distance value to convert
* @return String representation of the distance
*/
public String toString(double distance) {
return distance + toString();
}
@Override
public String toString() {
return names[0];
}
/**
* Converts the given distance from the given DistanceUnit, to the given DistanceUnit
@ -80,94 +153,166 @@ public enum DistanceUnit {
* @param distance Distance to convert
* @param from Unit to convert the distance from
* @param to Unit of distance to convert to
* @return Given distance converted to the distance in the given uni
* @return Given distance converted to the distance in the given unit
*/
public static double convert(double distance, DistanceUnit from, DistanceUnit to) {
if (from == to) {
return distance;
} else {
return distance * from.meters / to.meters;
}
return (to == MILES) ? distance / MILES_KILOMETRES_RATIO : distance * MILES_KILOMETRES_RATIO;
}
/**
* Parses a given distance and converts it to the specified unit.
*
* @param distance String defining a distance (value and unit)
* @param defaultUnit unit assumed if none is defined
* @param to unit of result
* @return parsed distance
*/
public static double parse(String distance, DistanceUnit defaultUnit, DistanceUnit to) {
if (distance.endsWith("mi")) {
return convert(Double.parseDouble(distance.substring(0, distance.length() - "mi".length())), MILES, to);
} else if (distance.endsWith("miles")) {
return convert(Double.parseDouble(distance.substring(0, distance.length() - "miles".length())), MILES, to);
} else if (distance.endsWith("km")) {
return convert(Double.parseDouble(distance.substring(0, distance.length() - "km".length())), KILOMETERS, to);
} else {
return convert(Double.parseDouble(distance), defaultUnit, to);
}
Distance dist = Distance.parseDistance(distance, defaultUnit);
return convert(dist.value, dist.unit, to);
}
public static DistanceUnit parseUnit(String distance, DistanceUnit defaultUnit) {
if (distance.endsWith("mi")) {
return MILES;
} else if (distance.endsWith("miles")) {
return MILES;
} else if (distance.endsWith("km")) {
return KILOMETERS;
} else {
return defaultUnit;
}
}
protected final double earthCircumference;
protected final double earthRadius;
protected final double distancePerDegree;
DistanceUnit(double earthRadius, double earthCircumference) {
this.earthCircumference = earthCircumference;
this.earthRadius = earthRadius;
this.distancePerDegree = earthCircumference / 360;
}
public double getEarthCircumference() {
return earthCircumference;
}
public double getEarthRadius() {
return earthRadius;
}
public double getDistancePerDegree() {
return distancePerDegree;
}
public abstract double toMiles(double distance);
public abstract double toKilometers(double distance);
public abstract String toString(double distance);
/**
* Convert a String to a {@link DistanceUnit}
*
* @param unit name of the unit
* @return unit matching the given name
* @throws ElasticSearchIllegalArgumentException if no unit matches the given name
*/
public static DistanceUnit fromString(String unit) {
if ("km".equals(unit)) {
return KILOMETERS;
} else if ("mi".equals(unit)) {
return MILES;
} else if ("miles".equals(unit)) {
return MILES;
for (DistanceUnit dunit : values()) {
for (String name : dunit.names) {
if(name.equals(unit)) {
return dunit;
}
}
}
throw new ElasticSearchIllegalArgumentException("No distance unit match [" + unit + "]");
}
/**
* Parses the suffix of a given distance string and return the corresponding {@link DistanceUnit}
*
* @param distance string representing a distance
* @param defaultUnit default unit to use, if no unit is provided by the string
* @return unit of the given distance
*/
public static DistanceUnit parseUnit(String distance, DistanceUnit defaultUnit) {
for (DistanceUnit unit : values()) {
for (String name : unit.names) {
if(distance.endsWith(name)) {
return unit;
}
}
}
return defaultUnit;
}
/**
* Write a {@link DistanceUnit} to a {@link StreamOutput}
*
* @param out {@link StreamOutput} to write to
* @param unit {@link DistanceUnit} to write
* @throws IOException
*/
public static void writeDistanceUnit(StreamOutput out, DistanceUnit unit) throws IOException {
if (unit == MILES) {
out.writeByte((byte) 0);
} else if (unit == KILOMETERS) {
out.writeByte((byte) 1);
out.writeByte((byte) unit.ordinal());
}
/**
* Read a {@link DistanceUnit} from a {@link StreamInput}
*
* @param in {@link StreamInput} to read the {@link DistanceUnit} from
* @return {@link DistanceUnit} read from the {@link StreamInput}
* @throws IOException if no unit can be read from the {@link StreamInput}
* @thrown ElasticSearchIllegalArgumentException if no matching {@link DistanceUnit} can be found
*/
public static DistanceUnit readDistanceUnit(StreamInput in) throws IOException {
byte b = in.readByte();
if(b<0 || b>=values().length) {
throw new ElasticSearchIllegalArgumentException("No type for distance unit matching [" + b + "]");
} else {
return values()[b];
}
}
public static DistanceUnit readDistanceUnit(StreamInput in) throws IOException {
byte b = in.readByte();
if (b == 0) {
return MILES;
} else if (b == 1) {
return KILOMETERS;
} else {
throw new ElasticSearchIllegalArgumentException("No type for distance unit matching [" + b + "]");
/**
* This class implements a value+unit tuple.
*/
public static class Distance implements Comparable<Distance> {
public final double value;
public final DistanceUnit unit;
private Distance(double value, DistanceUnit unit) {
super();
this.value = value;
this.unit = unit;
}
/**
* Converts a {@link Distance} value given in a specific {@link DistanceUnit} into
* a value equal to the specified value but in a other {@link DistanceUnit}.
*
* @param unit unit of the result
* @return converted distance
*/
public Distance convert(DistanceUnit unit) {
if(this.unit == unit) {
return this;
} else {
return new Distance(DistanceUnit.convert(value, this.unit, unit), unit);
}
}
@Override
public boolean equals(Object obj) {
if(obj == null) {
return false;
} else if (obj instanceof Distance) {
Distance other = (Distance) obj;
return DistanceUnit.convert(value, unit, other.unit) == other.value;
} else {
return false;
}
}
@Override
public int hashCode() {
return Double.valueOf(value * unit.meters).hashCode();
}
@Override
public int compareTo(Distance o) {
return Double.compare(value, DistanceUnit.convert(o.value, o.unit, unit));
}
@Override
public String toString() {
return unit.toString(value);
}
/**
* Parse a {@link Distance} from a given String
*
* @param distance String defining a {@link Distance}
* @param defaultUnit {@link DistanceUnit} to be assumed
* if not unit is provided in the first argument
* @return parsed {@link Distance}
*/
public static Distance parseDistance(String distance, DistanceUnit defaultUnit) {
for (DistanceUnit unit : values()) {
for (String name : unit.names) {
if(distance.endsWith(name)) {
return new Distance(Double.parseDouble(distance.substring(0, distance.length() - name.length())), unit);
}
}
}
return new Distance(Double.parseDouble(distance), defaultUnit);
}
}
}

View File

@ -1,3 +1,21 @@
/*
* Licensed to ElasticSearch and Shay Banon under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. ElasticSearch licenses this
* file to you 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.elasticsearch.index.mapper.geo;
import org.apache.lucene.document.Field;
@ -13,7 +31,9 @@ import org.elasticsearch.ElasticSearchIllegalArgumentException;
import org.elasticsearch.common.Strings;
import org.elasticsearch.common.geo.GeoJSONShapeParser;
import org.elasticsearch.common.geo.GeoShapeConstants;
import org.elasticsearch.common.geo.GeoUtils;
import org.elasticsearch.common.geo.SpatialStrategy;
import org.elasticsearch.common.unit.DistanceUnit;
import org.elasticsearch.common.xcontent.XContentBuilder;
import org.elasticsearch.index.codec.postingsformat.PostingsFormatProvider;
import org.elasticsearch.index.fielddata.FieldDataType;
@ -51,6 +71,7 @@ public class GeoShapeFieldMapper extends AbstractFieldMapper<String> {
public static final String TREE_GEOHASH = "geohash";
public static final String TREE_QUADTREE = "quadtree";
public static final String TREE_LEVELS = "tree_levels";
public static final String TREE_PRESISION = "precision";
public static final String DISTANCE_ERROR_PCT = "distance_error_pct";
public static final String STRATEGY = "strategy";
}
@ -58,8 +79,8 @@ public class GeoShapeFieldMapper extends AbstractFieldMapper<String> {
public static class Defaults {
public static final String TREE = Names.TREE_GEOHASH;
public static final String STRATEGY = SpatialStrategy.RECURSIVE.getStrategyName();
public static final int GEOHASH_LEVELS = GeohashPrefixTree.getMaxLevelsPossible();
public static final int QUADTREE_LEVELS = QuadPrefixTree.DEFAULT_MAX_LEVELS;
public static final int GEOHASH_LEVELS = GeoUtils.geoHashLevelsForPrecision("50m");
public static final int QUADTREE_LEVELS = GeoUtils.quadTreeLevelsForPrecision("50m");
public static final double DISTANCE_ERROR_PCT = 0.025d;
public static final FieldType FIELD_TYPE = new FieldType();
@ -80,7 +101,8 @@ public class GeoShapeFieldMapper extends AbstractFieldMapper<String> {
private String tree = Defaults.TREE;
private String strategyName = Defaults.STRATEGY;
private int treeLevels;
private int treeLevels = 0;
private double precisionInMeters = -1;
private double distanceErrorPct = Defaults.DISTANCE_ERROR_PCT;
private SpatialPrefixTree prefixTree;
@ -99,6 +121,11 @@ public class GeoShapeFieldMapper extends AbstractFieldMapper<String> {
return this;
}
public Builder treeLevelsByDistance(double meters) {
this.precisionInMeters = meters;
return this;
}
public Builder treeLevels(int treeLevels) {
this.treeLevels = treeLevels;
return this;
@ -112,14 +139,11 @@ public class GeoShapeFieldMapper extends AbstractFieldMapper<String> {
@Override
public GeoShapeFieldMapper build(BuilderContext context) {
FieldMapper.Names names = buildNames(context);
if (tree.equals(Names.TREE_GEOHASH)) {
int levels = treeLevels != 0 ? treeLevels : Defaults.GEOHASH_LEVELS;
prefixTree = new GeohashPrefixTree(GeoShapeConstants.SPATIAL_CONTEXT, levels);
} else if (tree.equals(Names.TREE_QUADTREE)) {
int levels = treeLevels != 0 ? treeLevels : Defaults.QUADTREE_LEVELS;
prefixTree = new QuadPrefixTree(GeoShapeConstants.SPATIAL_CONTEXT, levels);
final FieldMapper.Names names = buildNames(context);
if (Names.TREE_GEOHASH.equals(tree)) {
prefixTree = new GeohashPrefixTree(GeoShapeConstants.SPATIAL_CONTEXT, getLevels(treeLevels, precisionInMeters, Defaults.GEOHASH_LEVELS, true));
} else if (Names.TREE_QUADTREE.equals(tree)) {
prefixTree = new QuadPrefixTree(GeoShapeConstants.SPATIAL_CONTEXT, getLevels(treeLevels, precisionInMeters, Defaults.QUADTREE_LEVELS, false));
} else {
throw new ElasticSearchIllegalArgumentException("Unknown prefix tree type [" + tree + "]");
}
@ -127,7 +151,16 @@ public class GeoShapeFieldMapper extends AbstractFieldMapper<String> {
return new GeoShapeFieldMapper(names, prefixTree, strategyName, distanceErrorPct, fieldType, provider);
}
}
private static final int getLevels(int treeLevels, double precisionInMeters, int defaultLevels, boolean geoHash) {
if (treeLevels > 0 || precisionInMeters >= 0) {
return Math.max(treeLevels, precisionInMeters >= 0 ? (geoHash ? GeoUtils.geoHashLevelsForPrecision(precisionInMeters)
: GeoUtils.quadTreeLevelsForPrecision(precisionInMeters)) : 0);
}
return defaultLevels;
}
public static class TypeParser implements Mapper.TypeParser {
@Override
@ -141,6 +174,8 @@ public class GeoShapeFieldMapper extends AbstractFieldMapper<String> {
builder.tree(fieldNode.toString());
} else if (Names.TREE_LEVELS.equals(fieldName)) {
builder.treeLevels(Integer.parseInt(fieldNode.toString()));
} else if (Names.TREE_PRESISION.equals(fieldName)) {
builder.treeLevelsByDistance(DistanceUnit.parse(fieldNode.toString(), DistanceUnit.METERS, DistanceUnit.METERS));
} else if (Names.DISTANCE_ERROR_PCT.equals(fieldName)) {
builder.distanceErrorPct(Double.parseDouble(fieldNode.toString()));
} else if (Names.STRATEGY.equals(fieldName)) {

View File

@ -20,11 +20,10 @@
package org.elasticsearch.test.unit.common.unit;
import org.elasticsearch.common.unit.DistanceUnit;
import org.hamcrest.MatcherAssert;
import org.testng.annotations.Test;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.closeTo;
import static org.hamcrest.Matchers.*;
/**
*
@ -34,9 +33,31 @@ public class DistanceUnitTests {
@Test
void testSimpleDistanceUnit() {
MatcherAssert.assertThat(DistanceUnit.MILES.toKilometers(10), closeTo(16.09344, 0.001));
assertThat(DistanceUnit.MILES.toKilometers(10), closeTo(16.09344, 0.001));
assertThat(DistanceUnit.MILES.toMiles(10), closeTo(10, 0.001));
assertThat(DistanceUnit.KILOMETERS.toMiles(10), closeTo(6.21371192, 0.001));
assertThat(DistanceUnit.KILOMETERS.toKilometers(10), closeTo(10, 0.001));
assertThat(DistanceUnit.METERS.toKilometers(10), closeTo(0.01, 0.00001));
assertThat(DistanceUnit.METERS.toKilometers(1000), closeTo(1, 0.001));
assertThat(DistanceUnit.KILOMETERS.toMeters(1), closeTo(1000, 0.001));
}
@Test
void testDistanceUnitParsing() {
assertThat(DistanceUnit.Distance.parseDistance("50km", null).unit, equalTo(DistanceUnit.KILOMETERS));
assertThat(DistanceUnit.Distance.parseDistance("500m", null).unit, equalTo(DistanceUnit.METERS));
assertThat(DistanceUnit.Distance.parseDistance("51mi", null).unit, equalTo(DistanceUnit.MILES));
assertThat(DistanceUnit.Distance.parseDistance("52yd", null).unit, equalTo(DistanceUnit.YARD));
assertThat(DistanceUnit.Distance.parseDistance("12in", null).unit, equalTo(DistanceUnit.INCH));
assertThat(DistanceUnit.Distance.parseDistance("23mm", null).unit, equalTo(DistanceUnit.MILLIMETERS));
assertThat(DistanceUnit.Distance.parseDistance("23cm", null).unit, equalTo(DistanceUnit.CENTIMETERS));
double testValue = 12345.678;
for (DistanceUnit unit : DistanceUnit.values()) {
assertThat("Unit can be parsed from '" + unit.toString() + "'", DistanceUnit.fromString(unit.toString()), equalTo(unit));
assertThat("Unit can be parsed from '" + testValue + unit.toString() + "'", DistanceUnit.fromString(unit.toString()), equalTo(unit));
assertThat("Value can be parsed from '" + testValue + unit.toString() + "'", DistanceUnit.Distance.parseDistance(unit.toString(testValue), null).value, equalTo(testValue));
}
}
}

View File

@ -1,3 +1,21 @@
/*
* Licensed to ElasticSearch and Shay Banon under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. ElasticSearch licenses this
* file to you 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.elasticsearch.test.unit.index.mapper.geo;
import static org.hamcrest.MatcherAssert.assertThat;
@ -9,6 +27,7 @@ import java.io.IOException;
import org.apache.lucene.spatial.prefix.PrefixTreeStrategy;
import org.apache.lucene.spatial.prefix.tree.GeohashPrefixTree;
import org.apache.lucene.spatial.prefix.tree.QuadPrefixTree;
import org.elasticsearch.common.geo.GeoUtils;
import org.elasticsearch.common.xcontent.XContentFactory;
import org.elasticsearch.index.mapper.DocumentMapper;
import org.elasticsearch.index.mapper.FieldMapper;
@ -83,4 +102,148 @@ public class GeoShapeFieldMapperTests {
assertThat(strategy.getGrid(), instanceOf(QuadPrefixTree.class));
assertThat(strategy.getGrid().getMaxLevels(), equalTo(6));
}
@Test
public void testLevelPrecisionConfiguration() throws IOException {
{
String mapping = XContentFactory.jsonBuilder().startObject().startObject("type1")
.startObject("properties").startObject("location")
.field("type", "geo_shape")
.field("tree", "quadtree")
.field("tree_levels", "6")
.field("precision", "70m")
.field("distance_error_pct", "0.5")
.endObject().endObject()
.endObject().endObject().string();
DocumentMapper defaultMapper = MapperTests.newParser().parse(mapping);
FieldMapper fieldMapper = defaultMapper.mappers().name("location").mapper();
assertThat(fieldMapper, instanceOf(GeoShapeFieldMapper.class));
GeoShapeFieldMapper geoShapeFieldMapper = (GeoShapeFieldMapper) fieldMapper;
PrefixTreeStrategy strategy = geoShapeFieldMapper.defaultStrategy();
assertThat(strategy.getDistErrPct(), equalTo(0.5));
assertThat(strategy.getGrid(), instanceOf(QuadPrefixTree.class));
/* 70m is more precise so it wins */
assertThat(strategy.getGrid().getMaxLevels(), equalTo(GeoUtils.quadTreeLevelsForPrecision(70d)));
}
{
String mapping = XContentFactory.jsonBuilder().startObject().startObject("type1")
.startObject("properties").startObject("location")
.field("type", "geo_shape")
.field("tree", "geohash")
.field("tree_levels", "6")
.field("precision", "70m")
.field("distance_error_pct", "0.5")
.endObject().endObject()
.endObject().endObject().string();
DocumentMapper defaultMapper = MapperTests.newParser().parse(mapping);
FieldMapper fieldMapper = defaultMapper.mappers().name("location").mapper();
assertThat(fieldMapper, instanceOf(GeoShapeFieldMapper.class));
GeoShapeFieldMapper geoShapeFieldMapper = (GeoShapeFieldMapper) fieldMapper;
PrefixTreeStrategy strategy = geoShapeFieldMapper.defaultStrategy();
assertThat(strategy.getDistErrPct(), equalTo(0.5));
assertThat(strategy.getGrid(), instanceOf(GeohashPrefixTree.class));
/* 70m is more precise so it wins */
assertThat(strategy.getGrid().getMaxLevels(), equalTo(GeoUtils.geoHashLevelsForPrecision(70d)));
}
{
String mapping = XContentFactory.jsonBuilder().startObject().startObject("type1")
.startObject("properties").startObject("location")
.field("type", "geo_shape")
.field("tree", "geohash")
.field("tree_levels", GeoUtils.geoHashLevelsForPrecision(70d)+1)
.field("precision", "70m")
.field("distance_error_pct", "0.5")
.endObject().endObject()
.endObject().endObject().string();
DocumentMapper defaultMapper = MapperTests.newParser().parse(mapping);
FieldMapper fieldMapper = defaultMapper.mappers().name("location").mapper();
assertThat(fieldMapper, instanceOf(GeoShapeFieldMapper.class));
GeoShapeFieldMapper geoShapeFieldMapper = (GeoShapeFieldMapper) fieldMapper;
PrefixTreeStrategy strategy = geoShapeFieldMapper.defaultStrategy();
assertThat(strategy.getDistErrPct(), equalTo(0.5));
assertThat(strategy.getGrid(), instanceOf(GeohashPrefixTree.class));
assertThat(strategy.getGrid().getMaxLevels(), equalTo(GeoUtils.geoHashLevelsForPrecision(70d)+1));
}
{
String mapping = XContentFactory.jsonBuilder().startObject().startObject("type1")
.startObject("properties").startObject("location")
.field("type", "geo_shape")
.field("tree", "quadtree")
.field("tree_levels", GeoUtils.quadTreeLevelsForPrecision(70d)+1)
.field("precision", "70m")
.field("distance_error_pct", "0.5")
.endObject().endObject()
.endObject().endObject().string();
DocumentMapper defaultMapper = MapperTests.newParser().parse(mapping);
FieldMapper fieldMapper = defaultMapper.mappers().name("location").mapper();
assertThat(fieldMapper, instanceOf(GeoShapeFieldMapper.class));
GeoShapeFieldMapper geoShapeFieldMapper = (GeoShapeFieldMapper) fieldMapper;
PrefixTreeStrategy strategy = geoShapeFieldMapper.defaultStrategy();
assertThat(strategy.getDistErrPct(), equalTo(0.5));
assertThat(strategy.getGrid(), instanceOf(QuadPrefixTree.class));
assertThat(strategy.getGrid().getMaxLevels(), equalTo(GeoUtils.quadTreeLevelsForPrecision(70d)+1));
}
}
@Test
public void testLevelDefaults() throws IOException {
{
String mapping = XContentFactory.jsonBuilder().startObject().startObject("type1")
.startObject("properties").startObject("location")
.field("type", "geo_shape")
.field("tree", "quadtree")
.field("distance_error_pct", "0.5")
.endObject().endObject()
.endObject().endObject().string();
DocumentMapper defaultMapper = MapperTests.newParser().parse(mapping);
FieldMapper fieldMapper = defaultMapper.mappers().name("location").mapper();
assertThat(fieldMapper, instanceOf(GeoShapeFieldMapper.class));
GeoShapeFieldMapper geoShapeFieldMapper = (GeoShapeFieldMapper) fieldMapper;
PrefixTreeStrategy strategy = geoShapeFieldMapper.defaultStrategy();
assertThat(strategy.getDistErrPct(), equalTo(0.5));
assertThat(strategy.getGrid(), instanceOf(QuadPrefixTree.class));
/* 50m is default */
assertThat(strategy.getGrid().getMaxLevels(), equalTo(GeoUtils.quadTreeLevelsForPrecision(50d)));
}
{
String mapping = XContentFactory.jsonBuilder().startObject().startObject("type1")
.startObject("properties").startObject("location")
.field("type", "geo_shape")
.field("tree", "geohash")
.field("distance_error_pct", "0.5")
.endObject().endObject()
.endObject().endObject().string();
DocumentMapper defaultMapper = MapperTests.newParser().parse(mapping);
FieldMapper fieldMapper = defaultMapper.mappers().name("location").mapper();
assertThat(fieldMapper, instanceOf(GeoShapeFieldMapper.class));
GeoShapeFieldMapper geoShapeFieldMapper = (GeoShapeFieldMapper) fieldMapper;
PrefixTreeStrategy strategy = geoShapeFieldMapper.defaultStrategy();
assertThat(strategy.getDistErrPct(), equalTo(0.5));
assertThat(strategy.getGrid(), instanceOf(GeohashPrefixTree.class));
/* 50m is default */
assertThat(strategy.getGrid().getMaxLevels(), equalTo(GeoUtils.geoHashLevelsForPrecision(50d)));
}
}
}

View File

@ -19,12 +19,19 @@
package org.elasticsearch.test.unit.index.search.geo;
import org.apache.lucene.spatial.prefix.tree.GeohashPrefixTree;
import org.apache.lucene.spatial.prefix.tree.Node;
import org.apache.lucene.spatial.prefix.tree.QuadPrefixTree;
import org.elasticsearch.common.geo.GeoPoint;
import org.elasticsearch.common.geo.GeoUtils;
import org.testng.annotations.Test;
import com.spatial4j.core.context.SpatialContext;
import com.spatial4j.core.distance.DistanceUtils;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.lessThanOrEqualTo;
import static org.hamcrest.Matchers.not;
/**
@ -199,6 +206,54 @@ public class GeoUtilsTests {
assertThat(GeoUtils.normalizeLat(+18000000000091.0), equalTo(GeoUtils.normalizeLat(+091.0)));
}
@Test
public void testPrefixTreeCellSizes() {
assertThat(GeoUtils.EARTH_SEMI_MAJOR_AXIS, equalTo(DistanceUtils.EARTH_EQUATORIAL_RADIUS_KM * 1000));
assertThat(GeoUtils.quadTreeCellWidth(0), lessThanOrEqualTo(GeoUtils.EARTH_EQUATOR));
SpatialContext spatialContext = new SpatialContext(true);
GeohashPrefixTree geohashPrefixTree = new GeohashPrefixTree(spatialContext, GeohashPrefixTree.getMaxLevelsPossible()/2);
Node gNode = geohashPrefixTree.getWorldNode();
for(int i = 0; i<geohashPrefixTree.getMaxLevels(); i++) {
double width = GeoUtils.geoHashCellWidth(i);
double height = GeoUtils.geoHashCellHeight(i);
double size = GeoUtils.geoHashCellSize(i);
double degrees = 360.0 * width / GeoUtils.EARTH_EQUATOR;
int level = GeoUtils.quadTreeLevelsForPrecision(size);
assertThat(GeoUtils.quadTreeCellWidth(level), lessThanOrEqualTo(width));
assertThat(GeoUtils.quadTreeCellHeight(level), lessThanOrEqualTo(height));
assertThat(GeoUtils.geoHashLevelsForPrecision(size), equalTo(geohashPrefixTree.getLevelForDistance(degrees)));
assertThat("width at level "+i, gNode.getShape().getBoundingBox().getWidth(), equalTo(360.d * width / GeoUtils.EARTH_EQUATOR));
assertThat("height at level "+i, gNode.getShape().getBoundingBox().getHeight(), equalTo(180.d * height / GeoUtils.EARTH_POLAR_DISTANCE));
gNode = gNode.getSubCells(null).iterator().next();
}
QuadPrefixTree quadPrefixTree = new QuadPrefixTree(spatialContext);
Node qNode = quadPrefixTree.getWorldNode();
for (int i = 0; i < QuadPrefixTree.DEFAULT_MAX_LEVELS; i++) {
double degrees = 360.0/(1L<<i);
double width = GeoUtils.quadTreeCellWidth(i);
double height = GeoUtils.quadTreeCellHeight(i);
double size = GeoUtils.quadTreeCellSize(i);
int level = GeoUtils.quadTreeLevelsForPrecision(size);
assertThat(GeoUtils.quadTreeCellWidth(level), lessThanOrEqualTo(width));
assertThat(GeoUtils.quadTreeCellHeight(level), lessThanOrEqualTo(height));
assertThat(GeoUtils.quadTreeLevelsForPrecision(size), equalTo(quadPrefixTree.getLevelForDistance(degrees)));
assertThat("width at level "+i, qNode.getShape().getBoundingBox().getWidth(), equalTo(360.d * width / GeoUtils.EARTH_EQUATOR));
assertThat("height at level "+i, qNode.getShape().getBoundingBox().getHeight(), equalTo(180.d * height / GeoUtils.EARTH_POLAR_DISTANCE));
qNode = qNode.getSubCells(null).iterator().next();
}
}
private static void assertNormalizedPoint(GeoPoint input, GeoPoint expected) {
GeoUtils.normalizePoint(input);
assertThat(input, equalTo(expected));