org.apache.solr.legacy.BBoxStrategy.java Source code

Java tutorial

Introduction

Here is the source code for org.apache.solr.legacy.BBoxStrategy.java

Source

/*
 * 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.solr.legacy;

import org.apache.lucene.document.DoubleDocValuesField;
import org.apache.lucene.document.DoublePoint;
import org.apache.lucene.document.Field;
import org.apache.lucene.document.FieldType;
import org.apache.lucene.document.StoredField;
import org.apache.lucene.document.StringField;
import org.apache.lucene.index.DocValuesType;
import org.apache.lucene.index.IndexOptions;
import org.apache.lucene.index.Term;
import org.apache.lucene.search.BooleanClause;
import org.apache.lucene.search.BooleanQuery;
import org.apache.lucene.search.ConstantScoreQuery;
import org.apache.lucene.search.DoubleValuesSource;
import org.apache.lucene.search.Query;
import org.apache.lucene.search.TermQuery;
import org.apache.lucene.spatial.ShapeValuesSource;
import org.apache.lucene.spatial.SpatialStrategy;
import org.apache.lucene.spatial.bbox.BBoxOverlapRatioValueSource;
import org.apache.lucene.spatial.query.SpatialArgs;
import org.apache.lucene.spatial.query.SpatialOperation;
import org.apache.lucene.spatial.query.UnsupportedSpatialOperation;
import org.apache.lucene.spatial.util.DistanceToShapeValueSource;
import org.apache.lucene.util.BytesRefBuilder;
import org.apache.lucene.util.NumericUtils;
import org.locationtech.spatial4j.context.SpatialContext;
import org.locationtech.spatial4j.shape.Point;
import org.locationtech.spatial4j.shape.Rectangle;
import org.locationtech.spatial4j.shape.Shape;

/**
 * A SpatialStrategy for indexing and searching Rectangles by storing its
 * coordinates in numeric fields. It supports all {@link SpatialOperation}s and
 * has a custom overlap relevancy. It is based on GeoPortal's <a
 * href="http://geoportal.svn.sourceforge.net/svnroot/geoportal/Geoportal/trunk/src/com/esri/gpt/catalog/lucene/SpatialClauseAdapter.java">SpatialClauseAdapter</a>.
 * <p>
 * <b>Characteristics:</b>
 * <br>
 * <ul>
 * <li>Only indexes Rectangles; just one per field value. Other shapes can be provided
 * and the bounding box will be used.</li>
 * <li>Can query only by a Rectangle. Providing other shapes is an error.</li>
 * <li>Supports most {@link SpatialOperation}s but not Overlaps.</li>
 * <li>Uses the DocValues API for any sorting / relevancy.</li>
 * </ul>
 * <p>
 * <b>Implementation:</b>
 * <p>
 * This uses 4 double fields for minX, maxX, minY, maxY
 * and a boolean to mark a dateline cross. Depending on the particular {@link
 * SpatialOperation}s, there are a variety of range queries on {@link DoublePoint}s to be
 * done.
 * The {@link #makeOverlapRatioValueSource(org.locationtech.spatial4j.shape.Rectangle, double)}
 * works by calculating the query bbox overlap percentage against the indexed
 * shape overlap percentage. The indexed shape's coordinates are retrieved from
 * {@link org.apache.lucene.index.LeafReader#getNumericDocValues}.
 *
 * @lucene.experimental
 */
public class BBoxStrategy extends SpatialStrategy {

    // note: we use a FieldType to articulate the options we want on the field.  We don't use it as-is with a Field, we
    //  create more than one Field.

    /**
     * pointValues, docValues, and nothing else.
     */
    public static FieldType DEFAULT_FIELDTYPE;

    @Deprecated
    public static LegacyFieldType LEGACY_FIELDTYPE;
    static {
        // Default: pointValues + docValues
        FieldType type = new FieldType();
        type.setDimensions(1, Double.BYTES);//pointValues (assume Double)
        type.setDocValuesType(DocValuesType.NUMERIC);//docValues
        type.setStored(false);
        type.freeze();
        DEFAULT_FIELDTYPE = type;
        // Legacy default: legacyNumerics + docValues
        LegacyFieldType legacyType = new LegacyFieldType();
        legacyType.setIndexOptions(IndexOptions.DOCS);
        legacyType.setNumericType(LegacyNumericType.DOUBLE);
        legacyType.setNumericPrecisionStep(8);// same as solr default
        legacyType.setDocValuesType(DocValuesType.NUMERIC);//docValues
        legacyType.setStored(false);
        legacyType.freeze();
        LEGACY_FIELDTYPE = legacyType;
    }

