org.apache.lucene.spatial.bbox.BBoxOverlapRatioValueSource.java Source code

Java tutorial

Introduction

Here is the source code for org.apache.lucene.spatial.bbox.BBoxOverlapRatioValueSource.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.lucene.spatial.bbox;

import java.util.concurrent.atomic.AtomicReference;

import org.apache.lucene.search.Explanation;
import org.apache.lucene.spatial.ShapeValuesSource;
import org.locationtech.spatial4j.shape.Rectangle;

/**
 * The algorithm is implemented as envelope on envelope (rect on rect) overlays rather than
 * complex polygon on complex polygon overlays.
 * <p>
 * Spatial relevance scoring algorithm:
 * <DL>
 *   <DT>queryArea</DT> <DD>the area of the input query envelope</DD>
 *   <DT>targetArea</DT> <DD>the area of the target envelope (per Lucene document)</DD>
 *   <DT>intersectionArea</DT> <DD>the area of the intersection between the query and target envelopes</DD>
 *   <DT>queryTargetProportion</DT> <DD>A 0-1 factor that divides the score proportion between query and target.
 *   0.5 is evenly.</DD>
 *
 *   <DT>queryRatio</DT> <DD>intersectionArea / queryArea; (see note)</DD>
 *   <DT>targetRatio</DT> <DD>intersectionArea / targetArea; (see note)</DD>
 *   <DT>queryFactor</DT> <DD>queryRatio * queryTargetProportion;</DD>
 *   <DT>targetFactor</DT> <DD>targetRatio * (1 - queryTargetProportion);</DD>
 *   <DT>score</DT> <DD>queryFactor + targetFactor;</DD>
 * </DL>
 * Additionally, note that an optional minimum side length {@code minSideLength} may be used whenever an
 * area is calculated (queryArea, targetArea, intersectionArea). This allows for points or horizontal/vertical lines
 * to be used as the query shape and in such case the descending order should have smallest boxes up front. Without
 * this, a point or line query shape typically scores everything with the same value since there is 0 area.
 * <p>
 * Note: The actual computation of queryRatio and targetRatio is more complicated so that it considers
 * points and lines. Lines have the ratio of overlap, and points are either 1.0 or 0.0 depending on whether
 * it intersects or not.
 * <p>
 * Originally 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> but modified quite a bit. GeoPortal's algorithm will yield a score of 0
 * if either a line or point is compared, and it doesn't output a 0-1 normalized score (it multiplies the factors),
 * and it doesn't support minSideLength, and it had dateline bugs.
 *
 * @lucene.experimental
 */
public class BBoxOverlapRatioValueSource extends BBoxSimilarityValueSource {

    private final boolean isGeo;//-180/+180 degrees  (not part of identity; attached to parent strategy/field)

    private final Rectangle queryExtent;
    private final double queryArea;//not part of identity

    private final double minSideLength;

    private final double queryTargetProportion;

    //TODO option to compute geodetic area

    /**
     *
     * @param rectValueSource mandatory; source of rectangles
     * @param isGeo True if ctx.isGeo() and thus dateline issues should be attended to
     * @param queryExtent mandatory; the query rectangle
     * @param queryTargetProportion see class javadocs. Between 0 and 1.
     * @param minSideLength see class javadocs. 0.0 will effectively disable.
     */
    public BBoxOverlapRatioValueSource(ShapeValuesSource rectValueSource, boolean isGeo, Rectangle queryExtent,
            double queryTargetProportion, double minSideLength) {
        super(rectValueSource);
        this.isGeo = isGeo;
        this.minSideLength = minSideLength;
        this.queryExtent = queryExtent;
        this.queryArea = calcArea(queryExtent.getWidth(), queryExtent.getHeight());
        assert queryArea >= 0;
        this.queryTargetProportion = queryTargetProportion;
        if (queryTargetProportion < 0 || queryTargetProportion > 1.0)
            throw new IllegalArgumentException("queryTargetProportion must be >= 0 and <= 1");
    }

    /** Construct with 75% weighting towards target (roughly GeoPortal's default), geo degrees assumed, no
     * minimum side length. */
    public BBoxOverlapRatioValueSource(ShapeValuesSource rectValueSource, Rectangle queryExtent) {
        this(rectValueSource, true, queryExtent, 0.25, 0.0);
    }

    @Override
    public boolean equals(Object o) {
        if (!super.equals(o))
            return false;

        BBoxOverlapRatioValueSource that = (BBoxOverlapRatioValueSource) o;

        if (Double.compare(that.minSideLength, minSideLength) != 0)
            return false;
        if (Double.compare(that.queryTargetProportion, queryTargetProportion) != 0)
            return false;
        if (!queryExtent.equals(that.queryExtent))
            return false;

        return true;
    }

    @Override
    public int hashCode() {
        int result = super.hashCode();
        long temp;
        result = 31 * result + queryExtent.hashCode();
        temp = Double.doubleToLongBits(minSideLength);
        result = 31 * result + (int) (temp ^ (temp >>> 32));
        temp = Double.doubleToLongBits(queryTargetProportion);
        result = 31 * result + (int) (temp ^ (temp >>> 32));
        return result;
    }

