/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF 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.apache.lucene.spatial.geopoint.document;
import org.apache.lucene.analysis.Analyzer;
import org.apache.lucene.analysis.TokenStream;
import org.apache.lucene.document.Field;
import org.apache.lucene.document.FieldType;
import org.apache.lucene.index.DocValuesType;
import org.apache.lucene.index.IndexOptions;
import org.apache.lucene.geo.GeoUtils;
import org.apache.lucene.util.BitUtil;
import org.apache.lucene.util.BytesRef;
import org.apache.lucene.util.BytesRefBuilder;
import static org.apache.lucene.spatial.util.MortonEncoder.encode;
import static org.apache.lucene.geo.GeoUtils.MIN_LAT_INCL;
import static org.apache.lucene.geo.GeoUtils.MIN_LON_INCL;
/**
* <p>
* Field that indexes <code>latitude</code> <code>longitude</code> decimal-degree values
* for efficient encoding, sorting, and querying. This Geo capability is intended
* to provide a basic and efficient out of the box field type for indexing and
* querying 2 dimensional points in WGS-84 decimal degrees. An example usage is as follows:
*
* <pre class="prettyprint">
* document.add(new GeoPointField(name, -96.33, 32.66, Field.Store.NO));
* </pre>
*
* <p>To perform simple geospatial queries against a <code>GeoPointField</code>,
* see {@link org.apache.lucene.spatial.geopoint.search.GeoPointInBBoxQuery}, {@link org.apache.lucene.spatial.geopoint.search.GeoPointInPolygonQuery},
* or {@link org.apache.lucene.spatial.geopoint.search.GeoPointDistanceQuery}
*
* @lucene.experimental
*/
public final class GeoPointField extends Field {
/** encoding step value for GeoPoint prefix terms */
public static final int PRECISION_STEP = 9;
/** number of bits used for quantizing latitude and longitude values */
public static final short BITS = 31;
/** scaling factors to convert lat/lon into unsigned space */
private static final double LAT_SCALE = (0x1L<<BITS)/180.0D;
private static final double LON_SCALE = (0x1L<<BITS)/360.0D;
/**
* The maximum term length (used for <code>byte[]</code> buffer size)
* for encoding <code>geoEncoded</code> values.
* @see #geoCodedToPrefixCodedBytes(long, int, BytesRefBuilder)
*/
private static final int BUF_SIZE_LONG = 28/8 + 1;
/**
* Type for a GeoPointField that is not stored:
* normalization factors, frequencies, and positions are omitted.
*/
public static final FieldType TYPE_NOT_STORED = new FieldType();
static {
TYPE_NOT_STORED.setTokenized(false);
TYPE_NOT_STORED.setOmitNorms(true);
TYPE_NOT_STORED.setIndexOptions(IndexOptions.DOCS);
TYPE_NOT_STORED.setDocValuesType(DocValuesType.SORTED_NUMERIC);
TYPE_NOT_STORED.freeze();
}
/**
* Type for a stored GeoPointField:
* normalization factors, frequencies, and positions are omitted.
*/
public static final FieldType TYPE_STORED = new FieldType();
static {
TYPE_STORED.setTokenized(false);
TYPE_STORED.setOmitNorms(true);
TYPE_STORED.setIndexOptions(IndexOptions.DOCS);
TYPE_STORED.setDocValuesType(DocValuesType.SORTED_NUMERIC);
TYPE_STORED.setStored(true);
TYPE_STORED.freeze();
}
/** Creates a stored or un-stored GeoPointField
* @param name field name
* @param latitude latitude double value [-90.0 : 90.0]
* @param longitude longitude double value [-180.0 : 180.0]
* @param stored Store.YES if the content should also be stored
* @throws IllegalArgumentException if the field name is null.
*/
public GeoPointField(String name, double latitude, double longitude, Store stored) {
this(name, latitude, longitude, getFieldType(stored));
}
/** Expert: allows you to customize the {@link
* FieldType}.
* @param name field name
* @param latitude latitude double value [-90.0 : 90.0]
* @param longitude longitude double value [-180.0 : 180.0]
* @param type customized field type
* @throws IllegalArgumentException if the field name or type is null
*/
public GeoPointField(String name, double latitude, double longitude, FieldType type) {
super(name, type);
GeoUtils.checkLatitude(latitude);
GeoUtils.checkLongitude(longitude);
// field must be indexed
// todo does it make sense here to provide the ability to store a GeoPointField but not index?
if (type.indexOptions() == IndexOptions.NONE && type.stored() == false) {
throw new IllegalArgumentException("type.indexOptions() is set to NONE but type.stored() is false");
} else if (type.indexOptions() == IndexOptions.DOCS) {
if (type.docValuesType() != DocValuesType.SORTED_NUMERIC) {
throw new IllegalArgumentException("type.docValuesType() must be SORTED_NUMERIC but got " + type.docValuesType());
}
} else {
throw new IllegalArgumentException("type.indexOptions() must be one of NONE or DOCS but got " + type.indexOptions());
}
// set field data
fieldsData = encodeLatLon(latitude, longitude);
}
/**
* Static helper method for returning a valid FieldType based on termEncoding and stored options
*/
private static FieldType getFieldType(Store stored) {
if (stored == Store.YES) {
return TYPE_STORED;
} else if (stored == Store.NO) {
return TYPE_NOT_STORED;
} else {
throw new IllegalArgumentException("stored option must be NO or YES but got " + stored);
}
}
@Override
public TokenStream tokenStream(Analyzer analyzer, TokenStream reuse) {
if (fieldType().indexOptions() == IndexOptions.NONE) {
// not indexed
return null;
}
if (reuse instanceof GeoPointTokenStream == false) {
reuse = new GeoPointTokenStream();
}
final GeoPointTokenStream gpts = (GeoPointTokenStream)reuse;
gpts.setGeoCode(((Number) fieldsData).longValue());
return reuse;
}
/** access latitude value */
public double getLat() {
return decodeLatitude((long) fieldsData);
}
/** access longitude value */
public double getLon() {
return decodeLongitude((long) fieldsData);
}
@Override
public String toString() {
if (fieldsData == null) {
return null;
}
StringBuilder sb = new StringBuilder();
sb.append(decodeLatitude((long) fieldsData));
sb.append(',');
sb.append(decodeLongitude((long) fieldsData));
return sb.toString();
}
/*************************
* 31 bit encoding utils *
*************************/
public static long encodeLatLon(final double lat, final double lon) {
long result = encode(lat, lon);
if (result == 0xFFFFFFFFFFFFFFFFL) {
return result & 0xC000000000000000L;
}
return result >>> 2;
}
/** decode longitude value from morton encoded geo point */
public static final double decodeLongitude(final long hash) {
return unscaleLon(BitUtil.deinterleave(hash));
}
/** decode latitude value from morton encoded geo point */
public static final double decodeLatitude(final long hash) {
return unscaleLat(BitUtil.deinterleave(hash >>> 1));
}
private static final double unscaleLon(final long val) {
return (val / LON_SCALE) + MIN_LON_INCL;
}
private static final double unscaleLat(final long val) {
return (val / LAT_SCALE) + MIN_LAT_INCL;
}
/** Convert a geocoded morton long into a prefix coded geo term */
public static void geoCodedToPrefixCoded(long hash, int shift, BytesRefBuilder bytes) {
geoCodedToPrefixCodedBytes(hash, shift, bytes);
}
/** Convert a prefix coded geo term back into the geocoded morton long */
public static long prefixCodedToGeoCoded(final BytesRef val) {
final long result = 0L
| (val.bytes[val.offset+0] & 255L) << 24
| (val.bytes[val.offset+1] & 255L) << 16
| (val.bytes[val.offset+2] & 255L) << 8
| val.bytes[val.offset+3] & 255L;
return result << 32;
}
/**
* GeoTerms are coded using 4 prefix bytes + 1 byte to record number of prefix bits
*
* example prefix at shift 54 (yields 10 significant prefix bits):
* pppppppp pp000000 00000000 00000000 00001010
* (byte 1) (byte 2) (byte 3) (byte 4) (sigbits)
*/
private static void geoCodedToPrefixCodedBytes(final long hash, final int shift, final BytesRefBuilder bytes) {
// ensure shift is 32..63
if (shift < 32 || shift > 63) {
throw new IllegalArgumentException("Illegal shift value, must be 32..63; got shift=" + shift);
}
int nChars = BUF_SIZE_LONG + 1; // one extra for the byte that contains the number of significant bits
bytes.setLength(nChars);
bytes.grow(nChars--);
final int sigBits = 64 - shift;
bytes.setByteAt(BUF_SIZE_LONG, (byte)(sigBits));
long sortableBits = hash;
sortableBits >>>= shift;
sortableBits <<= 32 - sigBits;
do {
bytes.setByteAt(--nChars, (byte)(sortableBits));
sortableBits >>>= 8;
} while (nChars > 0);
}
/** Get the prefix coded geo term shift value */
public static int getPrefixCodedShift(final BytesRef val) {
final int shift = val.bytes[val.offset + BUF_SIZE_LONG];
if (shift > 63 || shift < 0)
throw new NumberFormatException("Invalid shift value (" + shift + ") in prefixCoded bytes (is encoded value really a geo point?)");
return shift;
}
}