/* * 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. */ using System; using Lucene.Net.Documents; using Lucene.Net.Index; using Lucene.Net.Search; using Lucene.Net.Search.Function; using Lucene.Net.Spatial.Queries; using Lucene.Net.Spatial.Util; using Spatial4n.Core.Context; using Spatial4n.Core.Shapes; namespace Lucene.Net.Spatial.BBox { public class BBoxStrategy : SpatialStrategy { public static String SUFFIX_MINX = "__minX"; public static String SUFFIX_MAXX = "__maxX"; public static String SUFFIX_MINY = "__minY"; public static String SUFFIX_MAXY = "__maxY"; public static 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). */ public readonly String field_bbox; public readonly String field_minX; public readonly String field_minY; public readonly String field_maxX; public readonly String field_maxY; public readonly String field_xdl; // crosses dateline public readonly double queryPower = 1.0; public readonly double targetPower = 1.0f; public int precisionStep = 8; // same as solr default public BBoxStrategy(SpatialContext ctx, String fieldNamePrefix) : base(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; } public void SetPrecisionStep(int p) { precisionStep = p; if (precisionStep <= 0 || precisionStep >= 64) precisionStep = int.MaxValue; } //--------------------------------- // Indexing //--------------------------------- public override AbstractField[] CreateIndexableFields(Shape shape) { var rect = shape as Rectangle; if (rect != null) return CreateIndexableFields(rect); throw new InvalidOperationException("Can only index Rectangle, not " + shape); } public AbstractField[] CreateIndexableFields(Rectangle bbox) { var fields = new AbstractField[5]; fields[0] = DoubleField(field_minX, bbox.GetMinX()); fields[1] = DoubleField(field_maxX, bbox.GetMaxX()); fields[2] = DoubleField(field_minY, bbox.GetMinY()); fields[3] = DoubleField(field_maxY, bbox.GetMaxY()); fields[4] = new Field(field_xdl, bbox.GetCrossesDateLine() ? "T" : "F", Field.Store.NO, Field.Index.NOT_ANALYZED_NO_NORMS) {OmitNorms = true, OmitTermFreqAndPositions = true}; return fields; } private AbstractField DoubleField(string field, double value) { var f = new NumericField(field, precisionStep, Field.Store.NO, true) {OmitNorms = true, OmitTermFreqAndPositions = true}; f.SetDoubleValue(value); return f; } public override ValueSource MakeDistanceValueSource(Point queryPoint) { return new BBoxSimilarityValueSource(this, new DistanceSimilarity(this.GetSpatialContext(), queryPoint)); } public ValueSource MakeBBoxAreaSimilarityValueSource(Rectangle queryBox) { return new BBoxSimilarityValueSource( this, new AreaSimilarity(queryBox, queryPower, targetPower)); } public override ConstantScoreQuery MakeQuery(SpatialArgs args) { return new ConstantScoreQuery(new QueryWrapperFilter(MakeSpatialQuery(args))); } public Query MakeQueryWithValueSource(SpatialArgs args, ValueSource valueSource) { var bq = new BooleanQuery(); var spatial = MakeFilter(args); bq.Add(new ConstantScoreQuery(spatial), Occur.MUST); // This part does the scoring Query spatialRankingQuery = new FunctionQuery(valueSource); bq.Add(spatialRankingQuery, Occur.MUST); return bq; } public override Filter MakeFilter(SpatialArgs args) { return new QueryWrapperFilter(MakeSpatialQuery(args)); } private Query MakeSpatialQuery(SpatialArgs args) { var bbox = args.Shape as Rectangle; if (bbox == null) throw new InvalidOperationException("Can only query by Rectangle, not " + args.Shape); Query spatial = null; // Useful for understanding Relations: // http://edndoc.esri.com/arcsde/9.1/general_topics/understand_spatial_relations.htm SpatialOperation op = args.Operation; 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 if (op == SpatialOperation.Overlaps) spatial = MakeIntersects(bbox); else { throw new UnsupportedSpatialOperation(op); } return spatial; } //------------------------------------------------------------------------------- // //------------------------------------------------------------------------------- /// /// Constructs a query to retrieve documents that fully contain the input envelope. /// /// /// The spatial query protected 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 = NumericRangeQuery.NewDoubleRange(field_minY, precisionStep, null, bbox.GetMinY(), false, true); Query qMaxY = NumericRangeQuery.NewDoubleRange(field_maxY, precisionStep, bbox.GetMaxY(), null, true, false); Query yConditions = this.MakeQuery(new Query[] { qMinY, qMaxY }, Occur.MUST); // X conditions Query xConditions = null; // 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 = NumericRangeQuery.NewDoubleRange(field_minX, precisionStep, null, bbox.GetMinX(), false, true); Query qMaxX = NumericRangeQuery.NewDoubleRange(field_maxX, precisionStep, bbox.GetMaxX(), null, true, false); Query qMinMax = this.MakeQuery(new Query[] { qMinX, qMaxX }, Occur.MUST); Query qNonXDL = this.MakeXDL(false, qMinMax); // 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 = NumericRangeQuery.NewDoubleRange(field_minX, precisionStep, null, bbox.GetMinX(), false, true); Query qXDLRight = NumericRangeQuery.NewDoubleRange(field_maxX, precisionStep, bbox.GetMaxX(), null, true, false); Query qXDLLeftRight = this.MakeQuery(new Query[] { qXDLLeft, qXDLRight }, Occur.SHOULD); Query qXDL = this.MakeXDL(true, qXDLLeftRight); // apply the non-XDL and XDL conditions xConditions = this.MakeQuery(new Query[] { qNonXDL, qXDL }, Occur.SHOULD); // queries that cross the date line } else { // 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 = NumericRangeQuery.NewDoubleRange(field_minX, precisionStep, null, bbox.GetMinX(), false, true); Query qXDLRight = NumericRangeQuery.NewDoubleRange(field_maxX, precisionStep, bbox.GetMaxX(), null, true, false); Query qXDLLeftRight = this.MakeQuery(new Query[] { qXDLLeft, qXDLRight }, Occur.MUST); xConditions = this.MakeXDL(true, qXDLLeftRight); } // both X and Y conditions must occur return this.MakeQuery(new Query[] { xConditions, yConditions }, Occur.MUST); } /// /// Constructs a query to retrieve documents that are disjoint to the input envelope. /// /// /// 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 = NumericRangeQuery.NewDoubleRange(field_minY, precisionStep, bbox.GetMaxY(), null, false, false); Query qMaxY = NumericRangeQuery.NewDoubleRange(field_maxY, precisionStep, null, bbox.GetMinY(), false, false); Query yConditions = this.MakeQuery(new Query[] { qMinY, qMaxY }, Occur.SHOULD); // X conditions Query xConditions = null; // 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 = NumericRangeQuery.NewDoubleRange(field_minX, precisionStep, bbox.GetMaxX(), null, false, false); Query qMaxX = NumericRangeQuery.NewDoubleRange(field_maxX, precisionStep, null, bbox.GetMinX(), false, false); Query qMinMax = this.MakeQuery(new Query[] { qMinX, qMaxX }, Occur.SHOULD); Query qNonXDL = this.MakeXDL(false, qMinMax); // 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 = NumericRangeQuery.NewDoubleRange(field_minX, precisionStep, bbox.GetMaxX(), null, false, false); Query qMaxXRight = NumericRangeQuery.NewDoubleRange(field_maxX, precisionStep, null, bbox.GetMinX(), false, false); Query qLeftRight = this.MakeQuery(new Query[] { qMinXLeft, qMaxXRight }, Occur.MUST); Query qXDL = this.MakeXDL(true, qLeftRight); // apply the non-XDL and XDL conditions xConditions = this.MakeQuery(new Query[] { qNonXDL, qXDL }, Occur.SHOULD); // 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 = NumericRangeQuery.NewDoubleRange(field_minX, precisionStep, 180.0, null, false, false); Query qMaxXLeft = NumericRangeQuery.NewDoubleRange(field_maxX, precisionStep, null, bbox.GetMinX(), false, false); Query qMinXRight = NumericRangeQuery.NewDoubleRange(field_minX, precisionStep, bbox.GetMaxX(), null, false, false); Query qMaxXRight = NumericRangeQuery.NewDoubleRange(field_maxX, precisionStep, null, -180.0, false, false); Query qLeft = this.MakeQuery(new Query[] { qMinXLeft, qMaxXLeft }, Occur.SHOULD); Query qRight = this.MakeQuery(new Query[] { qMinXRight, qMaxXRight }, Occur.SHOULD); Query qLeftRight = this.MakeQuery(new Query[] { qLeft, qRight }, Occur.MUST); // 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(new Query[] { xConditions, yConditions }, Occur.SHOULD); } /* * Constructs a query to retrieve documents that equal the input envelope. * * @return the spatial query */ public Query MakeEquals(Rectangle bbox) { // docMinX = queryExtent.GetMinX() AND docMinY = queryExtent.GetMinY() AND docMaxX = queryExtent.GetMaxX() AND docMaxY = queryExtent.GetMaxY() Query qMinX = NumericRangeQuery.NewDoubleRange(field_minX, precisionStep, bbox.GetMinX(), bbox.GetMinX(), true, true); Query qMinY = NumericRangeQuery.NewDoubleRange(field_minY, precisionStep, bbox.GetMinY(), bbox.GetMinY(), true, true); Query qMaxX = NumericRangeQuery.NewDoubleRange(field_maxX, precisionStep, bbox.GetMaxX(), bbox.GetMaxX(), true, true); Query qMaxY = NumericRangeQuery.NewDoubleRange(field_maxY, precisionStep, bbox.GetMaxY(), bbox.GetMaxY(), true, true); var bq = new BooleanQuery { {qMinX, Occur.MUST}, {qMinY, Occur.MUST}, {qMaxX, Occur.MUST}, {qMaxY, Occur.MUST} }; return bq; } /// /// Constructs a query to retrieve documents that intersect the input envelope. /// /// /// 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 round it we add all documents as a SHOULD // there must be an envelope, it must not be disjoint Query qDisjoint = MakeDisjoint(bbox); Query qIsNonXDL = this.MakeXDL(false); Query qIsXDL = this.MakeXDL(true); Query qHasEnv = this.MakeQuery(new Query[] { qIsNonXDL, qIsXDL }, Occur.SHOULD); var qNotDisjoint = new BooleanQuery {{qHasEnv, Occur.MUST}, {qDisjoint, 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; } /* * Makes a boolean query based upon a collection of queries and a logical operator. * * @param queries the query collection * @param occur the logical operator * @return the query */ BooleanQuery MakeQuery(Query[] queries, Occur occur) { var bq = new BooleanQuery(); foreach (Query query in queries) { bq.Add(query, occur); } return bq; } /* * 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 = NumericRangeQuery.NewDoubleRange(field_minY, precisionStep, bbox.GetMinY(), null, true, false); Query qMaxY = NumericRangeQuery.NewDoubleRange(field_maxY, precisionStep, null, bbox.GetMaxY(), false, true); Query yConditions = this.MakeQuery(new Query[] { qMinY, qMaxY }, Occur.MUST); // X conditions Query xConditions = null; // 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 = NumericRangeQuery.NewDoubleRange(field_minX, precisionStep, bbox.GetMinX(), null, true, false); Query qXDLRight = NumericRangeQuery.NewDoubleRange(field_maxX, precisionStep, null, bbox.GetMaxX(), false, true); Query qXDLLeftRight = this.MakeQuery(new Query[] { qXDLLeft, qXDLRight }, Occur.MUST); Query qXDL = this.MakeXDL(true, qXDLLeftRight); // queries that do not cross the date line if (!bbox.GetCrossesDateLine()) { // X Conditions for documents that do not cross the date line, // docMinX >= queryExtent.GetMinX() AND docMaxX <= queryExtent.GetMaxX() Query qMinX = NumericRangeQuery.NewDoubleRange(field_minX, precisionStep, bbox.GetMinX(), null, true, false); Query qMaxX = NumericRangeQuery.NewDoubleRange(field_maxX, precisionStep, null, bbox.GetMaxX(), false, true); Query qMinMax = this.MakeQuery(new Query[] { qMinX, qMaxX }, Occur.MUST); Query qNonXDL = this.MakeXDL(false, qMinMax); // apply the non-XDL or XDL X conditions if ((bbox.GetMinX() <= -180.0) && bbox.GetMaxX() >= 180.0) { xConditions = this.MakeQuery(new Query[] { qNonXDL, qXDL }, Occur.SHOULD); } else { xConditions = qNonXDL; } // 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 = NumericRangeQuery.NewDoubleRange(field_minX, precisionStep, bbox.GetMinX(), null, true, false); Query qMaxXLeft = NumericRangeQuery.NewDoubleRange(field_maxX, precisionStep, null, 180.0, false, true); Query qLeft = this.MakeQuery(new Query[] { qMinXLeft, qMaxXLeft }, Occur.MUST); // the document should be within the right portion of the query // docMinX >= -180.0 AND docMaxX <= queryExtent.GetMaxX() Query qMinXRight = NumericRangeQuery.NewDoubleRange(field_minX, precisionStep, -180.0, null, true, false); Query qMaxXRight = NumericRangeQuery.NewDoubleRange(field_maxX, precisionStep, null, bbox.GetMaxX(), false, true); Query qRight = this.MakeQuery(new Query[] { qMinXRight, qMaxXRight }, Occur.MUST); // 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(new Query[] { qLeft, qRight }, Occur.SHOULD); Query qNonXDL = this.MakeXDL(false, qLeftRight); // apply the non-XDL and XDL conditions xConditions = this.MakeQuery(new Query[] { qNonXDL, qXDL }, Occur.SHOULD); } // both X and Y conditions must occur return this.MakeQuery(new Query[] { xConditions, yConditions }, Occur.MUST); } /* * Constructs a query to retrieve documents that do or do not cross the date line. * * * @param crossedDateLine true for documents that cross the date line * @return the query */ public Query MakeXDL(bool 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 true for documents that cross the date line * @param query the spatial query * @return the query */ public Query MakeXDL(bool crossedDateLine, Query query) { var bq = new BooleanQuery {{this.MakeXDL(crossedDateLine), Occur.MUST}, {query, Occur.MUST}}; return bq; } } }