    @Override
    protected String similarityDescription() {
        return queryExtent.toString() + "," + queryTargetProportion;
    }

    @Override
    protected double score(Rectangle target, AtomicReference<Explanation> exp) {
        // calculate "height": the intersection height between two boxes.
        double top = Math.min(queryExtent.getMaxY(), target.getMaxY());
        double bottom = Math.max(queryExtent.getMinY(), target.getMinY());
        double height = top - bottom;
        if (height < 0) {
            if (exp != null) {
                exp.set(Explanation.noMatch("No intersection"));
            }
            return 0;//no intersection
        }

        // calculate "width": the intersection width between two boxes.
        double width = 0;
        {
            Rectangle a = queryExtent;
            Rectangle b = target;
            if (a.getCrossesDateLine() == b.getCrossesDateLine()) {
                //both either cross or don't
                double left = Math.max(a.getMinX(), b.getMinX());
                double right = Math.min(a.getMaxX(), b.getMaxX());
                if (!a.getCrossesDateLine()) {//both don't
                    if (left <= right) {
                        width = right - left;
                    } else if (isGeo && (Math.abs(a.getMinX()) == 180 || Math.abs(a.getMaxX()) == 180)
                            && (Math.abs(b.getMinX()) == 180 || Math.abs(b.getMaxX()) == 180)) {
                        width = 0;//both adjacent to dateline
                    } else {
                        if (exp != null) {
                            exp.set(Explanation.noMatch("No intersection"));
                        }
                        return 0;//no intersection
                    }
                } else {//both cross
                    width = right - left + 360;
                }
            } else {
                if (!a.getCrossesDateLine()) {//then flip
                    a = target;
                    b = queryExtent;
                }
                //a crosses, b doesn't
                double qryWestLeft = Math.max(a.getMinX(), b.getMinX());
                double qryWestRight = b.getMaxX();
                if (qryWestLeft < qryWestRight)
                    width += qryWestRight - qryWestLeft;

                double qryEastLeft = b.getMinX();
                double qryEastRight = Math.min(a.getMaxX(), b.getMaxX());
                if (qryEastLeft < qryEastRight)
                    width += qryEastRight - qryEastLeft;

                if (qryWestLeft > qryWestRight && qryEastLeft > qryEastRight) {
                    if (exp != null) {
                        exp.set(Explanation.noMatch("No intersection"));
                    }
                    return 0;//no intersection
                }
            }
        }

        // calculate queryRatio and targetRatio
        double intersectionArea = calcArea(width, height);
        double queryRatio;
        if (queryArea > 0) {
            queryRatio = intersectionArea / queryArea;
        } else if (queryExtent.getHeight() > 0) {//vert line
            queryRatio = height / queryExtent.getHeight();
        } else if (queryExtent.getWidth() > 0) {//horiz line
            queryRatio = width / queryExtent.getWidth();
        } else {
            queryRatio = queryExtent.relate(target).intersects() ? 1 : 0;//could be optimized
        }

        double targetArea = calcArea(target.getWidth(), target.getHeight());
        assert targetArea >= 0;
        double targetRatio;
        if (targetArea > 0) {
            targetRatio = intersectionArea / targetArea;
        } else if (target.getHeight() > 0) {//vert line
            targetRatio = height / target.getHeight();
        } else if (target.getWidth() > 0) {//horiz line
            targetRatio = width / target.getWidth();
        } else {
            targetRatio = target.relate(queryExtent).intersects() ? 1 : 0;//could be optimized
        }
        assert queryRatio >= 0 && queryRatio <= 1 : queryRatio;
        assert targetRatio >= 0 && targetRatio <= 1 : targetRatio;

        // combine ratios into a score

        double queryFactor = queryRatio * queryTargetProportion;
        double targetFactor = targetRatio * (1.0 - queryTargetProportion);
        double score = queryFactor + targetFactor;

        if (exp != null) {
            String minSideDesc = minSideLength > 0.0 ? " (minSide=" + minSideLength + ")" : "";
            exp.set(Explanation.match((float) score,
                    this.getClass().getSimpleName() + ": queryFactor + targetFactor",
                    Explanation.match((float) intersectionArea, "IntersectionArea" + minSideDesc,
                            Explanation.match((float) width, "width"), Explanation.match((float) height, "height"),
                            Explanation.match((float) queryTargetProportion, "queryTargetProportion")),
                    Explanation.match((float) queryFactor, "queryFactor",
                            Explanation.match((float) targetRatio, "ratio"),
                            Explanation.match((float) queryArea, "area of " + queryExtent + minSideDesc)),
                    Explanation.match((float) targetFactor, "targetFactor",
                            Explanation.match((float) targetRatio, "ratio"),
                            Explanation.match((float) targetArea, "area of " + target + minSideDesc))));
        }

        return score;
    }

    /** Calculates the area while applying the minimum side length. */
    private double calcArea(double width, double height) {
        return Math.max(minSideLength, width) * Math.max(minSideLength, height);
    }

}