    public static final String SUFFIX_MINX = "__minX";
    public static final String SUFFIX_MAXX = "__maxX";
    public static final String SUFFIX_MINY = "__minY";
    public static final String SUFFIX_MAXY = "__maxY";
    public static final String SUFFIX_XDL = "__xdl";

    /*
     * The Bounding Box gets stored as four fields for x/y min/max and a flag
     * that says if the box crosses the dateline (xdl).
     */
    final String field_bbox;
    final String field_minX;
    final String field_minY;
    final String field_maxX;
    final String field_maxY;
    final String field_xdl; // crosses dateline

    private final FieldType optionsFieldType;//from constructor; aggregate field type used to express all options
    private final int fieldsLen;
    private final boolean hasStored;
    private final boolean hasDocVals;
    private final boolean hasPointVals;
    // equiv to "hasLegacyNumerics":
    private final LegacyFieldType legacyNumericFieldType; // not stored; holds precision step.
    private final FieldType xdlFieldType;

    /**
     * Creates a new {@link BBoxStrategy} instance that uses {@link DoublePoint} and {@link DoublePoint#newRangeQuery}
     */
    public static BBoxStrategy newInstance(SpatialContext ctx, String fieldNamePrefix) {
        return new BBoxStrategy(ctx, fieldNamePrefix, DEFAULT_FIELDTYPE);
    }

    /**
     * Creates a new {@link BBoxStrategy} instance that uses {@link LegacyDoubleField} for backwards compatibility
     * @deprecated LegacyNumerics will be removed
     */
    @Deprecated
    public static BBoxStrategy newLegacyInstance(SpatialContext ctx, String fieldNamePrefix) {
        return new BBoxStrategy(ctx, fieldNamePrefix, LEGACY_FIELDTYPE);
    }

    /**
     * Creates this strategy.
     * {@code fieldType} is used to customize the indexing options of the 4 number fields, and to a lesser degree the XDL
     * field too. Search requires pointValues (or legacy numerics), and relevancy requires docValues. If these features
     * aren't needed then disable them.
     */
    public BBoxStrategy(SpatialContext ctx, String fieldNamePrefix, FieldType fieldType) {
        super(ctx, fieldNamePrefix);
        field_bbox = fieldNamePrefix;
        field_minX = fieldNamePrefix + SUFFIX_MINX;
        field_maxX = fieldNamePrefix + SUFFIX_MAXX;
        field_minY = fieldNamePrefix + SUFFIX_MINY;
        field_maxY = fieldNamePrefix + SUFFIX_MAXY;
        field_xdl = fieldNamePrefix + SUFFIX_XDL;

        fieldType.freeze();
        this.optionsFieldType = fieldType;

        int numQuads = 0;
        if ((this.hasStored = fieldType.stored())) {
            numQuads++;
        }
        if ((this.hasDocVals = fieldType.docValuesType() != DocValuesType.NONE)) {
            numQuads++;
        }
        if ((this.hasPointVals = fieldType.pointDimensionCount() > 0)) {
            numQuads++;
        }
        if (fieldType.indexOptions() != IndexOptions.NONE && fieldType instanceof LegacyFieldType
                && ((LegacyFieldType) fieldType).numericType() != null) {
            if (hasPointVals) {
                throw new IllegalArgumentException("pointValues and LegacyNumericType are mutually exclusive");
            }
            final LegacyFieldType legacyType = (LegacyFieldType) fieldType;
            if (legacyType.numericType() != LegacyNumericType.DOUBLE) {
                throw new IllegalArgumentException(getClass() + " does not support " + legacyType.numericType());
            }
            numQuads++;
            legacyNumericFieldType = new LegacyFieldType(LegacyDoubleField.TYPE_NOT_STORED);
            legacyNumericFieldType.setNumericPrecisionStep(legacyType.numericPrecisionStep());
            legacyNumericFieldType.freeze();
        } else {
            legacyNumericFieldType = null;
        }

        if (hasPointVals || legacyNumericFieldType != null) { // if we have an index...
            xdlFieldType = new FieldType(StringField.TYPE_NOT_STORED);
            xdlFieldType.setIndexOptions(IndexOptions.DOCS);
            xdlFieldType.freeze();
        } else {
            xdlFieldType = null;
        }

        this.fieldsLen = numQuads * 4 + (xdlFieldType != null ? 1 : 0);
    }

