/* See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * Esri Inc. 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 com.esri.gpt.catalog.lucene; import com.esri.gpt.catalog.discovery.Discoverable; import com.esri.gpt.catalog.discovery.DiscoveryException; import com.esri.gpt.catalog.discovery.LogicalClause; import com.esri.gpt.catalog.discovery.SpatialClause; import com.esri.gpt.framework.collection.StringAttributeMap; import com.esri.gpt.framework.context.RequestContext; import com.esri.gpt.framework.geometry.Envelope; import com.esri.gpt.framework.util.Val; import java.util.logging.Logger; import org.apache.lucene.index.Term; import org.apache.lucene.search.BooleanClause; import org.apache.lucene.search.BooleanQuery; import org.apache.lucene.search.NumericRangeQuery; import org.apache.lucene.search.Query; import org.apache.lucene.search.TermQuery; import org.apache.lucene.search.function.ValueSourceQuery; /** * Adapts a catalog discovery SpatialClause to the Lucene model. */ public class SpatialClauseAdapter extends DiscoveryClauseAdapter { /** class variables ========================================================= */ /** The Logger. */ private static Logger LOGGER = Logger.getLogger(SpatialClauseAdapter.class.getName()); /** instance variables ====================================================== */ /** The input envelope. */ private Envelope envelope; /** The field names. */ private String docMinX = "envelope.minx"; private String docMinY = "envelope.miny"; private String docMaxX = "envelope.maxx"; private String docMaxY = "envelope.maxy"; private String docMinXLeft = "envelope.minx"; private String docMaxXRight = "envelope.maxx"; private String docXDL = "envelope.xdl"; /** The query values */ private boolean qryCrossedDateline = false; private double qryMinX; private double qryMinY; private double qryMaxX; private double qryMaxY; /** constructors ============================================================ */ /** * Constructs with an associated query adapter. * @param queryAdapter the query adapter */ protected SpatialClauseAdapter(LuceneQueryAdapter queryAdapter) { super(queryAdapter); } /** methods ================================================================= */ /** * Adapts a catalog discovery SpatialClause to the Lucene model. * @param activeBooleanQuery the active Lucene boolean query * @param activeLogicalClause the active discovery logical clause * @param spatialClause the spatial clause to adapt * @throws DiscoveryException if an invalid clause is encountered */ protected void adaptSpatialClause(BooleanQuery activeBooleanQuery, LogicalClause activeLogicalClause, SpatialClause spatialClause) throws DiscoveryException { LOGGER.finer("Adapting SpatialClause...\n"+spatialClause); // determine the discoverable target, set names String sErr; Discoverable discoverable = spatialClause.getTarget(); if (discoverable == null) { sErr = "The SpatialClause.target is null."; throw new DiscoveryException(sErr); } if (discoverable.getStorable() == null) { sErr = "The SpatialClause.target.storeable is null."; throw new DiscoveryException(sErr); } else { Storeable storeable = (Storeable)discoverable.getStorable(); if (!(storeable instanceof GeometryProperty)) { sErr = "The SpatialClause.target.storeable is not a GeometryProperty."; throw new DiscoveryException(sErr); } } // check the envelope envelope = spatialClause.getBoundingEnvelope(); if ((envelope == null) || envelope.isEmpty()) { sErr = "The SpatialClause.boundingEnvelope is empty."; throw new DiscoveryException(sErr); } // initialize the values of the input query envelope qryMinX = envelope.getMinX(); qryMinY = envelope.getMinY(); qryMaxX = envelope.getMaxX(); qryMaxY = envelope.getMaxY(); if (qryMinX > qryMaxX) { qryCrossedDateline = true; } // determine spatialRelevance parameters // (original defaults were queryPower=2.0, targetPower=0.5) RequestContext rc = this.getQueryAdapter().getIndexAdapter().getRequestContext(); StringAttributeMap params = rc.getCatalogConfiguration().getParameters(); double queryPower = Val.chkDbl(params.getValue("spatialRelevance.queryPower"),1.0); double targetPower = Val.chkDbl(params.getValue("spatialRelevance.targetPower"),1.0); String rankingOption = Val.chkStr(params.getValue("spatialRelevance.ranking.enabled")); int rankingMaxDoc = Val.chkInt(params.getValue("spatialRelevance.ranking.maxDoc"),50000); boolean bUseSpatialRanking = false; if (rankingOption.equalsIgnoreCase("true")) { bUseSpatialRanking = true; } else if (rankingOption.equalsIgnoreCase("false")) { bUseSpatialRanking = false; } else { // default spatialRelevance.ranking.enabled option is "auto" if (this.getQueryAdapter() != null) { int maxDoc = this.getQueryAdapter().getMaxDoc(); if ((maxDoc > 0) && (maxDoc <= rankingMaxDoc)) { bUseSpatialRanking = true; } } } // Handle each operation - Beyond, Crosses, DWithin and Touches are not implemented if (bUseSpatialRanking) { Query spatialQuery = null; if (spatialClause instanceof SpatialClause.GeometryBBOXIntersects) { spatialQuery = this.makeIntersects(); } else if (spatialClause instanceof SpatialClause.GeometryContains) { spatialQuery = this.makeContains(); } else if (spatialClause instanceof SpatialClause.GeometryIntersects) { spatialQuery = this.makeIntersects(); } else if (spatialClause instanceof SpatialClause.GeometryIsDisjointTo) { bUseSpatialRanking = false; } else if (spatialClause instanceof SpatialClause.GeometryIsEqualTo) { bUseSpatialRanking = false; } else if (spatialClause instanceof SpatialClause.GeometryIsWithin) { spatialQuery = this.makeWithin(); } else if (spatialClause instanceof SpatialClause.GeometryOverlaps) { spatialQuery = this.makeIntersects(); } else { sErr = "Unrecognized spatial clause type: "; throw new DiscoveryException(sErr+spatialClause.getClass().getName()); } if (bUseSpatialRanking) { SpatialRankingValueSource srvs = new SpatialRankingValueSource(envelope,queryPower,targetPower); Query spatialRankingQuery = new ValueSourceQuery(srvs); BooleanQuery bq = new BooleanQuery(); bq.add(spatialQuery,BooleanClause.Occur.MUST); bq.add(spatialRankingQuery,BooleanClause.Occur.MUST); appendQuery(activeBooleanQuery,activeLogicalClause,bq); this.getQueryAdapter().setHasScoredExpression(true); } } if (!bUseSpatialRanking) { if (spatialClause instanceof SpatialClause.GeometryBBOXIntersects) { appendQuery(activeBooleanQuery,activeLogicalClause,makeIntersects()); } else if (spatialClause instanceof SpatialClause.GeometryContains) { appendQuery(activeBooleanQuery,activeLogicalClause,makeContains()); } else if (spatialClause instanceof SpatialClause.GeometryIntersects) { appendQuery(activeBooleanQuery,activeLogicalClause,makeIntersects()); } else if (spatialClause instanceof SpatialClause.GeometryIsDisjointTo) { appendQuery(activeBooleanQuery,activeLogicalClause,makeDisjoint()); } else if (spatialClause instanceof SpatialClause.GeometryIsEqualTo) { appendQuery(activeBooleanQuery,activeLogicalClause,makeEquals()); } else if (spatialClause instanceof SpatialClause.GeometryIsWithin) { appendQuery(activeBooleanQuery,activeLogicalClause,makeWithin()); } else if (spatialClause instanceof SpatialClause.GeometryOverlaps) { appendQuery(activeBooleanQuery,activeLogicalClause,makeIntersects()); } else { sErr = "Unrecognized spatial clause type: "; throw new DiscoveryException(sErr+spatialClause.getClass().getName()); } } } /** * Constructs a query to retrieve documents that fully contain the input envelope. * @return the spatial query */ private Query makeContains() { /* // the original contains query does not work for envelopes that cross the date line // docMinX <= qryMinX, docMinY <= qryMinY, docMaxX >= qryMaxX, docMaxY >= qryMaxY Query qMinX = NumericRangeQuery.newDoubleRange(docMinX,null,qryMinX,false,true); Query qMinY = NumericRangeQuery.newDoubleRange(docMinY,null,qryMinY,false,true); Query qMaxX = NumericRangeQuery.newDoubleRange(docMaxX,qryMaxX,null,true,false); Query qMaxY = NumericRangeQuery.newDoubleRange(docMaxY,qryMaxY,null,true,false); BooleanQuery bq = new BooleanQuery(); bq.add(qMinX,BooleanClause.Occur.MUST); bq.add(qMinY,BooleanClause.Occur.MUST); bq.add(qMaxX,BooleanClause.Occur.MUST); bq.add(qMaxY,BooleanClause.Occur.MUST); return bq; */ // general case // docMinX <= qryMinX AND docMinY <= qryMinY AND docMaxX >= qryMaxX AND docMaxY >= qryMaxY // Y conditions // docMinY <= qryMinY AND docMaxY >= qryMaxY Query qMinY = NumericRangeQuery.newDoubleRange(docMinY,null,qryMinY,false,true); Query qMaxY = NumericRangeQuery.newDoubleRange(docMaxY,qryMaxY,null,true,false); Query yConditions = this.makeQuery(new Query[]{qMinY,qMaxY},BooleanClause.Occur.MUST); // X conditions Query xConditions = null; // queries that do not cross the date line if (!qryCrossedDateline) { // 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 <= qryMinX AND docMaxX >= qryMaxX Query qMinX = NumericRangeQuery.newDoubleRange(docMinX,null,qryMinX,false,true); Query qMaxX = NumericRangeQuery.newDoubleRange(docMaxX,qryMaxX,null,true,false); Query qMinMax = this.makeQuery(new Query[]{qMinX,qMaxX},BooleanClause.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 <= qryMinX OR docMaxXRight >= qryMaxX Query qXDLLeft = NumericRangeQuery.newDoubleRange(docMinXLeft,null,qryMinX,false,true); Query qXDLRight = NumericRangeQuery.newDoubleRange(docMaxXRight,qryMaxX,null,true,false); Query qXDLLeftRight = this.makeQuery(new Query[]{qXDLLeft,qXDLRight},BooleanClause.Occur.SHOULD); Query qXDL = this.makeXDL(true,qXDLLeftRight); // apply the non-XDL and XDL conditions xConditions = this.makeQuery(new Query[]{qNonXDL,qXDL},BooleanClause.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 <= qryMinX AND docMaxXRight >= qryMaxX Query qXDLLeft = NumericRangeQuery.newDoubleRange(docMinXLeft,null,qryMinX,false,true); Query qXDLRight = NumericRangeQuery.newDoubleRange(docMaxXRight,qryMaxX,null,true,false); Query qXDLLeftRight = this.makeQuery(new Query[]{qXDLLeft,qXDLRight},BooleanClause.Occur.MUST); Query qXDL = this.makeXDL(true,qXDLLeftRight); xConditions = qXDL; } // both X and Y conditions must occur Query xyConditions = this.makeQuery(new Query[]{xConditions,yConditions},BooleanClause.Occur.MUST); return xyConditions; } /** * Constructs a query to retrieve documents that are disjoint to the input envelope. * @return the spatial query */ private Query makeDisjoint() { /* // the original disjoint query does not work for envelopes that cross the date line // docMinX > qryMaxX OR docMaxX < qryMinX OR docMinY > qryMaxY OR docMaxY < qryMinY Query qMinX = NumericRangeQuery.newDoubleRange(docMinX,qryMaxX,null,false,false); Query qMaxX = NumericRangeQuery.newDoubleRange(docMaxX,null,qryMinX,false,false); Query qMinY = NumericRangeQuery.newDoubleRange(docMinY,qryMaxY,null,false,false); Query qMaxY = NumericRangeQuery.newDoubleRange(docMaxY,null,qryMinY,false,false); BooleanQuery bq = new BooleanQuery(); bq.add(qMinX,BooleanClause.Occur.SHOULD); bq.add(qMinY,BooleanClause.Occur.SHOULD); bq.add(qMaxX,BooleanClause.Occur.SHOULD); bq.add(qMaxY,BooleanClause.Occur.SHOULD); */ // general case // docMinX > qryMaxX OR docMaxX < qryMinX OR docMinY > qryMaxY OR docMaxY < qryMinY // Y conditions // docMinY > qryMaxY OR docMaxY < qryMinY Query qMinY = NumericRangeQuery.newDoubleRange(docMinY,qryMaxY,null,false,false); Query qMaxY = NumericRangeQuery.newDoubleRange(docMaxY,null,qryMinY,false,false); Query yConditions = this.makeQuery(new Query[]{qMinY,qMaxY},BooleanClause.Occur.SHOULD); // X conditions Query xConditions = null; // queries that do not cross the date line if (!qryCrossedDateline) { // X Conditions for documents that do not cross the date line, // docMinX > qryMaxX OR docMaxX < qryMinX Query qMinX = NumericRangeQuery.newDoubleRange(docMinX,qryMaxX,null,false,false); Query qMaxX = NumericRangeQuery.newDoubleRange(docMaxX,null,qryMinX,false,false); Query qMinMax = this.makeQuery(new Query[]{qMinX,qMaxX},BooleanClause.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 > qryMaxX OR docMaxXLeft < qryMinX) AND // (docMinXRight > qryMaxX OR docMaxXRight < qryMinX) // where: docMaxXLeft = 180.0, docMinXRight = -180.0 // (docMaxXLeft < qryMinX) equates to (180.0 < qryMinX) and is ignored // (docMinXRight > qryMaxX) equates to (-180.0 > qryMaxX) and is ignored Query qMinXLeft = NumericRangeQuery.newDoubleRange(docMinXLeft,qryMaxX,null,false,false); Query qMaxXRight = NumericRangeQuery.newDoubleRange(docMaxXRight,null,qryMinX,false,false); Query qLeftRight = this.makeQuery(new Query[]{qMinXLeft,qMaxXRight},BooleanClause.Occur.MUST); Query qXDL = this.makeXDL(true,qLeftRight); // apply the non-XDL and XDL conditions xConditions = this.makeQuery(new Query[]{qNonXDL,qXDL},BooleanClause.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 > qryMaxXLeft OR docMaxX < qryMinX) AND (docMinX > qryMaxX OR docMaxX < qryMinXLeft) // where: qryMaxXLeft = 180.0, qryMinXLeft = -180.0 Query qMinXLeft = NumericRangeQuery.newDoubleRange(docMinX,180.0,null,false,false); Query qMaxXLeft = NumericRangeQuery.newDoubleRange(docMaxX,null,qryMinX,false,false); Query qMinXRight = NumericRangeQuery.newDoubleRange(docMinX,qryMaxX,null,false,false); Query qMaxXRight = NumericRangeQuery.newDoubleRange(docMaxX,null,-180.0,false,false); Query qLeft = this.makeQuery(new Query[]{qMinXLeft,qMaxXLeft},BooleanClause.Occur.SHOULD); Query qRight = this.makeQuery(new Query[]{qMinXRight,qMaxXRight},BooleanClause.Occur.SHOULD); Query qLeftRight = this.makeQuery(new Query[]{qLeft,qRight},BooleanClause.Occur.MUST); Query qNonXDL = this.makeXDL(false,qLeftRight); // No need to search for documents that do not cross the date line xConditions = qNonXDL; } // either X or Y conditions should occur Query xyConditions = this.makeQuery(new Query[]{xConditions,yConditions},BooleanClause.Occur.SHOULD); return xyConditions; } /** * Constructs a query to retrieve documents that equal the input envelope. * @return the spatial query */ private Query makeEquals() { // docMinX = qryMinX AND docMinY = qryMinY AND docMaxX = qryMaxX AND docMaxY = qryMaxY Query qMinX = NumericRangeQuery.newDoubleRange(docMinX,qryMinX,qryMinX,true,true); Query qMinY = NumericRangeQuery.newDoubleRange(docMinY,qryMinY,qryMinY,true,true); Query qMaxX = NumericRangeQuery.newDoubleRange(docMaxX,qryMaxX,qryMaxX,true,true); Query qMaxY = NumericRangeQuery.newDoubleRange(docMaxY,qryMaxY,qryMaxY,true,true); BooleanQuery bq = new BooleanQuery(); bq.add(qMinX,BooleanClause.Occur.MUST); bq.add(qMinY,BooleanClause.Occur.MUST); bq.add(qMaxX,BooleanClause.Occur.MUST); bq.add(qMaxY,BooleanClause.Occur.MUST); return bq; } /** * Constructs a query to retrieve documents that intersect the input envelope. * @return the spatial query */ private Query makeIntersects() { // 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(); Query qIsNonXDL = this.makeXDL(false); Query qIsXDL = this.makeXDL(true); Query qHasEnv = this.makeQuery(new Query[]{qIsNonXDL,qIsXDL},BooleanClause.Occur.SHOULD); BooleanQuery qNotDisjoint = new BooleanQuery(); qNotDisjoint.add(qHasEnv,BooleanClause.Occur.MUST); 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; } /** * 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 */ private BooleanQuery makeQuery(Query[] queries, BooleanClause.Occur occur) { BooleanQuery bq = new BooleanQuery(); for (Query query: queries) { bq.add(query,occur); } return bq; } /** * Constructs a query to retrieve documents are fully within the input envelope. * @return the spatial query */ private Query makeWithin() { /* // the original within query does not work for envelopes that cross the date line // docMinX >= qryMinX AND docMinY >= qryMinY AND docMaxX <= qryMaxX AND docMaxY <= qryMaxY Query qMinX = NumericRangeQuery.newDoubleRange(docMinX,qryMinX,null,true,false); Query qMinY = NumericRangeQuery.newDoubleRange(docMinY,qryMinY,null,true,false); Query qMaxX = NumericRangeQuery.newDoubleRange(docMaxX,null,qryMaxX,false,true); Query qMaxY = NumericRangeQuery.newDoubleRange(docMaxY,null,qryMaxY,false,true); BooleanQuery bq = new BooleanQuery(); bq.add(qMinX,BooleanClause.Occur.MUST); bq.add(qMinY,BooleanClause.Occur.MUST); bq.add(qMaxX,BooleanClause.Occur.MUST); bq.add(qMaxY,BooleanClause.Occur.MUST); return bq; */ // general case // docMinX >= qryMinX AND docMinY >= qryMinY AND docMaxX <= qryMaxX AND docMaxY <= qryMaxY // Y conditions // docMinY >= qryMinY AND docMaxY <= qryMaxY Query qMinY = NumericRangeQuery.newDoubleRange(docMinY,qryMinY,null,true,false); Query qMaxY = NumericRangeQuery.newDoubleRange(docMaxY,null,qryMaxY,false,true); Query yConditions = this.makeQuery(new Query[]{qMinY,qMaxY},BooleanClause.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 >= qryMinX AND docMaxXLeft <= 180.0 // AND docMinXRight >= -180.0 AND docMaxXRight <= qryMaxX Query qXDLLeft = NumericRangeQuery.newDoubleRange(docMinXLeft,qryMinX,null,true,false); Query qXDLRight = NumericRangeQuery.newDoubleRange(docMaxXRight,null,qryMaxX,false,true); Query qXDLLeftRight = this.makeQuery(new Query[]{qXDLLeft,qXDLRight},BooleanClause.Occur.MUST); Query qXDL = this.makeXDL(true,qXDLLeftRight); // queries that do not cross the date line if (!qryCrossedDateline) { // X Conditions for documents that do not cross the date line, // docMinX >= qryMinX AND docMaxX <= qryMaxX Query qMinX = NumericRangeQuery.newDoubleRange(docMinX,qryMinX,null,true,false); Query qMaxX = NumericRangeQuery.newDoubleRange(docMaxX,null,qryMaxX,false,true); Query qMinMax = this.makeQuery(new Query[]{qMinX,qMaxX},BooleanClause.Occur.MUST); Query qNonXDL = this.makeXDL(false,qMinMax); // apply the non-XDL or XDL X conditions if ((qryMinX <= -180.0) && qryMaxX >= 180.0) { xConditions = this.makeQuery(new Query[]{qNonXDL,qXDL},BooleanClause.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 >= qryMinX AND docMaxX <= 180.0 Query qMinXLeft = NumericRangeQuery.newDoubleRange(docMinX,qryMinX,null,true,false); Query qMaxXLeft = NumericRangeQuery.newDoubleRange(docMaxX,null,180.0,false,true); Query qLeft = this.makeQuery(new Query[]{qMinXLeft,qMaxXLeft},BooleanClause.Occur.MUST); // the document should be within the right portion of the query // docMinX >= -180.0 AND docMaxX <= qryMaxX Query qMinXRight = NumericRangeQuery.newDoubleRange(docMinX,-180.0,null,true,false); Query qMaxXRight = NumericRangeQuery.newDoubleRange(docMaxX,null,qryMaxX,false,true); Query qRight = this.makeQuery(new Query[]{qMinXRight,qMaxXRight},BooleanClause.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},BooleanClause.Occur.SHOULD); Query qNonXDL = this.makeXDL(false,qLeftRight); // apply the non-XDL and XDL conditions xConditions = this.makeQuery(new Query[]{qNonXDL,qXDL},BooleanClause.Occur.SHOULD); } // both X and Y conditions must occur Query xyConditions = this.makeQuery(new Query[]{xConditions,yConditions},BooleanClause.Occur.MUST); return xyConditions; } /** * 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) { return new TermQuery(new Term(docXDL,""+crossedDateLine)); } /** * 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) { BooleanQuery bq = new BooleanQuery(); bq.add(this.makeXDL(crossedDateLine),BooleanClause.Occur.MUST); bq.add(query,BooleanClause.Occur.MUST); return bq; } }