/*
* 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.search;
import org.apache.lucene.index.FilteredTermsEnum;
import org.apache.lucene.index.PointValues.Relation;
import org.apache.lucene.index.TermsEnum;
import org.apache.lucene.util.BytesRef;
import org.apache.lucene.util.BytesRefBuilder;
import org.apache.lucene.spatial.geopoint.document.GeoPointField;
import static org.apache.lucene.spatial.geopoint.document.GeoPointField.decodeLatitude;
import static org.apache.lucene.spatial.geopoint.document.GeoPointField.decodeLongitude;
import static org.apache.lucene.spatial.geopoint.document.GeoPointField.geoCodedToPrefixCoded;
import static org.apache.lucene.spatial.geopoint.document.GeoPointField.prefixCodedToGeoCoded;
import static org.apache.lucene.spatial.geopoint.document.GeoPointField.getPrefixCodedShift;
/**
* Decomposes a given {@link GeoPointMultiTermQuery} into a set of terms that represent the query criteria. The terms
* are then enumerated by the {@link GeoPointTermQueryConstantScoreWrapper} and all docs whose GeoPoint fields match
* the prefix terms or pass the {@link GeoPointMultiTermQuery.CellComparator#postFilter} criteria are returned in the
* resulting DocIdSet.
*
* @lucene.experimental
*/
final class GeoPointTermsEnum extends FilteredTermsEnum {
private final short maxShift;
private final GeoPointMultiTermQuery.CellComparator relationImpl;
private final BytesRefBuilder currentCellBRB;
private final Range range;
private short shift; // shift mask
private long start; // range start as encoded long
private long end; // range end as encoded long
private boolean hasNext = false;
public GeoPointTermsEnum(final TermsEnum tenum, final GeoPointMultiTermQuery query) {
super(tenum);
this.maxShift = query.maxShift;
this.relationImpl = query.cellComparator;
// start shift at maxShift value (from computeMaxShift)
this.shift = maxShift;
final long mask = (1L << shift) - 1;
this.start = query.minEncoded & ~mask;
this.end = start | mask;
this.currentCellBRB = new BytesRefBuilder();
this.range = new Range(-1, shift, true);
}
private boolean nextRelation() {
Relation relation;
do {
// within or a boundary
if ((shift % GeoPointField.PRECISION_STEP) == 0 &&
(relation = relationImpl.relate(decodeLatitude(start), decodeLatitude(end),
decodeLongitude(start), decodeLongitude(end))) != Relation.CELL_OUTSIDE_QUERY) {
// if at max depth or cell completely within
if (shift == maxShift || relation == Relation.CELL_INSIDE_QUERY) {
setRange(relation == Relation.CELL_CROSSES_QUERY);
advanceVariables();
return true;
}
}
// within cell but not at a depth factor of PRECISION_STEP
if (shift != maxShift && relationImpl.cellIntersectsMBR(start, end) == true) {
// descend: start need not change since shift handles end of range
end = start | (1L<<--shift) - 1;
} else {
advanceVariables();
}
} while(shift < 62);
return false;
}
private void setRange(final boolean boundary) {
range.start = start;
range.shift = shift;
range.boundary = boundary;
hasNext = true;
}
private void advanceVariables() {
/** set next variables */
long shiftMask = 1L << shift;
// pop-up if shift bit is set
while ((start & shiftMask) != 0) {
shiftMask = 1L << ++shift;
}
final long shiftMOne = shiftMask - 1;
start = start & ~shiftMOne | shiftMask;
end = start | shiftMOne;
}
private void seek(long term, short res) {
if (term < start && res < maxShift) {
throw new IllegalArgumentException("trying to seek backwards");
} else if (term == start && res == shift) {
return;
}
shift = res;
start = term;
end = start | ((1L<<shift)-1);
}
private final boolean hasNext() {
if (hasNext == false) {
return nextRelation();
}
return true;
}
@Override
protected final BytesRef nextSeekTerm(BytesRef term) {
if (hasNext() == false) {
return null;
}
geoCodedToPrefixCoded(range.start, range.shift, currentCellBRB);
hasNext = false;
return currentCellBRB.get();
}
/**
* The two-phase query approach. {@link #nextSeekTerm} is called to obtain the next term that matches a numeric
* range of the bounding box. Those terms that pass the initial range filter are then compared against the
* decoded min/max latitude and longitude values of the bounding box only if the range is not a "boundary" range
* (e.g., a range that straddles the boundary of the bbox).
* @param term term for candidate document
* @return match status
*/
@Override
protected AcceptStatus accept(BytesRef term) {
final long encodedTerm = prefixCodedToGeoCoded(term);
final short termShift = (short)(64-getPrefixCodedShift(term));
// range < term
while (range.compare(encodedTerm, termShift) < 0) {
// no more ranges, be gone
if (hasNext() == false) {
return AcceptStatus.END;
}
// peek next range, if the range > term then seek
final int peekCompare = range.compare(encodedTerm, termShift);
if (peekCompare > 0) {
return AcceptStatus.NO_AND_SEEK;
} else if (peekCompare < 0) {
seek(encodedTerm, termShift);
}
hasNext = false;
}
return AcceptStatus.YES;
}
/** Returns true if the current range term is a boundary of the query shape */
protected boolean boundaryTerm() {
if (range.start == -1) {
throw new IllegalStateException("GeoPointTermsEnum empty or not initialized");
}
return range.boundary;
}
protected boolean postFilter(final double lat, final double lon) {
return relationImpl.postFilter(lat, lon);
}
protected final class Range {
private short shift;
private long start;
private boolean boundary;
public Range(final long start, final short shift, final boolean boundary) {
this.boundary = boundary;
this.start = start;
this.shift = shift;
}
private int compare(long encoded, short shift) {
final int result = Long.compare(this.start, encoded);
if (result == 0) {
return Short.compare(shift, this.shift);
}
return result;
}
}
}