    /** Returns a field type representing the set of field options. This is identical to what was passed into the
     * constructor.  It's frozen. */
    public FieldType getFieldType() {
        return optionsFieldType;
    }

    //---------------------------------
    // Indexing
    //---------------------------------

    @Override
    public Field[] createIndexableFields(Shape shape) {
        return createIndexableFields(shape.getBoundingBox());
    }

    private Field[] createIndexableFields(Rectangle bbox) {
        Field[] fields = new Field[fieldsLen];
        int idx = -1;
        if (hasStored) {
            fields[++idx] = new StoredField(field_minX, bbox.getMinX());
            fields[++idx] = new StoredField(field_minY, bbox.getMinY());
            fields[++idx] = new StoredField(field_maxX, bbox.getMaxX());
            fields[++idx] = new StoredField(field_maxY, bbox.getMaxY());
        }
        if (hasDocVals) {
            fields[++idx] = new DoubleDocValuesField(field_minX, bbox.getMinX());
            fields[++idx] = new DoubleDocValuesField(field_minY, bbox.getMinY());
            fields[++idx] = new DoubleDocValuesField(field_maxX, bbox.getMaxX());
            fields[++idx] = new DoubleDocValuesField(field_maxY, bbox.getMaxY());
        }
        if (hasPointVals) {
            fields[++idx] = new DoublePoint(field_minX, bbox.getMinX());
            fields[++idx] = new DoublePoint(field_minY, bbox.getMinY());
            fields[++idx] = new DoublePoint(field_maxX, bbox.getMaxX());
            fields[++idx] = new DoublePoint(field_maxY, bbox.getMaxY());
        }
        if (legacyNumericFieldType != null) {
            fields[++idx] = new LegacyDoubleField(field_minX, bbox.getMinX(), legacyNumericFieldType);
            fields[++idx] = new LegacyDoubleField(field_minY, bbox.getMinY(), legacyNumericFieldType);
            fields[++idx] = new LegacyDoubleField(field_maxX, bbox.getMaxX(), legacyNumericFieldType);
            fields[++idx] = new LegacyDoubleField(field_maxY, bbox.getMaxY(), legacyNumericFieldType);
        }
        if (xdlFieldType != null) {
            fields[++idx] = new Field(field_xdl, bbox.getCrossesDateLine() ? "T" : "F", xdlFieldType);
        }
        assert idx == fields.length - 1;
        return fields;
    }

    //---------------------------------
    // Value Source / Relevancy
    //---------------------------------

    /**
     * Provides access to each rectangle per document
     */ //TODO raise to SpatialStrategy
    public ShapeValuesSource makeShapeValueSource() {
        return new BBoxValueSource(this);
    }

    @Override
    public DoubleValuesSource makeDistanceValueSource(Point queryPoint, double multiplier) {
        //TODO if makeShapeValueSource gets lifted to the top; this could become a generic impl.
        return new DistanceToShapeValueSource(makeShapeValueSource(), queryPoint, multiplier, ctx);
    }

    /** Returns a similarity based on {@link BBoxOverlapRatioValueSource}. This is just a
     * convenience method. */
    public DoubleValuesSource makeOverlapRatioValueSource(Rectangle queryBox, double queryTargetProportion) {
        return new BBoxOverlapRatioValueSource(makeShapeValueSource(), ctx.isGeo(), queryBox, queryTargetProportion,
                0.0);
    }

    //---------------------------------
    // Query Building
    //---------------------------------

    //  Utility on SpatialStrategy?
    //  public Query makeQueryWithValueSource(SpatialArgs args, ValueSource valueSource) {
    //    return new CustomScoreQuery(makeQuery(args), new FunctionQuery(valueSource));
    //or...
    //  return new BooleanQuery.Builder()
    //      .add(new FunctionQuery(valueSource), BooleanClause.Occur.MUST)//matches everything and provides score
    //      .add(filterQuery, BooleanClause.Occur.FILTER)//filters (score isn't used)
    //  .build();
    //  }

