package org.apache.lucene.spatial.prefix;
import java.io.IOException;
import java.util.Arrays;
import org.locationtech.spatial4j.shape.Shape;
import org.locationtech.spatial4j.shape.SpatialRelation;
import org.apache.lucene.index.LeafReaderContext;
import org.apache.lucene.index.PostingsEnum;
import org.apache.lucene.index.TermsEnum;
import org.apache.lucene.search.DocIdSet;
import org.apache.lucene.search.DocIdSetIterator;
import org.apache.lucene.spatial.prefix.tree.Cell;
import org.apache.lucene.spatial.prefix.tree.CellIterator;
import org.apache.lucene.spatial.prefix.tree.SpatialPrefixTree;
import org.apache.lucene.util.Bits;
import org.apache.lucene.util.BytesRef;
import org.apache.lucene.util.RamUsageEstimator;
import org.apache.lucene.util.SentinelIntSet;
public class ContainsPrefixTreeQuery extends AbstractPrefixTreeQuery {
protected final boolean multiOverlappingIndexedShapes;
public ContainsPrefixTreeQuery(Shape queryShape, String fieldName, SpatialPrefixTree grid, int detailLevel, boolean multiOverlappingIndexedShapes) {
super(queryShape, fieldName, grid, detailLevel);
this.multiOverlappingIndexedShapes = multiOverlappingIndexedShapes;
}
@Override
public boolean equals(Object o) {
if (!super.equals(o))
return false;
return multiOverlappingIndexedShapes == ((ContainsPrefixTreeQuery)o).multiOverlappingIndexedShapes;
}
@Override
public int hashCode() {
return super.hashCode() + (multiOverlappingIndexedShapes ? 1 : 0);
}
@Override
public String toString(String field) {
return getClass().getSimpleName() + "(" +
"fieldName=" + fieldName + "," +
"queryShape=" + queryShape + "," +
"detailLevel=" + detailLevel + "," +
"multiOverlappingIndexedShapes=" + multiOverlappingIndexedShapes +
")";
}
@Override
protected DocIdSet getDocIdSet(LeafReaderContext context) throws IOException {
return new ContainsVisitor(context).visit(grid.getWorldCell(), null);
}
private class ContainsVisitor extends BaseTermsEnumTraverser {
public ContainsVisitor(LeafReaderContext context) throws IOException {
super(context);
if (termsEnum != null) {
nextTerm();
}
}
BytesRef seekTerm = new BytesRef();
BytesRef thisTerm;
Cell indexedCell;
private SmallDocSet visit(Cell cell, Bits acceptContains) throws IOException {
if (thisTerm == null)
return null;
SmallDocSet combinedSubResults = null;
Shape subCellsFilter = queryShape;
if (cell.getLevel() != 0 && ((cell.getShapeRel() == null || cell.getShapeRel() == SpatialRelation.WITHIN))) {
subCellsFilter = null;
assert cell.getShape().relate(queryShape) == SpatialRelation.WITHIN;
}
CellIterator subCells = cell.getNextLevelCells(subCellsFilter);
while (subCells.hasNext()) {
Cell subCell = subCells.next();
if (!seek(subCell)) {
combinedSubResults = null;
} else if (subCell.getLevel() == detailLevel) {
combinedSubResults = getDocs(subCell, acceptContains);
} else if (!multiOverlappingIndexedShapes &&
subCell.getShapeRel() == SpatialRelation.WITHIN) {
combinedSubResults = getLeafDocs(subCell, acceptContains);
} else {
SmallDocSet leafDocs = getLeafDocs(subCell, acceptContains);
SmallDocSet subDocs = visit(subCell, acceptContains);
combinedSubResults = union(leafDocs, subDocs);
}
if (combinedSubResults == null)
break;
acceptContains = combinedSubResults;
}
return combinedSubResults;
}
private boolean seek(Cell cell) throws IOException {
if (thisTerm == null)
return false;
final int compare = indexedCell.compareToNoLeaf(cell);
if (compare > 0) {
return false;
} else if (compare == 0) {
return true;
} else {
seekTerm = cell.getTokenBytesNoLeaf(seekTerm);
final TermsEnum.SeekStatus seekStatus = termsEnum.seekCeil(seekTerm);
if (seekStatus == TermsEnum.SeekStatus.END) {
thisTerm = null;
return false;
}
thisTerm = termsEnum.term();
indexedCell = grid.readCell(thisTerm, indexedCell);
if (seekStatus == TermsEnum.SeekStatus.FOUND) {
return true;
}
return indexedCell.isLeaf() && indexedCell.compareToNoLeaf(cell) == 0;
}
}
private SmallDocSet getDocs(Cell cell, Bits acceptContains) throws IOException {
assert indexedCell.compareToNoLeaf(cell) == 0;
if (indexedCell.isLeaf()) {
SmallDocSet result = collectDocs(acceptContains);
nextTerm();
return result;
} else {
SmallDocSet docsAtPrefix = collectDocs(acceptContains);
if (!nextTerm()) {
return docsAtPrefix;
}
if (indexedCell.isLeaf() && indexedCell.compareToNoLeaf(cell) == 0) {
SmallDocSet docsAtLeaf = collectDocs(acceptContains);
nextTerm();
return union(docsAtPrefix, docsAtLeaf);
} else {
return docsAtPrefix;
}
}
}
private SmallDocSet getLeafDocs(Cell cell, Bits acceptContains) throws IOException {
assert indexedCell.compareToNoLeaf(cell) == 0;
if (!indexedCell.isLeaf()) {
if (!nextTerm() || !indexedCell.isLeaf() || indexedCell.getLevel() != cell.getLevel()) {
return null;
}
}
SmallDocSet result = collectDocs(acceptContains);
nextTerm();
return result;
}
private boolean nextTerm() throws IOException {
if ((thisTerm = termsEnum.next()) == null)
return false;
indexedCell = grid.readCell(thisTerm, indexedCell);
return true;
}
private SmallDocSet union(SmallDocSet aSet, SmallDocSet bSet) {
if (bSet != null) {
if (aSet == null)
return bSet;
return aSet.union(bSet);
}
return aSet;
}
private SmallDocSet collectDocs(Bits acceptContains) throws IOException {
SmallDocSet set = null;
postingsEnum = termsEnum.postings(postingsEnum, PostingsEnum.NONE);
int docid;
while ((docid = postingsEnum.nextDoc()) != DocIdSetIterator.NO_MORE_DOCS) {
if (acceptContains != null && acceptContains.get(docid) == false) {
continue;
}
if (set == null) {
int size = termsEnum.docFreq();
if (size <= 0)
size = 16;
set = new SmallDocSet(size);
}
set.set(docid);
}
return set;
}
}
private static class SmallDocSet extends DocIdSet implements Bits {
private final SentinelIntSet intSet;
private int maxInt = 0;
public SmallDocSet(int size) {
intSet = new SentinelIntSet(size, -1);
}
@Override
public boolean get(int index) {
return intSet.exists(index);
}
public void set(int index) {
intSet.put(index);
if (index > maxInt)
maxInt = index;
}
@Override
public int length() {
return maxInt;
}
public int size() {
return intSet.size();
}
public SmallDocSet union(SmallDocSet other) {
SmallDocSet bigger;
SmallDocSet smaller;
if (other.intSet.size() > this.intSet.size()) {
bigger = other;
smaller = this;
} else {
bigger = this;
smaller = other;
}
for (int v : smaller.intSet.keys) {
if (v == smaller.intSet.emptyVal)
continue;
bigger.set(v);
}
return bigger;
}
@Override
public Bits bits() throws IOException {
return size() > 4 ? this : null;
}
@Override
public DocIdSetIterator iterator() throws IOException {
if (size() == 0)
return null;
int d = 0;
final int[] docs = new int[intSet.size()];
for (int v : intSet.keys) {
if (v == intSet.emptyVal)
continue;
docs[d++] = v;
}
assert d == intSet.size();
final int size = d;
Arrays.sort(docs, 0, size);
return new DocIdSetIterator() {
int idx = -1;
@Override
public int docID() {
if (idx < 0) {
return -1;
} else if (idx < size) {
return docs[idx];
} else {
return NO_MORE_DOCS;
}
}
@Override
public int nextDoc() throws IOException {
if (++idx < size)
return docs[idx];
return NO_MORE_DOCS;
}
@Override
public int advance(int target) throws IOException {
return slowAdvance(target);
}
@Override
public long cost() {
return size;
}
};
}
@Override
public long ramBytesUsed() {
return RamUsageEstimator.alignObjectSize(
RamUsageEstimator.NUM_BYTES_OBJECT_REF
+ Integer.BYTES)
+ intSet.ramBytesUsed();
}
}
}