/*
* 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;
import org.apache.lucene.index.FieldInfo.IndexOptions;
import org.elasticsearch.ElasticSearchIllegalArgumentException;
import org.elasticsearch.common.Strings;
import org.elasticsearch.common.xcontent.XContentBuilder;
import org.elasticsearch.common.xcontent.XContentParser;
import org.elasticsearch.common.xcontent.support.XContentMapValues;
import org.elasticsearch.index.analysis.NamedAnalyzer;
import org.elasticsearch.index.field.data.FieldDataType;
import org.elasticsearch.index.mapper.*;
import org.elasticsearch.index.mapper.core.AbstractFieldMapper;
import org.elasticsearch.index.mapper.core.DoubleFieldMapper;
import org.elasticsearch.index.mapper.core.NumberFieldMapper;
import org.elasticsearch.index.mapper.core.StringFieldMapper;
import org.elasticsearch.index.mapper.object.ArrayValueMapperParser;
import org.elasticsearch.index.search.geo.GeoHashUtils;
import org.elasticsearch.index.search.geo.GeoUtils;
import org.elasticsearch.index.search.geo.Point;
import java.io.IOException;
import java.util.Map;
import static org.elasticsearch.index.mapper.MapperBuilders.doubleField;
import static org.elasticsearch.index.mapper.MapperBuilders.stringField;
import static org.elasticsearch.index.mapper.core.TypeParsers.parsePathType;
import static org.elasticsearch.index.mapper.core.TypeParsers.parseStore;
/**
* Parsing: We handle:
* <p/>
* - "field" : "geo_hash"
* - "field" : "lat,lon"
* - "field" : {
* "lat" : 1.1,
* "lon" : 2.1
* }
*
*
*/
public class GeoPointFieldMapper implements Mapper, ArrayValueMapperParser {
public static final String CONTENT_TYPE = "geo_point";
public static class Names {
public static final String LAT = "lat";
public static final String LAT_SUFFIX = "." + LAT;
public static final String LON = "lon";
public static final String LON_SUFFIX = "." + LON;
public static final String GEOHASH = "geohash";
public static final String GEOHASH_SUFFIX = "." + GEOHASH;
}
public static class Defaults {
public static final ContentPath.Type PATH_TYPE = ContentPath.Type.FULL;
public static final Field.Store STORE = Field.Store.NO;
public static final boolean ENABLE_LATLON = false;
public static final boolean ENABLE_GEOHASH = false;
public static final int PRECISION = GeoHashUtils.PRECISION;
public static final boolean NORMALIZE_LAT = true;
public static final boolean NORMALIZE_LON = true;
public static final boolean VALIDATE_LAT = true;
public static final boolean VALIDATE_LON = true;
}
public static class Builder extends Mapper.Builder<Builder, GeoPointFieldMapper> {
private ContentPath.Type pathType = Defaults.PATH_TYPE;
private boolean enableGeoHash = Defaults.ENABLE_GEOHASH;
private boolean enableLatLon = Defaults.ENABLE_LATLON;
private Integer precisionStep;
private int precision = Defaults.PRECISION;
private Field.Store store = Defaults.STORE;
boolean validateLat = Defaults.VALIDATE_LAT;
boolean validateLon = Defaults.VALIDATE_LON;
boolean normalizeLat = Defaults.NORMALIZE_LAT;
boolean normalizeLon = Defaults.NORMALIZE_LON;
public Builder(String name) {
super(name);
this.builder = this;
}
public Builder pathType(ContentPath.Type pathType) {
this.pathType = pathType;
return this;
}
public Builder enableGeoHash(boolean enableGeoHash) {
this.enableGeoHash = enableGeoHash;
return this;
}
public Builder enableLatLon(boolean enableLatLon) {
this.enableLatLon = enableLatLon;
return this;
}
public Builder precisionStep(int precisionStep) {
this.precisionStep = precisionStep;
return this;
}
public Builder precision(int precision) {
this.precision = precision;
return this;
}
public Builder store(Field.Store store) {
this.store = store;
return this;
}
@Override
public GeoPointFieldMapper build(BuilderContext context) {
ContentPath.Type origPathType = context.path().pathType();
context.path().pathType(pathType);
GeoStringFieldMapper geoStringMapper = new GeoStringFieldMapper.Builder(name)
.index(Field.Index.NOT_ANALYZED).omitNorms(true).indexOptions(IndexOptions.DOCS_ONLY).includeInAll(false).store(store).build(context);
DoubleFieldMapper latMapper = null;
DoubleFieldMapper lonMapper = null;
context.path().add(name);
if (enableLatLon) {
NumberFieldMapper.Builder latMapperBuilder = doubleField(Names.LAT).includeInAll(false);
NumberFieldMapper.Builder lonMapperBuilder = doubleField(Names.LON).includeInAll(false);
if (precisionStep != null) {
latMapperBuilder.precisionStep(precisionStep);
lonMapperBuilder.precisionStep(precisionStep);
}
latMapper = (DoubleFieldMapper) latMapperBuilder.includeInAll(false).store(store).build(context);
lonMapper = (DoubleFieldMapper) lonMapperBuilder.includeInAll(false).store(store).build(context);
}
StringFieldMapper geohashMapper = null;
if (enableGeoHash) {
geohashMapper = stringField(Names.GEOHASH).index(Field.Index.NOT_ANALYZED).includeInAll(false).omitNorms(true).indexOptions(IndexOptions.DOCS_ONLY).build(context);
}
context.path().remove();
context.path().pathType(origPathType);
return new GeoPointFieldMapper(name, pathType, enableLatLon, enableGeoHash, precisionStep, precision,
latMapper, lonMapper, geohashMapper, geoStringMapper,
validateLon, validateLat, normalizeLon, normalizeLat);
}
}
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 (fieldName.equals("path")) {
builder.pathType(parsePathType(name, fieldNode.toString()));
} else if (fieldName.equals("store")) {
builder.store(parseStore(name, fieldNode.toString()));
} else if (fieldName.equals("lat_lon")) {
builder.enableLatLon(XContentMapValues.nodeBooleanValue(fieldNode));
} else if (fieldName.equals("geohash")) {
builder.enableGeoHash(XContentMapValues.nodeBooleanValue(fieldNode));
} else if (fieldName.equals("precision_step")) {
builder.precisionStep(XContentMapValues.nodeIntegerValue(fieldNode));
} else if (fieldName.equals("geohash_precision")) {
builder.precision(XContentMapValues.nodeIntegerValue(fieldNode));
} else if (fieldName.equals("validate")) {
builder.validateLat = XContentMapValues.nodeBooleanValue(fieldNode);
builder.validateLon = XContentMapValues.nodeBooleanValue(fieldNode);
} else if (fieldName.equals("validate_lon")) {
builder.validateLon = XContentMapValues.nodeBooleanValue(fieldNode);
} else if (fieldName.equals("validate_lat")) {
builder.validateLat = XContentMapValues.nodeBooleanValue(fieldNode);
} else if (fieldName.equals("normalize")) {
builder.normalizeLat = XContentMapValues.nodeBooleanValue(fieldNode);
builder.normalizeLon = XContentMapValues.nodeBooleanValue(fieldNode);
} else if (fieldName.equals("normalize_lat")) {
builder.normalizeLat = XContentMapValues.nodeBooleanValue(fieldNode);
} else if (fieldName.equals("normalize_lon")) {
builder.normalizeLon = XContentMapValues.nodeBooleanValue(fieldNode);
}
}
return builder;
}
}
private final String name;
private final ContentPath.Type pathType;
private final boolean enableLatLon;
private final boolean enableGeoHash;
private final Integer precisionStep;
private final int precision;
private final DoubleFieldMapper latMapper;
private final DoubleFieldMapper lonMapper;
private final StringFieldMapper geohashMapper;
private final GeoStringFieldMapper geoStringMapper;
private final boolean validateLon;
private final boolean validateLat;
private final boolean normalizeLon;
private final boolean normalizeLat;
public GeoPointFieldMapper(String name, ContentPath.Type pathType, boolean enableLatLon, boolean enableGeoHash, Integer precisionStep, int precision,
DoubleFieldMapper latMapper, DoubleFieldMapper lonMapper, StringFieldMapper geohashMapper, GeoStringFieldMapper geoStringMapper,
boolean validateLon, boolean validateLat,
boolean normalizeLon, boolean normalizeLat) {
this.name = name;
this.pathType = pathType;
this.enableLatLon = enableLatLon;
this.enableGeoHash = enableGeoHash;
this.precisionStep = precisionStep;
this.precision = precision;
this.latMapper = latMapper;
this.lonMapper = lonMapper;
this.geoStringMapper = geoStringMapper;
this.geohashMapper = geohashMapper;
this.geoStringMapper.geoMapper = this;
this.validateLat = validateLat;
this.validateLon = validateLon;
this.normalizeLat = normalizeLat;
this.normalizeLon = normalizeLon;
}
@Override
public String name() {
return this.name;
}
public DoubleFieldMapper latMapper() {
return latMapper;
}
public DoubleFieldMapper lonMapper() {
return lonMapper;
}
public boolean isEnableLatLon() {
return enableLatLon;
}
@Override
public void parse(ParseContext context) throws IOException {
ContentPath.Type origPathType = context.path().pathType();
context.path().pathType(pathType);
context.path().add(name);
XContentParser.Token token = context.parser().currentToken();
if (token == XContentParser.Token.START_ARRAY) {
token = context.parser().nextToken();
if (token == XContentParser.Token.START_ARRAY) {
// its an array of array of lon/lat [ [1.2, 1.3], [1.4, 1.5] ]
while (token != XContentParser.Token.END_ARRAY) {
token = context.parser().nextToken();
double lon = context.parser().doubleValue();
token = context.parser().nextToken();
double lat = context.parser().doubleValue();
while ((token = context.parser().nextToken()) != XContentParser.Token.END_ARRAY) {
}
parseLatLon(context, lat, lon);
token = context.parser().nextToken();
}
} else {
// its an array of other possible values
if (token == XContentParser.Token.VALUE_NUMBER) {
double lon = context.parser().doubleValue();
token = context.parser().nextToken();
double lat = context.parser().doubleValue();
while ((token = context.parser().nextToken()) != XContentParser.Token.END_ARRAY) {
}
parseLatLon(context, lat, lon);
} else {
while (token != XContentParser.Token.END_ARRAY) {
if (token == XContentParser.Token.START_OBJECT) {
parseObjectLatLon(context);
} else if (token == XContentParser.Token.VALUE_STRING) {
parseStringLatLon(context);
}
token = context.parser().nextToken();
}
}
}
} else if (token == XContentParser.Token.START_OBJECT) {
parseObjectLatLon(context);
} else if (token == XContentParser.Token.VALUE_STRING) {
parseStringLatLon(context);
}
context.path().remove();
context.path().pathType(origPathType);
}
private void parseStringLatLon(ParseContext context) throws IOException {
String value = context.parser().text();
int comma = value.indexOf(',');
if (comma != -1) {
double lat = Double.parseDouble(value.substring(0, comma).trim());
double lon = Double.parseDouble(value.substring(comma + 1).trim());
parseLatLon(context, lat, lon);
} else { // geo hash
parseGeohash(context, value);
}
}
private void parseObjectLatLon(ParseContext context) throws IOException {
XContentParser.Token token;
String currentName = context.parser().currentName();
Double lat = null;
Double lon = null;
String geohash = null;
while ((token = context.parser().nextToken()) != XContentParser.Token.END_OBJECT) {
if (token == XContentParser.Token.FIELD_NAME) {
currentName = context.parser().currentName();
} else if (token.isValue()) {
if (currentName.equals(Names.LAT)) {
lat = context.parser().doubleValue();
} else if (currentName.equals(Names.LON)) {
lon = context.parser().doubleValue();
} else if (currentName.equals(Names.GEOHASH)) {
geohash = context.parser().text();
}
}
}
if (geohash != null) {
parseGeohash(context, geohash);
} else if (lat != null && lon != null) {
parseLatLon(context, lat, lon);
}
}
private void parseLatLon(ParseContext context, double lat, double lon) throws IOException {
if (normalizeLat || normalizeLon) {
Point point = new Point(lat, lon);
GeoUtils.normalizePoint(point, normalizeLat, normalizeLon);
lat = point.lat;
lon = point.lon;
}
if (validateLat) {
if (lat > 90.0 || lat < -90.0) {
throw new ElasticSearchIllegalArgumentException("illegal latitude value [" + lat + "] for " + name);
}
}
if (validateLon) {
if (lon > 180.0 || lon < -180) {
throw new ElasticSearchIllegalArgumentException("illegal longitude value [" + lon + "] for " + name);
}
}
context.externalValue(Double.toString(lat) + ',' + Double.toString(lon));
geoStringMapper.parse(context);
if (enableGeoHash) {
context.externalValue(GeoHashUtils.encode(lat, lon, precision));
geohashMapper.parse(context);
}
if (enableLatLon) {
context.externalValue(lat);
latMapper.parse(context);
context.externalValue(lon);
lonMapper.parse(context);
}
}
private void parseGeohash(ParseContext context, String geohash) throws IOException {
double[] values = GeoHashUtils.decode(geohash);
double lat = values[0];
double lon = values[1];
if (normalizeLat || normalizeLon) {
Point point = new Point(lat, lon);
GeoUtils.normalizePoint(point, normalizeLat, normalizeLon);
lat = point.lat;
lon = point.lon;
}
if (validateLat) {
if (lat > 90.0 || lat < -90.0) {
throw new ElasticSearchIllegalArgumentException("illegal latitude value [" + lat + "] for " + name);
}
}
if (validateLon) {
if (lon > 180.0 || lon < -180) {
throw new ElasticSearchIllegalArgumentException("illegal longitude value [" + lon + "] for " + name);
}
}
context.externalValue(Double.toString(lat) + ',' + Double.toString(lon));
geoStringMapper.parse(context);
if (enableGeoHash) {
context.externalValue(geohash);
geohashMapper.parse(context);
}
if (enableLatLon) {
context.externalValue(lat);
latMapper.parse(context);
context.externalValue(lon);
lonMapper.parse(context);
}
}
@Override
public void close() {
if (latMapper != null) {
latMapper.close();
}
if (lonMapper != null) {
lonMapper.close();
}
if (geohashMapper != null) {
geohashMapper.close();
}
if (geoStringMapper != null) {
geoStringMapper.close();
}
}
@Override
public void merge(Mapper mergeWith, MergeContext mergeContext) throws MergeMappingException {
// TODO
}
@Override
public void traverse(FieldMapperListener fieldMapperListener) {
geoStringMapper.traverse(fieldMapperListener);
if (enableGeoHash) {
geohashMapper.traverse(fieldMapperListener);
}
if (enableLatLon) {
latMapper.traverse(fieldMapperListener);
lonMapper.traverse(fieldMapperListener);
}
}
@Override
public void traverse(ObjectMapperListener objectMapperListener) {
}
@Override
public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
builder.startObject(name);
builder.field("type", CONTENT_TYPE);
if (pathType != Defaults.PATH_TYPE) {
builder.field("path", pathType.name().toLowerCase());
}
if (enableLatLon != Defaults.ENABLE_LATLON) {
builder.field("lat_lon", enableLatLon);
}
if (enableGeoHash != Defaults.ENABLE_GEOHASH) {
builder.field("geohash", enableGeoHash);
}
if (geoStringMapper.store() != Defaults.STORE) {
builder.field("store", geoStringMapper.store().name().toLowerCase());
}
if (precision != Defaults.PRECISION) {
builder.field("geohash_precision", precision);
}
if (precisionStep != null) {
builder.field("precision_step", precisionStep);
}
if (!validateLat && !validateLon) {
builder.field("validate", false);
} else {
if (validateLat != Defaults.VALIDATE_LAT) {
builder.field("validate_lat", validateLat);
}
if (validateLon != Defaults.VALIDATE_LON) {
builder.field("validate_lon", validateLon);
}
}
if (!normalizeLat && !normalizeLon) {
builder.field("normalize", false);
} else {
if (normalizeLat != Defaults.NORMALIZE_LAT) {
builder.field("normalize_lat", normalizeLat);
}
if (normalizeLon != Defaults.NORMALIZE_LON) {
builder.field("normalize_lon", normalizeLon);
}
}
builder.endObject();
return builder;
}
public static class GeoStringFieldMapper extends StringFieldMapper {
public static class Builder extends AbstractFieldMapper.OpenBuilder<Builder, StringFieldMapper> {
protected String nullValue = Defaults.NULL_VALUE;
public Builder(String name) {
super(name);
builder = this;
}
public Builder nullValue(String nullValue) {
this.nullValue = nullValue;
return this;
}
@Override
public Builder includeInAll(Boolean includeInAll) {
this.includeInAll = includeInAll;
return this;
}
@Override
public GeoStringFieldMapper build(BuilderContext context) {
GeoStringFieldMapper fieldMapper = new GeoStringFieldMapper(buildNames(context),
index, store, termVector, boost, omitNorms, indexOptions, nullValue,
indexAnalyzer, searchAnalyzer);
fieldMapper.includeInAll(includeInAll);
return fieldMapper;
}
}
GeoPointFieldMapper geoMapper;
public GeoStringFieldMapper(Names names, Field.Index index, Field.Store store, Field.TermVector termVector, float boost, boolean omitNorms, IndexOptions indexOptions, String nullValue, NamedAnalyzer indexAnalyzer, NamedAnalyzer searchAnalyzer) {
super(names, index, store, termVector, boost, omitNorms, indexOptions, nullValue, indexAnalyzer, searchAnalyzer);
}
@Override
public FieldDataType fieldDataType() {
return GeoPointFieldDataType.TYPE;
}
public GeoPointFieldMapper geoMapper() {
return geoMapper;
}
}
}