    @Override
    public Query makeQuery(SpatialArgs args) {
        Shape shape = args.getShape();
        if (!(shape instanceof Rectangle))
            throw new UnsupportedOperationException("Can only query by Rectangle, not " + shape);

        Rectangle bbox = (Rectangle) shape;
        Query spatial;

        // Useful for understanding Relations:
        // http://edndoc.esri.com/arcsde/9.1/general_topics/understand_spatial_relations.htm
        SpatialOperation op = args.getOperation();
        if (op == SpatialOperation.BBoxIntersects)
            spatial = makeIntersects(bbox);
        else if (op == SpatialOperation.BBoxWithin)
            spatial = makeWithin(bbox);
        else if (op == SpatialOperation.Contains)
            spatial = makeContains(bbox);
        else if (op == SpatialOperation.Intersects)
            spatial = makeIntersects(bbox);
        else if (op == SpatialOperation.IsEqualTo)
            spatial = makeEquals(bbox);
        else if (op == SpatialOperation.IsDisjointTo)
            spatial = makeDisjoint(bbox);
        else if (op == SpatialOperation.IsWithin)
            spatial = makeWithin(bbox);
        else { //no Overlaps support yet
            throw new UnsupportedSpatialOperation(op);
        }
        return new ConstantScoreQuery(spatial);
    }

    /**
     * Constructs a query to retrieve documents that fully contain the input envelope.
     *
     * @return the spatial query
     */
    Query makeContains(Rectangle bbox) {

        // general case
        // docMinX <= queryExtent.getMinX() AND docMinY <= queryExtent.getMinY() AND docMaxX >= queryExtent.getMaxX() AND docMaxY >= queryExtent.getMaxY()

        // Y conditions
        // docMinY <= queryExtent.getMinY() AND docMaxY >= queryExtent.getMaxY()
        Query qMinY = this.makeNumericRangeQuery(field_minY, null, bbox.getMinY(), false, true);
        Query qMaxY = this.makeNumericRangeQuery(field_maxY, bbox.getMaxY(), null, true, false);
        Query yConditions = this.makeQuery(BooleanClause.Occur.MUST, qMinY, qMaxY);

        // X conditions
        Query xConditions;

        // queries that do not cross the date line
        if (!bbox.getCrossesDateLine()) {

            // X Conditions for documents that do not cross the date line,
            // documents that contain the min X and max X of the query envelope,
            // docMinX <= queryExtent.getMinX() AND docMaxX >= queryExtent.getMaxX()
            Query qMinX = this.makeNumericRangeQuery(field_minX, null, bbox.getMinX(), false, true);
            Query qMaxX = this.makeNumericRangeQuery(field_maxX, bbox.getMaxX(), null, true, false);
            Query qMinMax = this.makeQuery(BooleanClause.Occur.MUST, qMinX, qMaxX);
            Query qNonXDL = this.makeXDL(false, qMinMax);

            if (!ctx.isGeo()) {
                xConditions = qNonXDL;
            } else {
                // X Conditions for documents that cross the date line,
                // the left portion of the document contains the min X of the query
                // OR the right portion of the document contains the max X of the query,
                // docMinXLeft <= queryExtent.getMinX() OR docMaxXRight >= queryExtent.getMaxX()
                Query qXDLLeft = this.makeNumericRangeQuery(field_minX, null, bbox.getMinX(), false, true);
                Query qXDLRight = this.makeNumericRangeQuery(field_maxX, bbox.getMaxX(), null, true, false);
                Query qXDLLeftRight = this.makeQuery(BooleanClause.Occur.SHOULD, qXDLLeft, qXDLRight);
                Query qXDL = this.makeXDL(true, qXDLLeftRight);

                Query qEdgeDL = null;
                if (bbox.getMinX() == bbox.getMaxX() && Math.abs(bbox.getMinX()) == 180) {
                    double edge = bbox.getMinX() * -1;//opposite dateline edge
                    qEdgeDL = makeQuery(BooleanClause.Occur.SHOULD, makeNumberTermQuery(field_minX, edge),
                            makeNumberTermQuery(field_maxX, edge));
                }

                // apply the non-XDL and XDL conditions
                xConditions = this.makeQuery(BooleanClause.Occur.SHOULD, qNonXDL, qXDL, qEdgeDL);
            }
        } else {
            // queries that cross the date line

            // No need to search for documents that do not cross the date line

            // X Conditions for documents that cross the date line,
            // the left portion of the document contains the min X of the query
            // AND the right portion of the document contains the max X of the query,
            // docMinXLeft <= queryExtent.getMinX() AND docMaxXRight >= queryExtent.getMaxX()
            Query qXDLLeft = this.makeNumericRangeQuery(field_minX, null, bbox.getMinX(), false, true);
            Query qXDLRight = this.makeNumericRangeQuery(field_maxX, bbox.getMaxX(), null, true, false);
            Query qXDLLeftRight = this.makeXDL(true, this.makeQuery(BooleanClause.Occur.MUST, qXDLLeft, qXDLRight));

            Query qWorld = makeQuery(BooleanClause.Occur.MUST, makeNumberTermQuery(field_minX, -180),
                    makeNumberTermQuery(field_maxX, 180));

            xConditions = makeQuery(BooleanClause.Occur.SHOULD, qXDLLeftRight, qWorld);
        }

        // both X and Y conditions must occur
        return this.makeQuery(BooleanClause.Occur.MUST, xConditions, yConditions);
    }

