/* * 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.prefix; import java.io.IOException; import org.locationtech.spatial4j.context.SpatialContext; import org.locationtech.spatial4j.distance.DistanceUtils; import org.locationtech.spatial4j.shape.Circle; import org.locationtech.spatial4j.shape.Point; import org.locationtech.spatial4j.shape.Rectangle; import org.locationtech.spatial4j.shape.Shape; import org.locationtech.spatial4j.shape.SpatialRelation; import org.apache.lucene.index.LeafReaderContext; import org.apache.lucene.search.DocIdSet; 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.BitDocIdSet; import org.apache.lucene.util.FixedBitSet; /** * Finds docs where its indexed shape is {@link org.apache.lucene.spatial.query.SpatialOperation#IsWithin * WITHIN} the query shape. It works by looking at cells outside of the query * shape to ensure documents there are excluded. By default, it will * examine all cells, and it's fairly slow. If you know that the indexed shapes * are never comprised of multiple disjoint parts (which also means it is not multi-valued), * then you can pass {@code SpatialPrefixTree.getDistanceForLevel(maxLevels)} as * the {@code queryBuffer} constructor parameter to minimally look this distance * beyond the query shape's edge. Even if the indexed shapes are sometimes * comprised of multiple disjoint parts, you might want to use this option with * a large buffer as a faster approximation with minimal false-positives. * * @lucene.experimental */ public class WithinPrefixTreeQuery extends AbstractVisitingPrefixTreeQuery { //TODO LUCENE-4869: implement faster algorithm based on filtering out false-positives of a // minimal query buffer by looking in a DocValues cache holding a representative // point of each disjoint component of a document's shape(s). //TODO Could the recursion in allCellsIntersectQuery() be eliminated when non-fuzzy or other // circumstances? private final Shape bufferedQueryShape;//if null then the whole world /** * See {@link AbstractVisitingPrefixTreeQuery#AbstractVisitingPrefixTreeQuery(org.locationtech.spatial4j.shape.Shape, String, org.apache.lucene.spatial.prefix.tree.SpatialPrefixTree, int, int)}. * {@code queryBuffer} is the (minimum) distance beyond the query shape edge * where non-matching documents are looked for so they can be excluded. If * -1 is used then the whole world is examined (a good default for correctness). */ public WithinPrefixTreeQuery(Shape queryShape, String fieldName, SpatialPrefixTree grid, int detailLevel, int prefixGridScanLevel, double queryBuffer) { super(queryShape, fieldName, grid, detailLevel, prefixGridScanLevel); this.bufferedQueryShape = queryBuffer == -1 ? null : bufferShape(queryShape, queryBuffer); } @Override public boolean equals(Object o) { if (!super.equals(o)) return false;//checks getClass == o.getClass & instanceof WithinPrefixTreeQuery that = (WithinPrefixTreeQuery) o; if (bufferedQueryShape != null ? !bufferedQueryShape.equals(that.bufferedQueryShape) : that.bufferedQueryShape != null) return false; return true; } @Override public int hashCode() { int result = super.hashCode(); result = 31 * result + (bufferedQueryShape != null ? bufferedQueryShape.hashCode() : 0); return result; } @Override public String toString(String field) { return getClass().getSimpleName() + "(" + "fieldName=" + fieldName + "," + "queryShape=" + queryShape + "," + "detailLevel=" + detailLevel + "," + "prefixGridScanLevel=" + prefixGridScanLevel + ")"; } /** Returns a new shape that is larger than shape by at distErr. */ //TODO move this generic code elsewhere? Spatial4j? protected Shape bufferShape(Shape shape, double distErr) { if (distErr <= 0) throw new IllegalArgumentException("distErr must be > 0"); SpatialContext ctx = grid.getSpatialContext(); if (shape instanceof Point) { return ctx.makeCircle((Point)shape, distErr); } else if (shape instanceof Circle) { Circle circle = (Circle) shape; double newDist = circle.getRadius() + distErr; if (ctx.isGeo() && newDist > 180) newDist = 180; return ctx.makeCircle(circle.getCenter(), newDist); } else { Rectangle bbox = shape.getBoundingBox(); double newMinX = bbox.getMinX() - distErr; double newMaxX = bbox.getMaxX() + distErr; double newMinY = bbox.getMinY() - distErr; double newMaxY = bbox.getMaxY() + distErr; if (ctx.isGeo()) { if (newMinY < -90) newMinY = -90; if (newMaxY > 90) newMaxY = 90; if (newMinY == -90 || newMaxY == 90 || bbox.getWidth() + 2*distErr > 360) { newMinX = -180; newMaxX = 180; } else { newMinX = DistanceUtils.normLonDEG(newMinX); newMaxX = DistanceUtils.normLonDEG(newMaxX); } } else { //restrict to world bounds newMinX = Math.max(newMinX, ctx.getWorldBounds().getMinX()); newMaxX = Math.min(newMaxX, ctx.getWorldBounds().getMaxX()); newMinY = Math.max(newMinY, ctx.getWorldBounds().getMinY()); newMaxY = Math.min(newMaxY, ctx.getWorldBounds().getMaxY()); } return ctx.makeRectangle(newMinX, newMaxX, newMinY, newMaxY); } } @Override protected DocIdSet getDocIdSet(LeafReaderContext context) throws IOException { return new VisitorTemplate(context) { private FixedBitSet inside; private FixedBitSet outside; @Override protected void start() { inside = new FixedBitSet(maxDoc); outside = new FixedBitSet(maxDoc); } @Override protected DocIdSet finish() { inside.andNot(outside); return new BitDocIdSet(inside); } @Override protected CellIterator findSubCellsToVisit(Cell cell) { //use buffered query shape instead of orig. Works with null too. return cell.getNextLevelCells(bufferedQueryShape); } @Override protected boolean visitPrefix(Cell cell) throws IOException { //cell.relate is based on the bufferedQueryShape; we need to examine what // the relation is against the queryShape SpatialRelation visitRelation = cell.getShape().relate(queryShape); if (cell.getLevel() == detailLevel) { collectDocs(visitRelation.intersects() ? inside : outside); return false; } else if (visitRelation == SpatialRelation.WITHIN) { collectDocs(inside); return false; } else if (visitRelation == SpatialRelation.DISJOINT) { collectDocs(outside); return false; } return true; } @Override protected void visitLeaf(Cell cell) throws IOException { if (allCellsIntersectQuery(cell)) collectDocs(inside); else collectDocs(outside); } /** Returns true if the provided cell, and all its sub-cells down to * detailLevel all intersect the queryShape. */ private boolean allCellsIntersectQuery(Cell cell) { SpatialRelation relate = cell.getShape().relate(queryShape); if (cell.getLevel() == detailLevel) return relate.intersects(); if (relate == SpatialRelation.WITHIN) return true; if (relate == SpatialRelation.DISJOINT) return false; // Note: Generating all these cells just to determine intersection is not ideal. // The real solution is LUCENE-4869. CellIterator subCells = cell.getNextLevelCells(null); while (subCells.hasNext()) { Cell subCell = subCells.next(); if (!allCellsIntersectQuery(subCell))//recursion return false; } return true; } @Override protected void visitScanned(Cell cell) throws IOException { visitLeaf(cell);//collects as we want, even if not a leaf // if (cell.isLeaf()) { // visitLeaf(cell); // } else { // visitPrefix(cell); // } } }.getDocIdSet(); } }