All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.apache.solr.schema.LatLonPointSpatialField Maven / Gradle / Ivy

/*
 * 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.solr.schema;

import java.io.IOException;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.Objects;

import org.apache.lucene.document.Field;
import org.apache.lucene.document.LatLonDocValuesField;
import org.apache.lucene.document.LatLonPoint;
import org.apache.lucene.geo.GeoEncodingUtils;
import org.apache.lucene.index.DocValues;
import org.apache.lucene.index.IndexableField;
import org.apache.lucene.index.LeafReaderContext;
import org.apache.lucene.queries.function.ValueSource;
import org.apache.lucene.search.DoubleValues;
import org.apache.lucene.search.DoubleValuesSource;
import org.apache.lucene.search.FieldComparator;
import org.apache.lucene.search.IndexOrDocValuesQuery;
import org.apache.lucene.search.IndexSearcher;
import org.apache.lucene.search.LeafFieldComparator;
import org.apache.lucene.search.Query;
import org.apache.lucene.search.SortField;
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.solr.common.SolrException;
import org.locationtech.spatial4j.context.SpatialContext;
import org.locationtech.spatial4j.distance.DistanceUtils;
import org.locationtech.spatial4j.shape.Circle;
import org.locationtech.spatial4j.shape.Point;
import org.locationtech.spatial4j.shape.Rectangle;
import org.locationtech.spatial4j.shape.Shape;

import static java.math.RoundingMode.CEILING;

/**
 * A spatial implementation based on Lucene's {@code LatLonPoint} and {@code LatLonDocValuesField}. The
 * first is based on Lucene's "Points" API, which is a BKD Index.  This field type is strictly limited to
 * coordinates in lat/lon decimal degrees.  The accuracy is about a centimeter (1.042cm).
 */
// TODO once LLP & LLDVF are out of Lucene Sandbox, we should be able to javadoc reference them.
public class LatLonPointSpatialField extends AbstractSpatialFieldType implements SchemaAware {
  private IndexSchema schema;

  // TODO handle polygons

  @Override
  protected void checkSupportsDocValues() { // we support DocValues
  }

  @Override
  public void inform(IndexSchema schema) {
    this.schema = schema;
  }

  @Override
  protected SpatialStrategy newSpatialStrategy(String fieldName) {
    SchemaField schemaField = schema.getField(fieldName); // TODO change AbstractSpatialFieldType so we get schemaField?
    return new LatLonPointSpatialStrategy(ctx, fieldName, schemaField.indexed(), schemaField.hasDocValues());
  }

  @Override
  public String toExternal(IndexableField f) {
    if (f.numericValue() != null) {
      return decodeDocValueToString(f.numericValue().longValue());
    }
    return super.toExternal(f);
  }

  /**
   * Decodes the docValues number into latitude and longitude components, formatting as "lat,lon".
   * The encoding is governed by {@code LatLonDocValuesField}.  The decimal output representation is reflective
   * of the available precision.
   * @param value Non-null; stored location field data
   * @return Non-null; "lat, lon"
   */
  public static String decodeDocValueToString(long value) {
    final double latDouble = GeoEncodingUtils.decodeLatitude((int) (value >> 32));
    final double lonDouble = GeoEncodingUtils.decodeLongitude((int) (value & 0xFFFFFFFFL));
    // This # decimal places gets us close to our available precision to 1.40cm; we have a test for it.
    // CEILING round-trips (decode then re-encode then decode to get identical results). Others did not. It also
    //   reverses the "floor" that occurred when we encoded.
    final int DECIMAL_PLACES = 7;
    final RoundingMode ROUND_MODE = CEILING;
    BigDecimal latitudeDecoded = BigDecimal.valueOf(latDouble).setScale(DECIMAL_PLACES, ROUND_MODE);
    BigDecimal longitudeDecoded = BigDecimal.valueOf(lonDouble).setScale(DECIMAL_PLACES, ROUND_MODE);
    return latitudeDecoded.stripTrailingZeros().toPlainString() + ","
        + longitudeDecoded.stripTrailingZeros().toPlainString();
    // return ((float)latDouble) + "," + ((float)lonDouble);  crude but not quite as accurate
  }

