/**
* License Agreement for OpenSearchServer
*
* Copyright (C) 2012-2014 Emmanuel Keller / Jaeksoft
*
* http://www.open-search-server.com
*
* This file is part of OpenSearchServer.
*
* OpenSearchServer is free software: you can redistribute it and/or
* modify it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* OpenSearchServer is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with OpenSearchServer.
* If not, see <http://www.gnu.org/licenses/>.
**/
package com.jaeksoft.searchlib.filter;
import java.io.IOException;
import java.text.NumberFormat;
import javax.xml.bind.annotation.XmlType;
import org.apache.lucene.analysis.Analyzer;
import org.apache.lucene.search.BooleanClause.Occur;
import org.apache.lucene.search.BooleanQuery;
import org.apache.lucene.search.Query;
import org.apache.lucene.search.TermRangeQuery;
import org.w3c.dom.Node;
import org.xml.sax.SAXException;
import com.jaeksoft.searchlib.SearchLibException;
import com.jaeksoft.searchlib.analysis.filter.DegreesRadiansFilter;
import com.jaeksoft.searchlib.geo.GeoParameters;
import com.jaeksoft.searchlib.query.ParseException;
import com.jaeksoft.searchlib.request.AbstractLocalSearchRequest;
import com.jaeksoft.searchlib.result.ResultSearchSingle;
import com.jaeksoft.searchlib.result.collector.DistanceInterface;
import com.jaeksoft.searchlib.result.collector.DocIdInterface;
import com.jaeksoft.searchlib.schema.SchemaField;
import com.jaeksoft.searchlib.util.Geospatial;
import com.jaeksoft.searchlib.util.StringUtils;
import com.jaeksoft.searchlib.util.Timer;
import com.jaeksoft.searchlib.util.XPathParser;
import com.jaeksoft.searchlib.util.XmlWriter;
import com.jaeksoft.searchlib.web.ServletTransaction;
import com.jaeksoft.searchlib.webservice.query.search.SearchQueryAbstract.OperatorEnum;
public class GeoFilter extends FilterAbstract<GeoFilter> {
public static enum Unit {
METERS("Meters", 1000, Geospatial.EARTH_RADIUS_KM),
KILOMETERS("Kilometers", 1, Geospatial.EARTH_RADIUS_KM),
MILES("Miles", 1, Geospatial.EARTH_RADIUS_MILES),
FEETS("Feets", 5280, Geospatial.EARTH_RADIUS_MILES);
final private String label;
final public double div;
final public double radius;
private Unit(String label, double div, double radius) {
this.label = label;
this.div = div;
this.radius = radius;
}
@Override
final public String toString() {
return label;
}
final public static Unit find(String name) {
for (Unit unit : values())
if (unit.name().equals(name))
return unit;
return null;
}
}
@XmlType(name = "GeoType")
public static enum Type {
SQUARED("Squared"),
RADIUS("Radius");
final private String label;
private Type(String label) {
this.label = label;
}
@Override
final public String toString() {
return label;
}
final public static Type find(String name) {
for (Type type : values())
if (type.name().equals(name))
return type;
return null;
}
}
private transient Query query;
private Unit unit;
private Type type;
private double distance;
public GeoFilter() {
this(Source.REQUEST, false, null, Unit.KILOMETERS, Type.SQUARED, 0);
}
public GeoFilter(Source source, boolean negative, String paramPosition,
Unit unit, Type type, double distance) {
super(FilterType.GEO_FILTER, source, negative, paramPosition);
query = null;
this.unit = unit;
this.type = type;
this.distance = distance;
}
public GeoFilter(XPathParser xpp, Node node) {
super(FilterType.GEO_FILTER, Source.CONFIGXML, "yes".equals(XPathParser
.getAttributeString(node, "negative")), null);
this.unit = Unit.find(XPathParser.getAttributeString(node, "unit"));
this.type = Type.find(XPathParser.getAttributeString(node, "type"));
this.distance = XPathParser.getAttributeDouble(node, "distance");
}
@Override
public String getDescription() {
StringBuilder sb = new StringBuilder("Geo filter: ");
sb.append(type);
sb.append(" - ");
sb.append(distance);
sb.append(' ');
sb.append(unit);
return sb.toString();
}
/**
* @return the unit
*/
public Unit getUnit() {
return unit;
}
/**
* @param unit
* the unit to set
*/
public void setUnit(Unit unit) {
this.unit = unit;
this.query = null;
}
/**
* @return the type
*/
public Type getType() {
return type;
}
/**
* @param type
* the type to set
*/
public void setType(Type type) {
this.type = type;
this.query = null;
}
/**
* @return the distance
*/
public double getMaxDistance() {
return distance;
}
/**
* @param distance
* the distance to set
*/
public void setMaxDistance(double distance) {
this.distance = distance;
this.query = null;
}
@Override
public void writeXmlConfig(XmlWriter xmlWriter) throws SAXException {
xmlWriter.startElement("geofilter", "type", type.name(), "unit",
unit.name(), "distance", Double.toString(distance));
xmlWriter.endElement();
}
@Override
public String getCacheKey(SchemaField defaultField, Analyzer analyzer,
AbstractLocalSearchRequest request) throws ParseException {
StringBuilder sb = new StringBuilder("GeoFilter - ");
sb.append(getQuery(request.getGeoParameters()).toString());
sb.append(" - ");
sb.append(type.name());
return sb.toString();
}
private Query getQuery(GeoParameters geoParams) throws ParseException {
if (query != null)
return query;
double lat = geoParams.getLatitudeRadian();
double lon = geoParams.getLongitudeRadian();
Geospatial.Location loc = new Geospatial.Location(lat, lon);
double dist = distance / unit.div;
NumberFormat nf = DegreesRadiansFilter.getRadiansFormat();
Geospatial.Location[] bound = Geospatial.boundingCoordinates(loc, dist,
unit.radius);
BooleanQuery booleanQuery = new BooleanQuery(true);
booleanQuery.add(
getBooleanQuery(nf, geoParams.getLatitudeField(),
bound[0].latitude, bound[1].latitude), Occur.MUST);
booleanQuery.add(
getBooleanQuery(nf, geoParams.getLongitudeField(),
bound[0].longitude, bound[1].longitude), Occur.MUST);
query = booleanQuery;
return query;
}
private final static BooleanQuery getBooleanQuery(final NumberFormat nf,
final String field, final double min, final double max) {
String fMin = nf.format(min);
String fMax = nf.format(max);
BooleanQuery booleanQuery = new BooleanQuery(true);
if (min < 0) {
if (max < 0)
booleanQuery.add(new TermRangeQuery(field, fMax, fMin, true,
true), Occur.MUST);
else {
booleanQuery.add(new TermRangeQuery(field,
DegreesRadiansFilter.NEGATIVE_RADIAN_ZERO, fMin, true,
true),
Occur.SHOULD);
booleanQuery.add(new TermRangeQuery(field,
DegreesRadiansFilter.POSITIVE_RADIAN_ZERO, fMax, true,
true),
Occur.SHOULD);
}
} else {
booleanQuery.add(new TermRangeQuery(field, fMin, fMax, true, true),
Occur.MUST);
}
return booleanQuery;
}
public static void main(String[] args) {
NumberFormat nf = DegreesRadiansFilter.getRadiansFormat();
System.out.println(getBooleanQuery(nf, "neg_neg", -1.25, -0.50));
System.out.println(getBooleanQuery(nf, "neg_zero", -0.50, 0));
System.out.println(getBooleanQuery(nf, "neg_pos", -0.50, +0.50));
System.out.println(getBooleanQuery(nf, "zero_zero", 0, 0));
System.out.println(getBooleanQuery(nf, "zero_pos", 0, +0.50));
System.out.println(getBooleanQuery(nf, "pos_pos", 0.50, 1.25));
}
@Override
public FilterHits getFilterHits(SchemaField defaultField,
Analyzer analyzer, AbstractLocalSearchRequest searchRequest,
Timer timer) throws ParseException, IOException, SearchLibException {
GeoParameters geoParams = searchRequest.getGeoParameters();
Query query = getQuery(geoParams);
ResultSearchSingle result = getResult(searchRequest.getConfig(), query,
geoParams, timer);
FilterHits filterHits = new FilterHits(result.getDocSetHits()
.getFilterHitsCollector(), isNegative(), timer);
if (type == Type.SQUARED)
return filterHits;
if (type == Type.RADIUS) {
DocIdInterface docIdInterface = result.getDocs();
DistanceInterface distanceInterface = docIdInterface
.getCollector(DistanceInterface.class);
int[] docIds = docIdInterface.getIds();
int pos = 0;
for (float dist : distanceInterface.getDistances()) {
if (dist > distance)
filterHits.fastRemove(docIds[pos]);
pos++;
}
return filterHits;
}
return null;
}
@Override
public GeoFilter duplicate() {
return new GeoFilter(getSource(), isNegative(), getParamPosition(),
unit, type, distance);
}
@Override
public void copyTo(FilterAbstract<?> selectedItem) {
if (!(selectedItem instanceof GeoFilter))
throw new RuntimeException("Wrong filter type "
+ selectedItem.getClass().getName());
super.copyTo(selectedItem);
GeoFilter copyTo = (GeoFilter) selectedItem;
copyTo.unit = unit;
copyTo.type = type;
copyTo.distance = distance;
copyTo.query = null;
}
@Override
final public void setFromServlet(final ServletTransaction transaction,
final String prefix) {
String pp = StringUtils.fastConcat(prefix, getParamPosition(), ".dist");
String q = transaction.getParameterString(pp);
if (q != null)
setMaxDistance(Double.parseDouble(q));
}
@Override
public void setParam(String params) throws SearchLibException {
setMaxDistance(Double.parseDouble(params));
}
@Override
public boolean isDistance() {
return type == GeoFilter.Type.RADIUS;
}
@Override
public void reset() {
query = null;
}
@Override
public OperatorEnum getOperator(OperatorEnum defaultOperator) {
return defaultOperator;
}
}