/*
* 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.search.Explanation;
import com.spatial4j.core.shape.Rectangle;
/**
* The algorithm is implemented as envelope on envelope overlays rather than
* complex polygon on complex polygon overlays.
* <p/>
* <p/>
* Spatial relevance scoring algorithm:
* <p/>
* <br/> queryArea = the area of the input query envelope
* <br/> targetArea = the area of the target envelope (per Lucene document)
* <br/> intersectionArea = the area of the intersection for the query/target envelopes
* <br/> queryPower = the weighting power associated with the query envelope (default = 1.0)
* <br/> targetPower = the weighting power associated with the target envelope (default = 1.0)
* <p/>
* <br/> queryRatio = intersectionArea / queryArea;
* <br/> targetRatio = intersectionArea / targetArea;
* <br/> queryFactor = Math.pow(queryRatio,queryPower);
* <br/> targetFactor = Math.pow(targetRatio,targetPower);
* <br/> score = queryFactor * targetFactor;
* <p/>
* Based on Geoportal's
* <a href="http://geoportal.svn.sourceforge.net/svnroot/geoportal/Geoportal/trunk/src/com/esri/gpt/catalog/lucene/SpatialRankingValueSource.java">
* SpatialRankingValueSource</a>.
*
* @lucene.experimental
*/
public class AreaSimilarity implements BBoxSimilarity {
/**
* Properties associated with the query envelope
*/
private final Rectangle queryExtent;
private final double queryArea;
private final double targetPower;
private final double queryPower;
public AreaSimilarity(Rectangle queryExtent, double queryPower, double targetPower) {
this.queryExtent = queryExtent;
this.queryArea = queryExtent.getArea(null);
this.queryPower = queryPower;
this.targetPower = targetPower;
// if (this.qryMinX > queryExtent.getMaxX()) {
// this.qryCrossedDateline = true;
// this.qryArea = Math.abs(qryMaxX + 360.0 - qryMinX) * Math.abs(qryMaxY - qryMinY);
// } else {
// this.qryArea = Math.abs(qryMaxX - qryMinX) * Math.abs(qryMaxY - qryMinY);
// }
}
public AreaSimilarity(Rectangle queryExtent) {
this(queryExtent, 2.0, 0.5);
}
public String getDelimiterQueryParameters() {
return queryExtent.toString() + ";" + queryPower + ";" + targetPower;
}
@Override
public double score(Rectangle target, Explanation exp) {
if (target == null || queryArea <= 0) {
return 0;
}
double targetArea = target.getArea(null);
if (targetArea <= 0) {
return 0;
}
double score = 0;
double top = Math.min(queryExtent.getMaxY(), target.getMaxY());
double bottom = Math.max(queryExtent.getMinY(), target.getMinY());
double height = top - bottom;
double width = 0;
// queries that cross the date line
if (queryExtent.getCrossesDateLine()) {
// documents that cross the date line
if (target.getCrossesDateLine()) {
double left = Math.max(queryExtent.getMinX(), target.getMinX());
double right = Math.min(queryExtent.getMaxX(), target.getMaxX());
width = right + 360.0 - left;
} else {
double qryWestLeft = Math.max(queryExtent.getMinX(), target.getMaxX());
double qryWestRight = Math.min(target.getMaxX(), 180.0);
double qryWestWidth = qryWestRight - qryWestLeft;
if (qryWestWidth > 0) {
width = qryWestWidth;
} else {
double qryEastLeft = Math.max(target.getMaxX(), -180.0);
double qryEastRight = Math.min(queryExtent.getMaxX(), target.getMaxX());
double qryEastWidth = qryEastRight - qryEastLeft;
if (qryEastWidth > 0) {
width = qryEastWidth;
}
}
}
} else { // queries that do not cross the date line
if (target.getCrossesDateLine()) {
double tgtWestLeft = Math.max(queryExtent.getMinX(), target.getMinX());
double tgtWestRight = Math.min(queryExtent.getMaxX(), 180.0);
double tgtWestWidth = tgtWestRight - tgtWestLeft;
if (tgtWestWidth > 0) {
width = tgtWestWidth;
} else {
double tgtEastLeft = Math.max(queryExtent.getMinX(), -180.0);
double tgtEastRight = Math.min(queryExtent.getMaxX(), target.getMaxX());
double tgtEastWidth = tgtEastRight - tgtEastLeft;
if (tgtEastWidth > 0) {
width = tgtEastWidth;
}
}
} else {
double left = Math.max(queryExtent.getMinX(), target.getMinX());
double right = Math.min(queryExtent.getMaxX(), target.getMaxX());
width = right - left;
}
}
// calculate the score
if ((width > 0) && (height > 0)) {
double intersectionArea = width * height;
double queryRatio = intersectionArea / queryArea;
double targetRatio = intersectionArea / targetArea;
double queryFactor = Math.pow(queryRatio, queryPower);
double targetFactor = Math.pow(targetRatio, targetPower);
score = queryFactor * targetFactor * 10000.0;
if (exp!=null) {
// StringBuilder sb = new StringBuilder();
// sb.append("\nscore=").append(score);
// sb.append("\n query=").append();
// sb.append("\n target=").append(target.toString());
// sb.append("\n intersectionArea=").append(intersectionArea);
//
// sb.append(" queryArea=").append(queryArea).append(" targetArea=").append(targetArea);
// sb.append("\n queryRatio=").append(queryRatio).append(" targetRatio=").append(targetRatio);
// sb.append("\n queryFactor=").append(queryFactor).append(" targetFactor=").append(targetFactor);
// sb.append(" (queryPower=").append(queryPower).append(" targetPower=").append(targetPower).append(")");
exp.setValue((float)score);
exp.setDescription(this.getClass().getSimpleName());
Explanation e = null;
exp.addDetail( e = new Explanation((float)intersectionArea, "IntersectionArea") );
e.addDetail(new Explanation((float)width, "width; Query: "+queryExtent.toString()));
e.addDetail(new Explanation((float)height, "height; Target: "+target.toString()));
exp.addDetail( e = new Explanation((float)queryFactor, "Query") );
e.addDetail(new Explanation((float)queryArea, "area"));
e.addDetail(new Explanation((float)queryRatio, "ratio"));
e.addDetail(new Explanation((float)queryPower, "power"));
exp.addDetail( e = new Explanation((float)targetFactor, "Target") );
e.addDetail(new Explanation((float)targetArea, "area"));
e.addDetail(new Explanation((float)targetRatio, "ratio"));
e.addDetail(new Explanation((float)targetPower, "power"));
}
}
else if(exp !=null) {
exp.setValue(0);
exp.setDescription("Shape does not intersect");
}
return score;
}
/**
* Determines if this ValueSource is equal to another.
*
* @param o the ValueSource to compare
* @return <code>true</code> if the two objects are based upon the same query envelope
*/
@Override
public boolean equals(Object o) {
if (o.getClass() != AreaSimilarity.class)
return false;
AreaSimilarity other = (AreaSimilarity) o;
return getDelimiterQueryParameters().equals(other.getDelimiterQueryParameters());
}
/**
* Returns the ValueSource hash code.
*
* @return the hash code
*/
@Override
public int hashCode() {
return getDelimiterQueryParameters().hashCode();
}
}