  // TODO move to Lucene-spatial-extras once LatLonPoint & LatLonDocValuesField moves out of sandbox
  public static class LatLonPointSpatialStrategy extends SpatialStrategy {

    private final boolean indexed; // for query/filter
    private final boolean docValues; // for sort. Can be used to query/filter.

    public LatLonPointSpatialStrategy(SpatialContext ctx, String fieldName, boolean indexed, boolean docValues) {
      super(ctx, fieldName);
      if (!ctx.isGeo()) {
        throw new IllegalArgumentException("ctx must be geo=true: " + ctx);
      }
      this.indexed = indexed;
      this.docValues = docValues;
    }

    @Override
    public Field[] createIndexableFields(Shape shape) {
      if (!(shape instanceof Point)) {
        throw new SolrException(SolrException.ErrorCode.BAD_REQUEST,
            getClass().getSimpleName() + " only supports indexing points; got: " + shape);
      }
      Point point = (Point) shape;

      int fieldsLen = (indexed ? 1 : 0) + (docValues ? 1 : 0);
      Field[] fields = new Field[fieldsLen];
      int fieldsIdx = 0;
      if (indexed) {
        fields[fieldsIdx++] = new LatLonPoint(getFieldName(), point.getY(), point.getX());
      }
      if (docValues) {
        fields[fieldsIdx++] = new LatLonDocValuesField(getFieldName(), point.getY(), point.getX());
      }
      return fields;
    }

    @Override
    public Query makeQuery(SpatialArgs args) {
      if (args.getOperation() != SpatialOperation.Intersects) {
        throw new UnsupportedSpatialOperation(args.getOperation());
      }
      Shape shape = args.getShape();
      if (indexed && docValues) {
        return new IndexOrDocValuesQuery(makeQueryFromIndex(shape), makeQueryFromDocValues(shape));
      } else if (indexed) {
        return makeQueryFromIndex(shape);
      } else if (docValues) {
        return makeQueryFromDocValues(shape);
      } else {
        throw new SolrException(SolrException.ErrorCode.BAD_REQUEST,
            getFieldName() + " needs indexed (preferred) or docValues to support search");
      }
    }

    // Uses LatLonPoint
    protected Query makeQueryFromIndex(Shape shape) {
      // note: latitude then longitude order for LLP's methods
      if (shape instanceof Circle) {
        Circle circle = (Circle) shape;
        double radiusMeters = circle.getRadius() * DistanceUtils.DEG_TO_KM * 1000;
        return LatLonPoint.newDistanceQuery(getFieldName(),
            circle.getCenter().getY(), circle.getCenter().getX(),
            radiusMeters);
      } else if (shape instanceof Rectangle) {
        Rectangle rect = (Rectangle) shape;
        return LatLonPoint.newBoxQuery(getFieldName(),
            rect.getMinY(), rect.getMaxY(), rect.getMinX(), rect.getMaxX());
      } else if (shape instanceof Point) {
        Point point = (Point) shape;
        return LatLonPoint.newDistanceQuery(getFieldName(),
            point.getY(), point.getX(), 0);
      } else {
        throw new UnsupportedOperationException("Shape " + shape.getClass() + " is not supported by " + getClass());
      }
//      } else if (shape instanceof LucenePolygonShape) {
//        // TODO support multi-polygon
//        Polygon poly = ((LucenePolygonShape)shape).lucenePolygon;
//        return LatLonPoint.newPolygonQuery(getFieldName(), poly);
    }

