package org.apache.solr.schema;
/**
* 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.
*/
import org.apache.lucene.document.Field;
import org.apache.lucene.document.Fieldable;
import org.apache.lucene.search.BooleanClause;
import org.apache.lucene.search.BooleanQuery;
import org.apache.lucene.search.Query;
import org.apache.lucene.search.SortField;
import org.apache.lucene.spatial.DistanceUtils;
import org.apache.lucene.spatial.tier.InvalidGeoException;
import org.apache.solr.common.SolrException;
import org.apache.solr.response.TextResponseWriter;
import org.apache.solr.response.XMLWriter;
import org.apache.solr.search.QParser;
import org.apache.solr.search.SpatialOptions;
import org.apache.solr.search.function.ValueSource;
import org.apache.solr.search.function.VectorValueSource;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
/**
* Represents a Latitude/Longitude as a 2 dimensional point. Latitude is <b>always</b> specified first.
* Can also, optionally, integrate in Spatial Tile capabilities. The default is for tile fields from 4 - 15,
* just as in the SpatialTileField that we are extending.
*/
public class LatLonType extends AbstractSubTypeFieldType implements SpatialQueryable {
protected static final int LAT = 0;
protected static final int LONG = 1;
@Override
protected void init(IndexSchema schema, Map<String, String> 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 Fieldable[] createFields(SchemaField field, String externalVal, float boost) {
//we could have tileDiff + 3 fields (two for the lat/lon, one for storage)
Fieldable[] f = new Fieldable[(field.indexed() ? 2 : 0) + (field.stored() ? 1 : 0)];
if (field.indexed()) {
int i = 0;
double[] latLon = new double[0];
try {
latLon = DistanceUtils.parseLatitudeLongitude(null, externalVal);
} catch (InvalidGeoException e) {
throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, e);
}
//latitude
f[i] = subField(field, i).createField(String.valueOf(latLon[LAT]), boost);
i++;
//longitude
f[i] = subField(field, i).createField(String.valueOf(latLon[LONG]), boost);
}
if (field.stored()) {
f[f.length - 1] = createField(field.getName(), externalVal,
getFieldStore(field, externalVal), Field.Index.NO, Field.TermVector.NO,
false, false, boost);
}
return f;
}
@Override
public Query createSpatialQuery(QParser parser, SpatialOptions options) {
BooleanQuery result = new BooleanQuery();
double[] point = new double[0];
try {
point = DistanceUtils.parseLatitudeLongitude(options.pointStr);
} catch (InvalidGeoException e) {
throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, e);
}
//Get the distance
double[] ur;
double[] ll;
if (options.measStr == null || options.measStr.equals("hsin")) {
ur = DistanceUtils.latLonCornerDegs(point[LAT], point[LONG], options.distance, null, true, options.radius);
ll = DistanceUtils.latLonCornerDegs(point[LAT], point[LONG], options.distance, null, false, options.radius);
} else {
ur = DistanceUtils.vectorBoxCorner(point, null, options.distance, true);
ll = DistanceUtils.vectorBoxCorner(point, null, options.distance, false);
}
SchemaField subSF;
Query range;
double angDistDegs = DistanceUtils.angularDistance(options.distance,
DistanceUtils.EARTH_MEAN_RADIUS_MI) * DistanceUtils.RADIANS_TO_DEGREES;
//for the poles, do something slightly different
if (point[LAT] + angDistDegs > 90.0) { //we cross the north pole
//we don't need a longitude boundary at all
double minLat = Math.min(ll[LAT], ur[LAT]);
subSF = subField(options.field, LAT);
range = subSF.getType().getRangeQuery(parser, subSF,
String.valueOf(minLat),
"90", true, true);
result.add(range, BooleanClause.Occur.MUST);
} else if (point[LAT] - angDistDegs < -90.0) {//we cross the south pole
subSF = subField(options.field, LAT);
double maxLat = Math.max(ll[LAT], ur[LAT]);
range = subSF.getType().getRangeQuery(parser, subSF,
"-90", String.valueOf(maxLat), true, true);
result.add(range, BooleanClause.Occur.MUST);
} else{
//Latitude
//we may need to generate multiple queries depending on the range
//Are we crossing the 180 deg. longitude, if so, we need to do some special things
if (ll[LONG] > 0.0 && ur[LONG] < 0.0) {
//TODO: refactor into common code, etc.
//Now check other side of the Equator
if (ll[LAT] < 0.0 && ur[LAT] > 0.0) {
addEquatorialBoundary(parser, options, result, ur[LAT], ll[LAT]);
} //check poles
else {
subSF = subField(options.field, LAT);
//not crossing the equator
range = subSF.getType().getRangeQuery(parser, subSF,
String.valueOf(ll[LAT]),
String.valueOf(ur[LAT]), true, true);
result.add(range, BooleanClause.Occur.MUST);
}
//Longitude
addMeridianBoundary(parser, options, result, ur[LONG], ll[LONG], "180.0", "-180.0");
} else if (ll[LONG] < 0.0 && ur[LONG] > 0.0) {//prime meridian (0 degrees
//Now check other side of the Equator
if (ll[LAT] < 0.0 && ur[LAT] > 0.0) {
addEquatorialBoundary(parser, options, result, ur[LAT], ll[LAT]);
} else {
subSF = subField(options.field, LAT);
//not crossing the equator
range = subSF.getType().getRangeQuery(parser, subSF,
String.valueOf(ll[LAT]),
String.valueOf(ur[LAT]), true, true);
result.add(range, BooleanClause.Occur.MUST);
}
//Longitude
addMeridianBoundary(parser, options, result, ur[LONG], ll[LONG], "0.0", ".0");
} else {// we are all in the Eastern or Western hemi
//Now check other side of the Equator
if (ll[LAT] < 0.0 && ur[LAT] > 0.0) {
addEquatorialBoundary(parser, options, result, ur[LAT], ll[LAT]);
} else {//we are all in either the Northern or the Southern Hemi.
//TODO: nice to move this up so that it is the first thing and we can avoid the extra checks since
//this is actually the most likely case
subSF = subField(options.field, LAT);
range = subSF.getType().getRangeQuery(parser, subSF,
String.valueOf(ll[LAT]),
String.valueOf(ur[LAT]), true, true);
result.add(range, BooleanClause.Occur.MUST);
}
//Longitude, all in the same hemi
subSF = subField(options.field, LONG);
range = subSF.getType().getRangeQuery(parser, subSF,
String.valueOf(ll[LONG]),
String.valueOf(ur[LONG]), true, true);
result.add(range, BooleanClause.Occur.MUST);
}
}
return result;
}
/**
* Add a boundary condition around a meridian
* @param parser
* @param options
* @param result
* @param upperRightLon
* @param lowerLeftLon
* @param eastern
* @param western
*/
private void addMeridianBoundary(QParser parser, SpatialOptions options, BooleanQuery result, double upperRightLon,
double lowerLeftLon, String eastern, String western) {
SchemaField subSF;
Query range;
BooleanQuery lonQ = new BooleanQuery();
subSF = subField(options.field, LONG);
//Eastern Hemisphere
range = subSF.getType().getRangeQuery(parser, subSF,
String.valueOf(lowerLeftLon),
eastern, true, true);
lonQ.add(range, BooleanClause.Occur.SHOULD);
//Western hemi
range = subSF.getType().getRangeQuery(parser, subSF,
western,
String.valueOf(upperRightLon), true, true);
lonQ.add(range, BooleanClause.Occur.SHOULD);
//One or the other must occur
result.add(lonQ, BooleanClause.Occur.MUST);
}
/**
* Add query conditions for boundaries like the equator, poles and meridians
*
* @param parser
* @param options
* @param result
* @param upperRight
* @param lowerLeft
*/
protected void addEquatorialBoundary(QParser parser, SpatialOptions options, BooleanQuery result, double upperRight, double lowerLeft) {
SchemaField subSF;
Query range;
BooleanQuery tmpQ = new BooleanQuery();
subSF = subField(options.field, LAT);
//southern hemi.
range = subSF.getType().getRangeQuery(parser, subSF,
String.valueOf(lowerLeft),
"0", true, true);
tmpQ.add(range, BooleanClause.Occur.SHOULD);
//northern hemi
range = subSF.getType().getRangeQuery(parser, subSF,
"0", String.valueOf(upperRight), true, true);
tmpQ.add(range, BooleanClause.Occur.SHOULD);
//One or the other must occur
result.add(tmpQ, BooleanClause.Occur.MUST);
}
@Override
public ValueSource getValueSource(SchemaField field, QParser parser) {
ArrayList<ValueSource> vs = new ArrayList<ValueSource>(2);
for (int i = 0; i < 2; i++) {
SchemaField sub = subField(field, i);
vs.add(sub.getType().getValueSource(sub, parser));
}
return new LatLonValueSource(field, vs);
}
@Override
public boolean isPolyField() {
return true;
}
@Override
public void write(XMLWriter xmlWriter, String name, Fieldable f) throws IOException {
xmlWriter.writeStr(name, f.stringValue());
}
@Override
public void write(TextResponseWriter writer, String name, Fieldable f) throws IOException {
writer.writeStr(name, f.stringValue(), false);
}
@Override
public SortField getSortField(SchemaField field, boolean top) {
throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, "Sorting not supported on SpatialTileField " + field.getName());
}
//It never makes sense to create a single field, so make it impossible to happen
@Override
public Field createField(SchemaField field, String externalVal, float boost) {
throw new UnsupportedOperationException("SpatialTileField uses multiple fields. field=" + field.getName());
}
}
class LatLonValueSource extends VectorValueSource {
private final SchemaField sf;
public LatLonValueSource(SchemaField sf, List<ValueSource> sources) {
super(sources);
this.sf = sf;
}
@Override
public String name() {
return "latlon";
}
@Override
public String description() {
return name() + "(" + sf.getName() + ")";
}
}