/* * 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.bbox; 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.legacy.LegacyDoubleField; import org.apache.lucene.legacy.LegacyFieldType; import org.apache.lucene.legacy.LegacyNumericRangeQuery; import org.apache.lucene.legacy.LegacyNumericType; import org.apache.lucene.legacy.LegacyNumericUtils; import org.apache.lucene.queries.function.ValueSource; import org.apache.lucene.search.BooleanClause; import org.apache.lucene.search.BooleanQuery; import org.apache.lucene.search.ConstantScoreQuery; import org.apache.lucene.search.Query; import org.apache.lucene.search.TermQuery; import org.apache.lucene.spatial.SpatialStrategy; 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 as a ValueSource in which * {@link org.apache.lucene.queries.function.FunctionValues#objectVal(int)} returns a {@link * Shape}. */ //TODO raise to SpatialStrategy public ValueSource makeShapeValueSource() { return new BBoxValueSource(this); } @Override public ValueSource 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 ValueSource 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."); } }