    /**
     * Constructs a query to retrieve documents that are disjoint to the input envelope.
     *
     * @return the spatial query
     */
    Query makeDisjoint(Rectangle bbox) {

        // general case
        // docMinX > queryExtent.getMaxX() OR docMaxX < queryExtent.getMinX() OR docMinY > queryExtent.getMaxY() OR docMaxY < queryExtent.getMinY()

        // Y conditions
        // docMinY > queryExtent.getMaxY() OR docMaxY < queryExtent.getMinY()
        Query qMinY = this.makeNumericRangeQuery(field_minY, bbox.getMaxY(), null, false, false);
        Query qMaxY = this.makeNumericRangeQuery(field_maxY, null, bbox.getMinY(), false, false);
        Query yConditions = this.makeQuery(BooleanClause.Occur.SHOULD, qMinY, qMaxY);

        // X conditions
        Query xConditions;

        // queries that do not cross the date line
        if (!bbox.getCrossesDateLine()) {

            // X Conditions for documents that do not cross the date line,
            // docMinX > queryExtent.getMaxX() OR docMaxX < queryExtent.getMinX()
            Query qMinX = this.makeNumericRangeQuery(field_minX, bbox.getMaxX(), null, false, false);
            if (bbox.getMinX() == -180.0 && ctx.isGeo()) {//touches dateline; -180 == 180
                BooleanQuery.Builder bq = new BooleanQuery.Builder();
                bq.add(qMinX, BooleanClause.Occur.MUST);
                bq.add(makeNumberTermQuery(field_maxX, 180.0), BooleanClause.Occur.MUST_NOT);
                qMinX = bq.build();
            }
            Query qMaxX = this.makeNumericRangeQuery(field_maxX, null, bbox.getMinX(), false, false);

            if (bbox.getMaxX() == 180.0 && ctx.isGeo()) {//touches dateline; -180 == 180
                BooleanQuery.Builder bq = new BooleanQuery.Builder();
                bq.add(qMaxX, BooleanClause.Occur.MUST);
                bq.add(makeNumberTermQuery(field_minX, -180.0), BooleanClause.Occur.MUST_NOT);
                qMaxX = bq.build();
            }
            Query qMinMax = this.makeQuery(BooleanClause.Occur.SHOULD, qMinX, qMaxX);
            Query qNonXDL = this.makeXDL(false, qMinMax);

            if (!ctx.isGeo()) {
                xConditions = qNonXDL;
            } else {
                // X Conditions for documents that cross the date line,

                // both the left and right portions of the document must be disjoint to the query
                // (docMinXLeft > queryExtent.getMaxX() OR docMaxXLeft < queryExtent.getMinX()) AND
                // (docMinXRight > queryExtent.getMaxX() OR docMaxXRight < queryExtent.getMinX())
                // where: docMaxXLeft = 180.0, docMinXRight = -180.0
                // (docMaxXLeft  < queryExtent.getMinX()) equates to (180.0  < queryExtent.getMinX()) and is ignored
                // (docMinXRight > queryExtent.getMaxX()) equates to (-180.0 > queryExtent.getMaxX()) and is ignored
                Query qMinXLeft = this.makeNumericRangeQuery(field_minX, bbox.getMaxX(), null, false, false);
                Query qMaxXRight = this.makeNumericRangeQuery(field_maxX, null, bbox.getMinX(), false, false);
                Query qLeftRight = this.makeQuery(BooleanClause.Occur.MUST, qMinXLeft, qMaxXRight);
                Query qXDL = this.makeXDL(true, qLeftRight);

                // apply the non-XDL and XDL conditions
                xConditions = this.makeQuery(BooleanClause.Occur.SHOULD, qNonXDL, qXDL);
            }
            // queries that cross the date line
        } else {

            // X Conditions for documents that do not cross the date line,
            // the document must be disjoint to both the left and right query portions
            // (docMinX > queryExtent.getMaxX()Left OR docMaxX < queryExtent.getMinX()) AND (docMinX > queryExtent.getMaxX() OR docMaxX < queryExtent.getMinX()Left)
            // where: queryExtent.getMaxX()Left = 180.0, queryExtent.getMinX()Left = -180.0
            Query qMinXLeft = this.makeNumericRangeQuery(field_minX, 180.0, null, false, false);
            Query qMaxXLeft = this.makeNumericRangeQuery(field_maxX, null, bbox.getMinX(), false, false);
            Query qMinXRight = this.makeNumericRangeQuery(field_minX, bbox.getMaxX(), null, false, false);
            Query qMaxXRight = this.makeNumericRangeQuery(field_maxX, null, -180.0, false, false);
            Query qLeft = this.makeQuery(BooleanClause.Occur.SHOULD, qMinXLeft, qMaxXLeft);
            Query qRight = this.makeQuery(BooleanClause.Occur.SHOULD, qMinXRight, qMaxXRight);
            Query qLeftRight = this.makeQuery(BooleanClause.Occur.MUST, qLeft, qRight);

            // No need to search for documents that do not cross the date line

            xConditions = this.makeXDL(false, qLeftRight);
        }

        // either X or Y conditions should occur
        return this.makeQuery(BooleanClause.Occur.SHOULD, xConditions, yConditions);
    }