    // Uses DocValuesField  (otherwise identical to above)
    protected Query makeQueryFromDocValues(Shape shape) {
      // note: latitude then longitude order for LLP's methods
      if (shape instanceof Circle) {
        Circle circle = (Circle) shape;
        double radiusMeters = circle.getRadius() * DistanceUtils.DEG_TO_KM * 1000;
        return LatLonDocValuesField.newSlowDistanceQuery(getFieldName(),
            circle.getCenter().getY(), circle.getCenter().getX(),
            radiusMeters);
      } else if (shape instanceof Rectangle) {
        Rectangle rect = (Rectangle) shape;
        return LatLonDocValuesField.newSlowBoxQuery(getFieldName(),
            rect.getMinY(), rect.getMaxY(), rect.getMinX(), rect.getMaxX());
      } else if (shape instanceof Point) {
        Point point = (Point) shape;
        return LatLonDocValuesField.newSlowDistanceQuery(getFieldName(),
            point.getY(), point.getX(), 0);
      } else {
        throw new UnsupportedOperationException("Shape " + shape.getClass() + " is not supported by " + getClass());
      }
//      } else if (shape instanceof LucenePolygonShape) {
//        // TODO support multi-polygon
//        Polygon poly = ((LucenePolygonShape)shape).lucenePolygon;
//        return LatLonPoint.newPolygonQuery(getFieldName(), poly);
    }

    @Override
    public DoubleValuesSource makeDistanceValueSource(Point queryPoint, double multiplier) {
      if (!docValues) {
        throw new SolrException(SolrException.ErrorCode.BAD_REQUEST,
            getFieldName() + " must have docValues enabled to support this feature");
      }
      // Internally, the distance from LatLonPointSortField/Comparator is in meters. So we must also go from meters to
      //  degrees, which is what Lucene spatial-extras is oriented around.
      return new DistanceSortValueSource(getFieldName(), queryPoint,
          DistanceUtils.KM_TO_DEG / 1000.0 * multiplier);
    }

    /**
     * A {@link ValueSource} based around {@code LatLonDocValuesField#newDistanceSort(String, double, double)}.
     */
    private static class DistanceSortValueSource extends DoubleValuesSource {
      private final String fieldName;
      private final Point queryPoint;
      private final double multiplier;

      DistanceSortValueSource(String fieldName, Point queryPoint, double multiplier) {
        this.fieldName = fieldName;
        this.queryPoint = queryPoint;
        this.multiplier = multiplier;
      }

      @Override
      public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        DistanceSortValueSource that = (DistanceSortValueSource) o;
        return Double.compare(that.multiplier, multiplier) == 0 &&
            Objects.equals(fieldName, that.fieldName) &&
            Objects.equals(queryPoint, that.queryPoint);
      }

      @Override
      public int hashCode() {
        return Objects.hash(fieldName, queryPoint, multiplier);
      }

      @Override
      public DoubleValues getValues(LeafReaderContext ctx, DoubleValues scores) throws IOException {
        return new DoubleValues() {

          @SuppressWarnings("unchecked")
          final FieldComparator comparator =
              (FieldComparator) getSortField(false).getComparator(1, 1);
          final LeafFieldComparator leafComparator = comparator.getLeafComparator(ctx);
          final double mult = multiplier; // so it's a local field

          double value = Double.POSITIVE_INFINITY;

          @Override
          public double doubleValue() throws IOException {
            return value;
          }

          @Override
          public boolean advanceExact(int doc) throws IOException {
            leafComparator.copy(0, doc);
            value = comparator.value(0) * mult;
            return true;
          }
        };
      }

      @Override
      public boolean needsScores() {
        return false;
      }

      @Override
      public boolean isCacheable(LeafReaderContext ctx) {
        return DocValues.isCacheable(ctx, fieldName);
      }

      @Override
      public DoubleValuesSource rewrite(IndexSearcher searcher) throws IOException {
        return this;
      }

      @Override
      public String toString() {
        return "distSort(" + fieldName + ", " + queryPoint + ", mult:" + multiplier + ")";
      }

      @Override
      public SortField getSortField(boolean reverse) {
        if (reverse) {
          return super.getSortField(true); // will use an impl that calls getValues
        }
        return LatLonDocValuesField.newDistanceSort(fieldName, queryPoint.getY(), queryPoint.getX());
      }

    }

  }

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy