package org.elasticsearch.index.mapper.geo; import org.apache.lucene.document.Field; import org.apache.lucene.document.Fieldable; import org.apache.lucene.index.FieldInfo; 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.lucene.spatial.SpatialStrategy; import org.elasticsearch.common.lucene.spatial.prefix.TermQueryPrefixTreeStrategy; import org.elasticsearch.common.lucene.spatial.prefix.tree.GeohashPrefixTree; import org.elasticsearch.common.lucene.spatial.prefix.tree.QuadPrefixTree; import org.elasticsearch.common.lucene.spatial.prefix.tree.SpatialPrefixTree; import org.elasticsearch.common.xcontent.XContentBuilder; import org.elasticsearch.index.mapper.FieldMapper; import org.elasticsearch.index.mapper.Mapper; import org.elasticsearch.index.mapper.MapperParsingException; import org.elasticsearch.index.mapper.ParseContext; import org.elasticsearch.index.mapper.core.AbstractFieldMapper; import java.io.IOException; import java.util.Map; /** * FieldMapper for indexing {@link com.spatial4j.core.shape.Shape}s. * <p/> * Currently Shapes can only be indexed and can only be queried using * {@link org.elasticsearch.index.query.GeoShapeFilterParser}, consequently * a lot of behavior in this Mapper is disabled. * <p/> * Format supported: * <p/> * "field" : { * "type" : "polygon", * "coordinates" : [ * [ [100.0, 0.0], [101.0, 0.0], [101.0, 1.0], [100.0, 1.0], [100.0, 0.0] ] * ] * } */ public class GeoShapeFieldMapper extends AbstractFieldMapper<String> { public static final String CONTENT_TYPE = "geo_shape"; public static class Names { public static final String TREE = "tree"; public static final String TREE_LEVELS = "tree_levels"; public static final String GEOHASH = "geohash"; public static final String QUADTREE = "quadtree"; public static final String DISTANCE_ERROR_PCT = "distance_error_pct"; } public static class Defaults { public static final String TREE = Names.GEOHASH; public static final int GEOHASH_LEVELS = GeohashPrefixTree.getMaxLevelsPossible(); public static final int QUADTREE_LEVELS = QuadPrefixTree.DEFAULT_MAX_LEVELS; public static final double DISTANCE_ERROR_PCT = 0.025d; } public static class Builder extends AbstractFieldMapper.Builder<Builder, GeoShapeFieldMapper> { private String tree = Defaults.TREE; private int treeLevels; private double distanceErrorPct = Defaults.DISTANCE_ERROR_PCT; private SpatialPrefixTree prefixTree; public Builder(String name) { super(name); } public Builder tree(String tree) { this.tree = tree; return this; } public Builder treeLevels(int treeLevels) { this.treeLevels = treeLevels; return this; } public Builder distanceErrorPct(double distanceErrorPct) { this.distanceErrorPct = distanceErrorPct; return this; } @Override public GeoShapeFieldMapper build(BuilderContext context) { if (tree.equals(Names.GEOHASH)) { int levels = treeLevels != 0 ? treeLevels : Defaults.GEOHASH_LEVELS; prefixTree = new GeohashPrefixTree(GeoShapeConstants.SPATIAL_CONTEXT, levels); } else if (tree.equals(Names.QUADTREE)) { int levels = treeLevels != 0 ? treeLevels : Defaults.QUADTREE_LEVELS; prefixTree = new QuadPrefixTree(GeoShapeConstants.SPATIAL_CONTEXT, levels); } else { throw new ElasticSearchIllegalArgumentException("Unknown prefix tree type [" + tree + "]"); } return new GeoShapeFieldMapper(buildNames(context), prefixTree, distanceErrorPct); } } public static class TypeParser implements Mapper.TypeParser { @Override public Mapper.Builder parse(String name, Map<String, Object> node, ParserContext parserContext) throws MapperParsingException { Builder builder = new Builder(name); for (Map.Entry<String, Object> entry : node.entrySet()) { String fieldName = Strings.toUnderscoreCase(entry.getKey()); Object fieldNode = entry.getValue(); if (Names.TREE.equals(fieldName)) { builder.tree(fieldNode.toString()); } else if (Names.TREE_LEVELS.equals(fieldName)) { builder.treeLevels(Integer.parseInt(fieldNode.toString())); } else if (Names.DISTANCE_ERROR_PCT.equals(fieldName)) { builder.distanceErrorPct(Double.parseDouble(fieldNode.toString())); } } return builder; } } private final SpatialStrategy spatialStrategy; public GeoShapeFieldMapper(FieldMapper.Names names, SpatialPrefixTree prefixTree, double distanceErrorPct) { super(names, Field.Index.NOT_ANALYZED, Field.Store.NO, Field.TermVector.NO, 1, true, FieldInfo.IndexOptions.DOCS_ONLY, null, null); this.spatialStrategy = new TermQueryPrefixTreeStrategy(names, prefixTree, distanceErrorPct); } @Override protected Fieldable parseCreateField(ParseContext context) throws IOException { return spatialStrategy.createField(GeoJSONShapeParser.parse(context.parser())); } @Override protected void doXContentBody(XContentBuilder builder) throws IOException { builder.field("type", contentType()); // TODO: Come up with a better way to get the name, maybe pass it from builder if (spatialStrategy.getPrefixTree() instanceof GeohashPrefixTree) { // Don't emit the tree name since GeohashPrefixTree is the default // Only emit the tree levels if it isn't the default value if (spatialStrategy.getPrefixTree().getMaxLevels() != Defaults.GEOHASH_LEVELS) { builder.field(Names.TREE_LEVELS, spatialStrategy.getPrefixTree().getMaxLevels()); } } else { builder.field(Names.TREE, Names.QUADTREE); if (spatialStrategy.getPrefixTree().getMaxLevels() != Defaults.QUADTREE_LEVELS) { builder.field(Names.TREE_LEVELS, spatialStrategy.getPrefixTree().getMaxLevels()); } } if (spatialStrategy.getDistanceErrorPct() != Defaults.DISTANCE_ERROR_PCT) { builder.field(Names.DISTANCE_ERROR_PCT, spatialStrategy.getDistanceErrorPct()); } } @Override protected String contentType() { return CONTENT_TYPE; } @Override public String value(Fieldable field) { throw new UnsupportedOperationException("GeoShape fields cannot be converted to String values"); } @Override public String valueFromString(String value) { throw new UnsupportedOperationException("GeoShape fields cannot be converted to String values"); } @Override public String valueAsString(Fieldable field) { throw new UnsupportedOperationException("GeoShape fields cannot be converted to String values"); } public SpatialStrategy spatialStrategy() { return this.spatialStrategy; } }