    /**
     * Constructs a query to retrieve documents that equal the input envelope.
     *
     * @return the spatial query
     */
    Query makeEquals(Rectangle bbox) {

        // docMinX = queryExtent.getMinX() AND docMinY = queryExtent.getMinY() AND docMaxX = queryExtent.getMaxX() AND docMaxY = queryExtent.getMaxY()
        Query qMinX = makeNumberTermQuery(field_minX, bbox.getMinX());
        Query qMinY = makeNumberTermQuery(field_minY, bbox.getMinY());
        Query qMaxX = makeNumberTermQuery(field_maxX, bbox.getMaxX());
        Query qMaxY = makeNumberTermQuery(field_maxY, bbox.getMaxY());
        return makeQuery(BooleanClause.Occur.MUST, qMinX, qMinY, qMaxX, qMaxY);
    }

    /**
     * Constructs a query to retrieve documents that intersect the input envelope.
     *
     * @return the spatial query
     */
    Query makeIntersects(Rectangle bbox) {

        // the original intersects query does not work for envelopes that cross the date line,
        // switch to a NOT Disjoint query

        // MUST_NOT causes a problem when it's the only clause type within a BooleanQuery,
        // to get around it we add all documents as a SHOULD

        // there must be an envelope, it must not be disjoint
        Query qHasEnv;
        if (ctx.isGeo()) {
            Query qIsNonXDL = this.makeXDL(false);
            Query qIsXDL = ctx.isGeo() ? this.makeXDL(true) : null;
            qHasEnv = this.makeQuery(BooleanClause.Occur.SHOULD, qIsNonXDL, qIsXDL);
        } else {
            qHasEnv = this.makeXDL(false);
        }

        BooleanQuery.Builder qNotDisjoint = new BooleanQuery.Builder();
        qNotDisjoint.add(qHasEnv, BooleanClause.Occur.MUST);
        Query qDisjoint = makeDisjoint(bbox);
        qNotDisjoint.add(qDisjoint, BooleanClause.Occur.MUST_NOT);

        //Query qDisjoint = makeDisjoint();
        //BooleanQuery qNotDisjoint = new BooleanQuery();
        //qNotDisjoint.add(new MatchAllDocsQuery(),BooleanClause.Occur.SHOULD);
        //qNotDisjoint.add(qDisjoint,BooleanClause.Occur.MUST_NOT);
        return qNotDisjoint.build();
    }

    /**
     * Makes a boolean query based upon a collection of queries and a logical operator.
     *
     * @param occur the logical operator
     * @param queries the query collection
     * @return the query
     */
    BooleanQuery makeQuery(BooleanClause.Occur occur, Query... queries) {
        BooleanQuery.Builder bq = new BooleanQuery.Builder();
        for (Query query : queries) {
            if (query != null)
                bq.add(query, occur);
        }
        return bq.build();
    }

    /**
     * Constructs a query to retrieve documents are fully within the input envelope.
     *
     * @return the spatial query
     */
    Query makeWithin(Rectangle bbox) {

        // general case
        // docMinX >= queryExtent.getMinX() AND docMinY >= queryExtent.getMinY() AND docMaxX <= queryExtent.getMaxX() AND docMaxY <= queryExtent.getMaxY()

        // Y conditions
        // docMinY >= queryExtent.getMinY() AND docMaxY <= queryExtent.getMaxY()
        Query qMinY = this.makeNumericRangeQuery(field_minY, bbox.getMinY(), null, true, false);
        Query qMaxY = this.makeNumericRangeQuery(field_maxY, null, bbox.getMaxY(), false, true);
        Query yConditions = this.makeQuery(BooleanClause.Occur.MUST, qMinY, qMaxY);

        // X conditions
        Query xConditions;

        if (ctx.isGeo() && bbox.getMinX() == -180.0 && bbox.getMaxX() == 180.0) {
            //if query world-wraps, only the y condition matters
            return yConditions;

        } else if (!bbox.getCrossesDateLine()) {
            // queries that do not cross the date line

            // docMinX >= queryExtent.getMinX() AND docMaxX <= queryExtent.getMaxX()
            Query qMinX = this.makeNumericRangeQuery(field_minX, bbox.getMinX(), null, true, false);
            Query qMaxX = this.makeNumericRangeQuery(field_maxX, null, bbox.getMaxX(), false, true);
            Query qMinMax = this.makeQuery(BooleanClause.Occur.MUST, qMinX, qMaxX);

            double edge = 0;//none, otherwise opposite dateline of query
            if (bbox.getMinX() == -180.0)
                edge = 180;
            else if (bbox.getMaxX() == 180.0)
                edge = -180;
            if (edge != 0 && ctx.isGeo()) {
                Query edgeQ = makeQuery(BooleanClause.Occur.MUST, makeNumberTermQuery(field_minX, edge),
                        makeNumberTermQuery(field_maxX, edge));
                qMinMax = makeQuery(BooleanClause.Occur.SHOULD, qMinMax, edgeQ);
            }

            xConditions = this.makeXDL(false, qMinMax);

            // queries that cross the date line
        } else {

            // X Conditions for documents that do not cross the date line

            // the document should be within the left portion of the query
            // docMinX >= queryExtent.getMinX() AND docMaxX <= 180.0
            Query qMinXLeft = this.makeNumericRangeQuery(field_minX, bbox.getMinX(), null, true, false);
            Query qMaxXLeft = this.makeNumericRangeQuery(field_maxX, null, 180.0, false, true);
            Query qLeft = this.makeQuery(BooleanClause.Occur.MUST, qMinXLeft, qMaxXLeft);

            // the document should be within the right portion of the query
            // docMinX >= -180.0 AND docMaxX <= queryExtent.getMaxX()
            Query qMinXRight = this.makeNumericRangeQuery(field_minX, -180.0, null, true, false);
            Query qMaxXRight = this.makeNumericRangeQuery(field_maxX, null, bbox.getMaxX(), false, true);
            Query qRight = this.makeQuery(BooleanClause.Occur.MUST, qMinXRight, qMaxXRight);

            // either left or right conditions should occur,
            // apply the left and right conditions to documents that do not cross the date line
            Query qLeftRight = this.makeQuery(BooleanClause.Occur.SHOULD, qLeft, qRight);
            Query qNonXDL = this.makeXDL(false, qLeftRight);

            // X Conditions for documents that cross the date line,
            // the left portion of the document must be within the left portion of the query,
            // AND the right portion of the document must be within the right portion of the query
            // docMinXLeft >= queryExtent.getMinX() AND docMaxXLeft <= 180.0
            // AND docMinXRight >= -180.0 AND docMaxXRight <= queryExtent.getMaxX()
            Query qXDLLeft = this.makeNumericRangeQuery(field_minX, bbox.getMinX(), null, true, false);
            Query qXDLRight = this.makeNumericRangeQuery(field_maxX, null, bbox.getMaxX(), false, true);
            Query qXDLLeftRight = this.makeQuery(BooleanClause.Occur.MUST, qXDLLeft, qXDLRight);
            Query qXDL = this.makeXDL(true, qXDLLeftRight);

            // apply the non-XDL and XDL conditions
            xConditions = this.makeQuery(BooleanClause.Occur.SHOULD, qNonXDL, qXDL);
        }

        // both X and Y conditions must occur
        return this.makeQuery(BooleanClause.Occur.MUST, xConditions, yConditions);
    }

