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

org.apache.solr.schema.LatLonType 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.util.ArrayList;
import java.util.List;
import java.util.Map;

import org.apache.lucene.document.StoredField;
import org.apache.lucene.index.IndexReader;
import org.apache.lucene.index.IndexableField;
import org.apache.lucene.index.LeafReaderContext;
import org.apache.lucene.queries.function.FunctionValues;
import org.apache.lucene.queries.function.ValueSource;
import org.apache.lucene.queries.function.valuesource.VectorValueSource;
import org.apache.lucene.search.BooleanClause;
import org.apache.lucene.search.BooleanQuery;
import org.apache.lucene.search.ConstantScoreWeight;
import org.apache.lucene.search.DocIdSetIterator;
import org.apache.lucene.search.Explanation;
import org.apache.lucene.search.IndexSearcher;
import org.apache.lucene.search.Query;
import org.apache.lucene.search.QueryVisitor;
import org.apache.lucene.search.ScoreMode;
import org.apache.lucene.search.Scorer;
import org.apache.lucene.search.SortField;
import org.apache.lucene.search.Weight;
import org.apache.solr.common.SolrException;
import org.apache.solr.response.TextResponseWriter;
import org.apache.solr.search.DelegatingCollector;
import org.apache.solr.search.ExtendedQueryBase;
import org.apache.solr.search.PostFilter;
import org.apache.solr.search.QParser;
import org.apache.solr.search.SpatialOptions;
import org.apache.solr.uninverting.UninvertingReader.Type;
import org.apache.solr.util.SpatialUtils;
import org.locationtech.spatial4j.context.SpatialContext;
import org.locationtech.spatial4j.distance.DistanceUtils;
import org.locationtech.spatial4j.shape.Point;
import org.locationtech.spatial4j.shape.Rectangle;


/**
 * Represents a Latitude/Longitude as a 2 dimensional point.  Latitude is always specified first.
 *
 * @deprecated use {@link LatLonPointSpatialField} instead
 */
@Deprecated
public class LatLonType extends AbstractSubTypeFieldType implements SpatialQueryable {
  protected static final int LAT = 0;
  protected static final int LON = 1;

  @Override
  protected void init(IndexSchema schema, Map args) {
    super.init(schema, args);
    //TODO: refactor this, as we are creating the suffix cache twice, since the super.init does it too
    createSuffixCache(3);//we need three extra fields: one for the storage field, two for the lat/lon
  }

  @Override
  public List createFields(SchemaField field, Object value) {
    String externalVal = value.toString();
    //we could have 3 fields (two for the lat & lon, one for storage)
    List f = new ArrayList<>(3);
    if (field.indexed()) {
      Point point = SpatialUtils.parsePointSolrException(externalVal, SpatialContext.GEO);
      //latitude
      SchemaField subLatSF = subField(field, LAT, schema);
      f.addAll(subLatSF.createFields(String.valueOf(point.getY())));
      //longitude
      SchemaField subLonSF = subField(field, LON, schema);
      f.addAll(subLonSF.createFields(String.valueOf(point.getX())));
    }

    if (field.stored()) {
      f.add(createField(field.getName(), externalVal, StoredField.TYPE));
    }
    return f;
  }
  
  @Override
  protected void checkSupportsDocValues() {
    // DocValues supported only when enabled at the fieldType 
    if (!hasProperty(DOC_VALUES)) {
      throw new UnsupportedOperationException("LatLonType can't have docValues=true in the field definition, use docValues=true in the fieldType definition, or in subFieldType/subFieldSuffix");
    }
  }


  @Override
  protected Query getSpecializedRangeQuery(QParser parser, SchemaField field, String part1, String part2, boolean minInclusive, boolean maxInclusive) {
    Point p1 = SpatialUtils.parsePointSolrException(part1, SpatialContext.GEO);
    Point p2 = SpatialUtils.parsePointSolrException(part2, SpatialContext.GEO);

    SchemaField latSF = subField(field, LAT, parser.getReq().getSchema());
    SchemaField lonSF = subField(field, LON, parser.getReq().getSchema());
    BooleanQuery.Builder result = new BooleanQuery.Builder();
    // points must currently be ordered... should we support specifying any two opposite corner points?
    result.add(latSF.getType().getRangeQuery(parser, latSF,
        Double.toString(p1.getY()), Double.toString(p2.getY()), minInclusive, maxInclusive), BooleanClause.Occur.MUST);
    result.add(lonSF.getType().getRangeQuery(parser, lonSF,
        Double.toString(p1.getX()), Double.toString(p2.getX()), minInclusive, maxInclusive), BooleanClause.Occur.MUST);
    return result.build();
  }

  @Override
  public Query getFieldQuery(QParser parser, SchemaField field, String externalVal) {
    Point p1 = SpatialUtils.parsePointSolrException(externalVal, SpatialContext.GEO);

    SchemaField latSF = subField(field, LAT, parser.getReq().getSchema());
    SchemaField lonSF = subField(field, LON, parser.getReq().getSchema());
    BooleanQuery.Builder result = new BooleanQuery.Builder();
    result.add(latSF.getType().getFieldQuery(parser, latSF,
        Double.toString(p1.getY())), BooleanClause.Occur.MUST);
    result.add(lonSF.getType().getFieldQuery(parser, lonSF,
        Double.toString(p1.getX())), BooleanClause.Occur.MUST);
    return result.build();
  }



  @Override
  public Query createSpatialQuery(QParser parser, SpatialOptions options) {
    Point point = SpatialUtils.parsePointSolrException(options.pointStr, SpatialContext.GEO);

    // lat & lon in degrees
    double latCenter = point.getY();
    double lonCenter = point.getX();
    
    double distDeg = DistanceUtils.dist2Degrees(options.distance, options.radius);
    Rectangle bbox = DistanceUtils.calcBoxByDistFromPtDEG(latCenter, lonCenter, distDeg, SpatialContext.GEO, null);
    double latMin = bbox.getMinY();
    double latMax = bbox.getMaxY();
    double lonMin, lonMax, lon2Min, lon2Max;
    if (bbox.getCrossesDateLine()) {
       lonMin = -180;
       lonMax = bbox.getMaxX();
       lon2Min = bbox.getMinX();
       lon2Max = 180;
    } else {
       lonMin = bbox.getMinX();
       lonMax = bbox.getMaxX();
       lon2Min = -180;
       lon2Max = 180;
    }
    
    IndexSchema schema = parser.getReq().getSchema();
    
    // Now that we've figured out the ranges, build them!
    SchemaField latSF = subField(options.field, LAT, schema);
    SchemaField lonSF = subField(options.field, LON, schema);

    SpatialDistanceQuery spatial = new SpatialDistanceQuery();


    if (options.bbox) {
      BooleanQuery.Builder result = new BooleanQuery.Builder();

      Query latRange = latSF.getType().getRangeQuery(parser, latSF,
                String.valueOf(latMin),
                String.valueOf(latMax),
                true, true);
      result.add(latRange, BooleanClause.Occur.MUST);

      if (lonMin != -180 || lonMax != 180) {
        Query lonRange = lonSF.getType().getRangeQuery(parser, lonSF,
                String.valueOf(lonMin),
                String.valueOf(lonMax),
                true, true);
        if (lon2Min != -180 || lon2Max != 180) {
          // another valid longitude range
          BooleanQuery.Builder bothLons = new BooleanQuery.Builder();
          bothLons.add(lonRange, BooleanClause.Occur.SHOULD);

          lonRange = lonSF.getType().getRangeQuery(parser, lonSF,
                String.valueOf(lon2Min),
                String.valueOf(lon2Max),
                true, true);
          bothLons.add(lonRange, BooleanClause.Occur.SHOULD);

          lonRange = bothLons.build();
        }

        result.add(lonRange, BooleanClause.Occur.MUST);
      }

      spatial.bboxQuery = result.build();
    }


    spatial.origField = options.field.getName();
    spatial.latSource = latSF.getType().getValueSource(latSF, parser);
    spatial.lonSource = lonSF.getType().getValueSource(lonSF, parser);
    spatial.latMin = latMin;
    spatial.latMax = latMax;
    spatial.lonMin = lonMin;
    spatial.lonMax = lonMax;
    spatial.lon2Min = lon2Min;
    spatial.lon2Max = lon2Max;
    spatial.lon2 = lon2Min != -180 || lon2Max != 180;

    spatial.latCenter = latCenter;
    spatial.lonCenter = lonCenter;
    spatial.dist = options.distance;
    spatial.planetRadius = options.radius;

    spatial.calcDist = !options.bbox;

    return spatial;
  }

  @Override
  public ValueSource getValueSource(SchemaField field, QParser parser) {
    ArrayList vs = new ArrayList<>(2);
    for (int i = 0; i < 2; i++) {
      SchemaField sub = subField(field, i, schema);
      vs.add(sub.getType().getValueSource(sub, parser));
    }
    return new LatLonValueSource(field, vs);
  }

  @Override
  public boolean isPolyField() {
    return true;
  }

  @Override
  public void write(TextResponseWriter writer, String name, IndexableField f) throws IOException {
    writer.writeStr(name, f.stringValue(), true);
  }

  @Override
  public SortField getSortField(SchemaField field, boolean top) {
    throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, "Sorting not supported on LatLonType " + field.getName());
  }
  
  @Override
  public Type getUninversionType(SchemaField sf) {
    return null;
  }



  //It never makes sense to create a single field, so make it impossible to happen

  @Override
  public IndexableField createField(SchemaField field, Object value) {
    throw new UnsupportedOperationException("LatLonType uses multiple fields.  field=" + field.getName());
  }

  @Override
  public double getSphereRadius() {
    return DistanceUtils.EARTH_MEAN_RADIUS_KM;
  }

}

class LatLonValueSource extends VectorValueSource {
  private final SchemaField sf;

  public LatLonValueSource(SchemaField sf, List sources) {
    super(sources);
    this.sf = sf;
  }

  @Override
  public String name() {
    return "latlon";
  }

  @Override
  public String description() {
    return name() + "(" + sf.getName() + ")";
  }
}




////////////////////////////////////////////////////////////////////////////////////////////
// TODO: recast as a value source that doesn't have to match all docs

class SpatialDistanceQuery extends ExtendedQueryBase implements PostFilter {
  String origField;
  ValueSource latSource;
  ValueSource lonSource;
  double lonMin, lonMax, lon2Min, lon2Max, latMin, latMax;
  boolean lon2;

  boolean calcDist;  // actually calculate the distance with haversine
  Query bboxQuery;

  double latCenter;
  double lonCenter;
  double dist;
  double planetRadius;


  @Override
  public Query rewrite(IndexReader reader) throws IOException {
    return bboxQuery != null ? bboxQuery.rewrite(reader) : this;
  }

  protected class SpatialWeight extends ConstantScoreWeight {
    protected IndexSearcher searcher;
    @SuppressWarnings({"rawtypes"})
    protected Map latContext;
    @SuppressWarnings({"rawtypes"})
    protected Map lonContext;

    @SuppressWarnings({"unchecked"})
    public SpatialWeight(IndexSearcher searcher, float boost) throws IOException {
      super(SpatialDistanceQuery.this, boost);
      this.searcher = searcher;
      this.latContext = ValueSource.newContext(searcher);
      this.lonContext = ValueSource.newContext(searcher);
      latSource.createWeight(latContext, searcher);
      lonSource.createWeight(lonContext, searcher);
    }

    @Override
    public Scorer scorer(LeafReaderContext context) throws IOException {
      return new SpatialScorer(context, this, score());
    }

    @Override
    public boolean isCacheable(LeafReaderContext ctx) {
      return false;
    }

    @Override
    public Explanation explain(LeafReaderContext context, int doc) throws IOException {
      return ((SpatialScorer)scorer(context)).explain(super.explain(context, doc), doc);
    }
  }

  protected class SpatialScorer extends Scorer {
    final IndexReader reader;
    final SpatialWeight weight;
    final int maxDoc;
    final float qWeight;
    int doc=-1;
    final FunctionValues latVals;
    final FunctionValues lonVals;


    final double lonMin, lonMax, lon2Min, lon2Max, latMin, latMax;
    final boolean lon2;
    final boolean calcDist;
    
    final double latCenterRad;
    final double lonCenterRad;
    final double latCenterRad_cos;
    final double dist;
    final double planetRadius;

    int lastDistDoc;
    double lastDist;

    @SuppressWarnings({"unchecked"})
    public SpatialScorer(LeafReaderContext readerContext, SpatialWeight w, float qWeight) throws IOException {
      super(w);
      this.weight = w;
      this.qWeight = qWeight;
      this.reader = readerContext.reader();
      this.maxDoc = reader.maxDoc();
      latVals = latSource.getValues(weight.latContext, readerContext);
      lonVals = lonSource.getValues(weight.lonContext, readerContext);

      this.lonMin = SpatialDistanceQuery.this.lonMin;
      this.lonMax = SpatialDistanceQuery.this.lonMax;
      this.lon2Min = SpatialDistanceQuery.this.lon2Min;
      this.lon2Max = SpatialDistanceQuery.this.lon2Max;
      this.latMin = SpatialDistanceQuery.this.latMin;
      this.latMax = SpatialDistanceQuery.this.latMax;
      this.lon2 = SpatialDistanceQuery.this.lon2;
      this.calcDist = SpatialDistanceQuery.this.calcDist;

      this.latCenterRad = SpatialDistanceQuery.this.latCenter * DistanceUtils.DEGREES_TO_RADIANS;
      this.lonCenterRad = SpatialDistanceQuery.this.lonCenter * DistanceUtils.DEGREES_TO_RADIANS;
      this.latCenterRad_cos = this.calcDist ? Math.cos(latCenterRad) : 0;
      this.dist = SpatialDistanceQuery.this.dist;
      this.planetRadius = SpatialDistanceQuery.this.planetRadius;

    }

    boolean match() throws IOException {
      // longitude should generally be more restrictive than latitude
      // (e.g. in the US, it immediately separates the coasts, and in world search separates
      // US from Europe from Asia, etc.
      double lon = lonVals.doubleVal(doc);
      if (! ((lon >= lonMin && lon <=lonMax) || (lon2 && lon >= lon2Min && lon <= lon2Max)) ) {
        return false;
      }

      double lat = latVals.doubleVal(doc);
      if ( !(lat >= latMin && lat <= latMax) ) {
        return false;
      }

      if (!calcDist) return true;

      // TODO: test for internal box where we wouldn't need to calculate the distance

      return dist(lat, lon) <= dist;
    }

    double dist(double lat, double lon) {
      double latRad = lat * DistanceUtils.DEGREES_TO_RADIANS;
      double lonRad = lon * DistanceUtils.DEGREES_TO_RADIANS;
      
      // haversine, specialized to avoid a cos() call on latCenterRad
      double diffX = latCenterRad - latRad;
      double diffY = lonCenterRad - lonRad;
      double hsinX = Math.sin(diffX * 0.5);
      double hsinY = Math.sin(diffY * 0.5);
      double h = hsinX * hsinX +
              (latCenterRad_cos * Math.cos(latRad) * hsinY * hsinY);
      double result = (planetRadius * 2 * Math.atan2(Math.sqrt(h), Math.sqrt(1 - h)));

      // save the results of this calculation
      lastDistDoc = doc;
      lastDist = result;
      
      return result;
    }

    @Override
    public int docID() {
      return doc;
    }

    @Override
    public DocIdSetIterator iterator() {
      return new DocIdSetIterator() {

        @Override
        public int docID() {
          return doc;
        }

        // instead of matching all docs, we could also embed a query.
        // the score could either ignore the subscore, or boost it.
        // Containment:  floatline(foo:myTerm, "myFloatField", 1.0, 0.0f)
        // Boost:        foo:myTerm^floatline("myFloatField",1.0,0.0f)
        @Override
        public int nextDoc() throws IOException {
          for(;;) {
            ++doc;
            if (doc>=maxDoc) {
              return doc=NO_MORE_DOCS;
            }
            if (!match()) continue;
            return doc;
          }
        }

        @Override
        public int advance(int target) throws IOException {
          // this will work even if target==NO_MORE_DOCS
          doc=target-1;
          return nextDoc();
        }

        @Override
        public long cost() {
          return maxDoc;
        }

      };
    }

    @Override
    public float score() throws IOException {
      double dist = (doc == lastDistDoc) ? lastDist : dist(latVals.doubleVal(doc), lonVals.doubleVal(doc));
      return (float)(dist * qWeight);
    }

    @Override
    public float getMaxScore(int upTo) throws IOException {
      return Float.POSITIVE_INFINITY;
    }

    public Explanation explain(Explanation base, int doc) throws IOException {
      if (base.isMatch() == false) {
        return base;
      }
      double dist = dist(latVals.doubleVal(doc), lonVals.doubleVal(doc));

      String description = SpatialDistanceQuery.this.toString();
      return Explanation.match((float) (base.getValue().floatValue() * dist), description + " product of:",
          base, Explanation.match((float) dist, "hsin("+latVals.doubleVal(doc)+","+lonVals.doubleVal(doc)));
    }

  }

  @Override
  public DelegatingCollector getFilterCollector(IndexSearcher searcher) {
    try {
      return new SpatialCollector(new SpatialWeight(searcher, 1f));
    } catch (IOException e) {
      throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, e);
    }
  }


  class SpatialCollector extends DelegatingCollector {
    final SpatialWeight weight;
    SpatialScorer spatialScorer;
    int maxdoc;
    
    public SpatialCollector(SpatialWeight weight) {
      this.weight = weight;
    }

    @Override
    public void collect(int doc) throws IOException {
      spatialScorer.doc = doc;
      if (spatialScorer.match()) leafDelegate.collect(doc);
    }

    @Override
    protected void doSetNextReader(LeafReaderContext context) throws IOException {
      super.doSetNextReader(context);
      maxdoc = context.reader().maxDoc();
      spatialScorer = new SpatialScorer(context, weight, 1.0f);
    }
  }


  @Override
  public Weight createWeight(IndexSearcher searcher, ScoreMode scoreMode, float boost) throws IOException {
    // if we were supposed to use bboxQuery, then we should have been rewritten using that query
    assert bboxQuery == null;
    return new SpatialWeight(searcher, boost);
  }


  /** Prints a user-readable version of this query. */
  @Override
  public String toString(String field)
  {
    return super.getOptions() +
            (calcDist ? "geofilt" : "bbox") + "(latlonSource="+origField +"(" + latSource + "," + lonSource + ")"
            +",latCenter="+latCenter+",lonCenter="+lonCenter
            +",dist=" + dist
            +",latMin=" + latMin + ",latMax="+latMax
            +",lonMin=" + lonMin + ",lonMax"+lonMax
            +",lon2Min=" + lon2Min + ",lon2Max" + lon2Max
            +",calcDist="+calcDist
            +",planetRadius="+planetRadius
            // + (bboxQuery == null ? "" : ",bboxQuery="+bboxQuery)
            +")";
  }


  /** Returns true if o is equal to this. */
  @Override
  public boolean equals(Object o) {
    if (!sameClassAs(o)) return false;
    SpatialDistanceQuery other = (SpatialDistanceQuery)o;
    return     this.latCenter == other.latCenter
            && this.lonCenter == other.lonCenter
            && this.latMin == other.latMin
            && this.latMax == other.latMax
            && this.lonMin == other.lonMin
            && this.lonMax == other.lonMax
            && this.lon2Min == other.lon2Min
            && this.lon2Max == other.lon2Max
            && this.dist == other.dist
            && this.planetRadius == other.planetRadius
            && this.calcDist == other.calcDist
            && this.lonSource.equals(other.lonSource)
            && this.latSource.equals(other.latSource)
        ;
  }

  /** Returns a hash code value for this object. */
  @Override
  public int hashCode() {
    // don't bother making the hash expensive - the center latitude + min longitude will be very unique
    long hash = classHash();
    hash = hash * 31 + Double.doubleToLongBits(latCenter);
    hash = hash * 31 + Double.doubleToLongBits(lonMin);
    return (int) (hash >> 32 + hash);
  }

  @Override
  public void visit(QueryVisitor visitor) {
    visitor.visitLeaf(this);
  }
}






© 2015 - 2024 Weber Informatics LLC | Privacy Policy