    /**
     * Constructs a query to retrieve documents that do or do not cross the date line.
     *
     * @param crossedDateLine <code>true</true> for documents that cross the date line
     * @return the query
     */
    private Query makeXDL(boolean crossedDateLine) {
        // The 'T' and 'F' values match solr fields
        return new TermQuery(new Term(field_xdl, crossedDateLine ? "T" : "F"));
    }

    /**
     * Constructs a query to retrieve documents that do or do not cross the date line
     * and match the supplied spatial query.
     *
     * @param crossedDateLine <code>true</true> for documents that cross the date line
     * @param query the spatial query
     * @return the query
     */
    private Query makeXDL(boolean crossedDateLine, Query query) {
        if (!ctx.isGeo()) {
            assert !crossedDateLine;
            return query;
        }
        BooleanQuery.Builder bq = new BooleanQuery.Builder();
        bq.add(this.makeXDL(crossedDateLine), BooleanClause.Occur.MUST);
        bq.add(query, BooleanClause.Occur.MUST);
        return bq.build();
    }

    private Query makeNumberTermQuery(String field, double number) {
        if (hasPointVals) {
            return DoublePoint.newExactQuery(field, number);
        } else if (legacyNumericFieldType != null) {
            BytesRefBuilder bytes = new BytesRefBuilder();
            LegacyNumericUtils.longToPrefixCoded(NumericUtils.doubleToSortableLong(number), 0, bytes);
            return new TermQuery(new Term(field, bytes.get()));
        }
        throw new UnsupportedOperationException("An index is required for this operation.");
    }

    /**
     * Returns a numeric range query based on FieldType
     * {@link LegacyNumericRangeQuery} is used for indexes created using {@code FieldType.LegacyNumericType}
     * {@link DoublePoint#newRangeQuery} is used for indexes created using {@link DoublePoint} fields
     *
     * @param fieldname field name. must not be <code>null</code>.
     * @param min minimum value of the range.
     * @param max maximum value of the range.
     * @param minInclusive include the minimum value if <code>true</code>.
     * @param maxInclusive include the maximum value if <code>true</code>
     */
    private Query makeNumericRangeQuery(String fieldname, Double min, Double max, boolean minInclusive,
            boolean maxInclusive) {
        if (hasPointVals) {
            if (min == null) {
                min = Double.NEGATIVE_INFINITY;
            }

            if (max == null) {
                max = Double.POSITIVE_INFINITY;
            }

            if (minInclusive == false) {
                min = Math.nextUp(min);
            }

            if (maxInclusive == false) {
                max = Math.nextDown(max);
            }

            return DoublePoint.newRangeQuery(fieldname, min, max);
        } else if (legacyNumericFieldType != null) {// todo remove legacy numeric support in 7.0
            return LegacyNumericRangeQuery.newDoubleRange(fieldname, legacyNumericFieldType.numericPrecisionStep(),
                    min, max, minInclusive, maxInclusive);
        }
        throw new UnsupportedOperationException("An index is required for this operation.");
    }
}