/**
Copyright (C) SYSTAP, LLC DBA Blazegraph 2006-2016. All rights reserved.
Contact:
SYSTAP, LLC DBA Blazegraph
2501 Calvert ST NW #106
Washington, DC 20008
licenses@blazegraph.com
This program 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; version 2 of the License.
This program 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 this program; if not, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
/*
* Created on July 27, 2015
*/
package com.bigdata.rdf.sparql.ast.eval;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.Executor;
import java.util.concurrent.FutureTask;
import org.apache.log4j.Logger;
import org.openrdf.model.Literal;
import org.openrdf.model.URI;
import com.bigdata.bop.BOp;
import com.bigdata.bop.BOpContextBase;
import com.bigdata.bop.BufferAnnotations;
import com.bigdata.bop.Constant;
import com.bigdata.bop.IBindingSet;
import com.bigdata.bop.IConstant;
import com.bigdata.bop.IPredicate;
import com.bigdata.bop.IVariable;
import com.bigdata.bop.IVariableOrConstant;
import com.bigdata.bop.PipelineOp;
import com.bigdata.bop.Var;
import com.bigdata.bop.fed.QueryEngineFactory;
import com.bigdata.bop.join.BaseJoinStats;
import com.bigdata.bop.join.PipelineJoin;
import com.bigdata.bop.join.PipelineJoin.Annotations;
import com.bigdata.btree.IRangeQuery;
import com.bigdata.btree.ITuple;
import com.bigdata.btree.filter.Advancer;
import com.bigdata.btree.filter.TupleFilter;
import com.bigdata.journal.AbstractJournal;
import com.bigdata.rdf.internal.IV;
import com.bigdata.rdf.internal.IVUtility;
import com.bigdata.rdf.internal.constraints.RangeBOp;
import com.bigdata.rdf.internal.gis.CoordinateDD;
import com.bigdata.rdf.internal.gis.CoordinateUtility;
import com.bigdata.rdf.internal.gis.ICoordinate.UNITS;
import com.bigdata.rdf.internal.impl.TermId;
import com.bigdata.rdf.internal.impl.extensions.GeoSpatialLiteralExtension;
import com.bigdata.rdf.internal.impl.literal.LiteralExtensionIV;
import com.bigdata.rdf.model.BigdataURI;
import com.bigdata.rdf.model.BigdataValue;
import com.bigdata.rdf.model.BigdataValueFactory;
import com.bigdata.rdf.sparql.ast.ConstantNode;
import com.bigdata.rdf.sparql.ast.DummyConstantNode;
import com.bigdata.rdf.sparql.ast.GlobalAnnotations;
import com.bigdata.rdf.sparql.ast.GroupNodeBase;
import com.bigdata.rdf.sparql.ast.IGroupMemberNode;
import com.bigdata.rdf.sparql.ast.RangeNode;
import com.bigdata.rdf.sparql.ast.StatementPatternNode;
import com.bigdata.rdf.sparql.ast.TermNode;
import com.bigdata.rdf.sparql.ast.VarNode;
import com.bigdata.rdf.sparql.ast.optimizers.ASTRangeOptimizer;
import com.bigdata.rdf.sparql.ast.service.BigdataNativeServiceOptions;
import com.bigdata.rdf.sparql.ast.service.BigdataServiceCall;
import com.bigdata.rdf.sparql.ast.service.IServiceOptions;
import com.bigdata.rdf.sparql.ast.service.ServiceCallCreateParams;
import com.bigdata.rdf.sparql.ast.service.ServiceNode;
import com.bigdata.rdf.spo.ISPO;
import com.bigdata.rdf.spo.SPO;
import com.bigdata.rdf.spo.SPOKeyOrder;
import com.bigdata.rdf.store.AbstractTripleStore;
import com.bigdata.relation.IRelation;
import com.bigdata.relation.accesspath.AccessPath;
import com.bigdata.relation.accesspath.BlockingBuffer;
import com.bigdata.relation.accesspath.ChunkConsumerIterator;
import com.bigdata.relation.accesspath.UnsynchronizedArrayBuffer;
import com.bigdata.service.geospatial.GeoSpatial;
import com.bigdata.service.geospatial.GeoSpatial.GeoFunction;
import com.bigdata.service.geospatial.GeoSpatialDatatypeFieldConfiguration.ServiceMapping;
import com.bigdata.service.geospatial.GeoSpatialDatatypeFieldConfiguration.ValueType;
import com.bigdata.service.geospatial.GeoSpatialConfig;
import com.bigdata.service.geospatial.GeoSpatialCounters;
import com.bigdata.service.geospatial.GeoSpatialDatatypeConfiguration;
import com.bigdata.service.geospatial.GeoSpatialDatatypeFieldConfiguration;
import com.bigdata.service.geospatial.GeoSpatialSearchException;
import com.bigdata.service.geospatial.IGeoSpatialLiteralSerializer;
import com.bigdata.service.geospatial.IGeoSpatialQuery;
import com.bigdata.service.geospatial.ZOrderIndexBigMinAdvancer;
import com.bigdata.service.geospatial.impl.GeoSpatialQuery;
import com.bigdata.service.geospatial.impl.GeoSpatialUtility.PointLatLon;
import com.bigdata.util.concurrent.Haltable;
import com.bigdata.util.concurrent.LatchedExecutor;
import cutthecrap.utils.striterators.ICloseableIterator;
import cutthecrap.utils.striterators.Resolver;
import cutthecrap.utils.striterators.Striterator;
/**
* A factory for a geospatial service, see {@link GeoSpatial#SEARCH}.
*
* @author <a href="mailto:ms@metaphacts.com">Michael Schmidt</a>
* @version $Id$
*/
public class GeoSpatialServiceFactory extends AbstractServiceFactoryBase {
private static final Logger log = Logger
.getLogger(GeoSpatialServiceFactory.class);
final protected static boolean INFO = log.isInfoEnabled();
final protected static boolean DEBUG = log.isDebugEnabled();
/*
* Note: This could extend the base class to allow for search service
* configuration options.
*/
private final BigdataNativeServiceOptions serviceOptions;
public GeoSpatialServiceFactory() {
serviceOptions = new BigdataNativeServiceOptions();
serviceOptions.setRunFirst(true);
}
@Override
public BigdataNativeServiceOptions getServiceOptions() {
return serviceOptions;
}
public BigdataServiceCall create(final ServiceCallCreateParams createParams) {
if (createParams == null)
throw new IllegalArgumentException();
final AbstractTripleStore store = createParams.getTripleStore();
final Properties props = store.getIndexManager() != null
&& store.getIndexManager() instanceof AbstractJournal ? ((AbstractJournal) store
.getIndexManager()).getProperties() : null;
final GeoSpatialDefaults dflts = new GeoSpatialDefaults(props);
final ServiceNode serviceNode = createParams.getServiceNode();
if (serviceNode == null)
throw new IllegalArgumentException();
/*
* Validate the geospatial predicates for a given search variable.
*/
final Map<IVariable<?>, Map<URI, StatementPatternNode>> map = verifyGraphPattern(
store, serviceNode.getGraphPattern());
if (map == null)
throw new RuntimeException("Not a geospatial service request.");
if (map.size() != 1)
throw new RuntimeException(
"Multiple geospatial service requests may not be combined.");
final Map.Entry<IVariable<?>, Map<URI, StatementPatternNode>> e = map
.entrySet().iterator().next();
final IVariable<?> searchVar = e.getKey();
final Map<URI, StatementPatternNode> statementPatterns = e.getValue();
validateSearch(searchVar, statementPatterns);
/**
* Get the service call configuration from annotations (attachable via query hints).
* Here's how to define the hints:
*
* <code>
hint:Prior <http://www.bigdata.com/queryHints#maxParallel> "20" .
hint:Prior <http://www.bigdata.com/queryHints#com.bigdata.relation.accesspath.BlockingBuffer.chunkOfChunksCapacity> "10" .
hint:Prior <http://www.bigdata.com/queryHints#com.bigdata.relation.accesspath.IBuffer.chunkCapacity> "100" .
hint:Prior <http://www.bigdata.com/queryHints#com.bigdata.bop.join.PipelineJoin.avgDataPointsPerThread> "25000" .
</code>
*/
final Integer maxParallel =
serviceNode.getQueryHintAsInteger(
PipelineOp.Annotations.MAX_PARALLEL,
PipelineOp.Annotations.DEFAULT_MAX_PARALLEL);
final Integer minDatapointsPerTask =
serviceNode.getQueryHintAsInteger(
Annotations.MIN_DATAPOINTS_PER_TASK,
Annotations.DEFAULT_MIN_DATAPOINTS_PER_TASK);
final Integer numTasksPerThread =
serviceNode.getQueryHintAsInteger(
Annotations.NUM_TASKS_PER_THREAD,
Annotations.DEFAULT_NUM_TASKS_PER_THREAD);
final Integer threadLocalBufferCapacity =
serviceNode.getQueryHintAsInteger(
BufferAnnotations.CHUNK_CAPACITY,
BufferAnnotations.DEFAULT_CHUNK_CAPACITY);
final Integer globalBufferChunkOfChunksCapacity =
serviceNode.getQueryHintAsInteger(
BufferAnnotations.CHUNK_OF_CHUNKS_CAPACITY,
BufferAnnotations.DEFAULT_CHUNK_OF_CHUNKS_CAPACITY);
if (DEBUG) {
log.debug("maxParallel=" + maxParallel);
log.debug("numTasksPerThread=" + numTasksPerThread);
log.debug("threadLocalBufferCapacity=" + threadLocalBufferCapacity);
log.debug("globalBufferChunkOfChunksCapacity=" + globalBufferChunkOfChunksCapacity);
}
if (!store.getLexiconRelation().getLexiconConfiguration().isGeoSpatial()) {
throw new GeoSpatialSearchException(
"Geospatial is disabled. Please enable geospatial and reload your data.");
}
/*
* Create and return the geospatial service call object, which will
* execute this search request.
*/
return new GeoSpatialServiceCall(searchVar, statementPatterns,
getServiceOptions(), dflts, store, maxParallel,
numTasksPerThread*maxParallel /* max num tasks to generate */,
minDatapointsPerTask, threadLocalBufferCapacity,
globalBufferChunkOfChunksCapacity, createParams.getStats());
}
/**
* Validate the search request. This looks for search magic predicates and
* returns them all. It is an error if anything else is found in the group.
* All such search patterns are reported back by this method, but the service
* can only be invoked for one a single search variable at a time. The caller
* will detect both the absence of any search and the presence of more than
* one search and throw an exception.
*/
private Map<IVariable<?>, Map<URI, StatementPatternNode>> verifyGraphPattern(
final AbstractTripleStore database,
final GroupNodeBase<IGroupMemberNode> group) {
// lazily allocate iff we find some search predicates in this group.
Map<IVariable<?>, Map<URI, StatementPatternNode>> tmp = null;
final int arity = group.arity();
for (int i = 0; i < arity; i++) {
final BOp child = group.get(i);
if (child instanceof GroupNodeBase<?>) {
throw new RuntimeException("Nested groups are not allowed.");
}
if (child instanceof StatementPatternNode) {
final StatementPatternNode sp = (StatementPatternNode) child;
final TermNode p = sp.p();
if (!p.isConstant())
throw new RuntimeException("Expecting geospatial predicate: "
+ sp);
final URI uri = (URI) ((ConstantNode) p).getValue();
if (!uri.stringValue().startsWith(GeoSpatial.NAMESPACE))
throw new RuntimeException("Expecting search predicate: " + sp);
/*
* Some search predicate.
*/
if (!ASTGeoSpatialSearchOptimizer.searchUris.contains(uri))
throw new RuntimeException("Unknown geospatial magic predicate: " + uri);
final TermNode s = sp.s();
if (!s.isVariable())
throw new RuntimeException(
"Subject of geospatial search pattern must not be a constant: " + sp);
final IVariable<?> searchVar = ((VarNode) s).getValueExpression();
// Lazily allocate map.
if (tmp == null) {
tmp = new LinkedHashMap<IVariable<?>, Map<URI, StatementPatternNode>>();
}
// Lazily allocate set for that searchVar.
Map<URI, StatementPatternNode> statementPatterns = tmp
.get(searchVar);
if (statementPatterns == null) {
tmp.put(
searchVar,
statementPatterns = new LinkedHashMap<URI, StatementPatternNode>());
}
// Add search predicate to set for that searchVar.
statementPatterns.put(uri, sp);
}
}
return tmp;
}
/**
* Validate the search. There must be exactly one {@link GeoSpatial#SEARCH}
* predicate. There should not be duplicates of any of the search predicates
* for a given searchVar.
*/
private void validateSearch(final IVariable<?> searchVar,
final Map<URI, StatementPatternNode> statementPatterns) {
final Set<URI> uris = new LinkedHashSet<URI>();
for (StatementPatternNode sp : statementPatterns.values()) {
final URI uri = (URI) (sp.p()).getValue();
if (!uris.add(uri))
throw new RuntimeException(
"Search predicate appears multiple times for same search variable: predicate="
+ uri + ", searchVar=" + searchVar);
if (uri.equals(GeoSpatial.PREDICATE) || uri.equals(GeoSpatial.CONTEXT)) {
assertObjectIsURI(sp);
} else if (uri.equals(GeoSpatial.LOCATION_VALUE) || uri.equals(GeoSpatial.TIME_VALUE)
|| uri.equals(GeoSpatial.LAT_VALUE) || uri.equals(GeoSpatial.LON_VALUE)
|| uri.equals(GeoSpatial.LITERAL_VALUE) || uri.equals(GeoSpatial.COORD_SYSTEM_VALUE)
|| uri.equals(GeoSpatial.CUSTOM_FIELDS_VALUES)
|| uri.equals(GeoSpatial.LOCATION_AND_TIME_VALUE)) {
assertObjectIsVariable(sp);
} else if (uri.equals(GeoSpatial.SEARCH_DATATYPE)) {
assertObjectIsUriOrVariable(sp);
} else {
assertObjectIsLiteralOrVariable(sp);
}
}
}
private void assertObjectIsURI(final StatementPatternNode sp) {
final TermNode o = sp.o();
if (o instanceof URI) {
throw new IllegalArgumentException(
"Object is not a URI: " + sp);
}
}
private void assertObjectIsUriOrVariable(final StatementPatternNode sp) {
final TermNode o = sp.o();
boolean isNotUri= !o.isConstant()
|| !(((ConstantNode) o).getValue() instanceof URI);
boolean isNotVariable = !o.isVariable();
if (isNotUri && isNotVariable) {
throw new IllegalArgumentException(
"Object is not uri or variable: " + sp);
}
}
private void assertObjectIsLiteralOrVariable(final StatementPatternNode sp) {
final TermNode o = sp.o();
boolean isNotLiteral = !o.isConstant()
|| !(((ConstantNode) o).getValue() instanceof Literal);
boolean isNotVariable = !o.isVariable();
if (isNotLiteral && isNotVariable) {
throw new IllegalArgumentException(
"Object is not literal or variable: " + sp);
}
}
private void assertObjectIsVariable(final StatementPatternNode sp) {
final TermNode o = sp.o();
if (!o.isVariable()) {
throw new IllegalArgumentException(
"Object is not a variable: " + sp);
}
}
/**
*
* Note: This has the {@link AbstractTripleStore} reference attached. This is
* not a {@link Serializable} object. It MUST run on the query controller.
*/
private static class GeoSpatialServiceCall implements BigdataServiceCall {
private final IServiceOptions serviceOptions;
final GeoSpatialServiceCallConfiguration gssConfig;
private IVariable<?>[] vars;
private final AbstractTripleStore kb;
private final GeoSpatialCounters geoSpatialCounters;
private final int numTasks;
private final int minDatapointsPerTask;
private final int threadLocalBufferCapacity;
private final int globalBufferChunkOfChunksCapacity;
private final BaseJoinStats stats;
/**
* The service used for executing subtasks (optional).
*
* @see #maxParallelChunks
*/
final private Executor executor;
public GeoSpatialServiceCall(final IVariable<?> searchVar,
final Map<URI, StatementPatternNode> sps,
final IServiceOptions serviceOptions,
final GeoSpatialDefaults dflts, final AbstractTripleStore kb,
final int maxParallel, final int numTasks,
final int minDatapointsPerTask,
final int threadLocalBufferCapacity,
final int globalBufferChunkOfChunksCapacity,
final BaseJoinStats stats) {
if (searchVar == null)
throw new IllegalArgumentException();
if (sps == null)
throw new IllegalArgumentException();
if (serviceOptions == null)
throw new IllegalArgumentException();
if (kb == null)
throw new IllegalArgumentException();
this.serviceOptions = serviceOptions;
this.gssConfig =
new GeoSpatialServiceCallConfiguration(
dflts, kb.getLexiconRelation().getLexiconConfiguration().getGeoSpatialConfig(),
searchVar, sps);
// for now: a single variable containing the result
this.vars = new IVariable[] { searchVar };
this.kb = kb;
geoSpatialCounters =
QueryEngineFactory.getInstance().getQueryController(
kb.getIndexManager()).getGeoSpatialCounters();
this.numTasks = numTasks;
this.minDatapointsPerTask = minDatapointsPerTask;
this.threadLocalBufferCapacity = threadLocalBufferCapacity;
this.globalBufferChunkOfChunksCapacity = globalBufferChunkOfChunksCapacity;
if (DEBUG) {
log.debug("Number of threads used for execution: " + maxParallel);
}
// set up executor service (not using any if no parallel access desired)
executor = maxParallel<=1 ?
null : new LatchedExecutor(kb.getIndexManager().getExecutorService(), maxParallel);
this.stats = stats;
}
@Override
public ICloseableIterator<IBindingSet> call(final IBindingSet[] incomingBs) {
// iterate over the incoming binding set, issuing search requests
// for all bindings in the binding set
return new GeoSpatialInputBindingsIterator(incomingBs, gssConfig, kb, this);
}
@Override
public IServiceOptions getServiceOptions() {
return serviceOptions;
}
/**
* The search function itself, implementing a search request for a single
* query.
*
* @param query
* the geospatial search query
* @param kb
* the triple store to issue search against
*
* @return an iterator over the search results
*/
public ICloseableIterator<IBindingSet> search(
final GeoSpatialQuery query, final AbstractTripleStore kb) {
final BOpContextBase context = new BOpContextBase(
QueryEngineFactory.getInstance().getQueryController(kb.getIndexManager()));
geoSpatialCounters.registerGeoSpatialSearchRequest();
final GlobalAnnotations globals = new GlobalAnnotations(kb
.getLexiconRelation().getNamespace(), kb.getSPORelation()
.getTimestamp());
final BigdataValueFactory vf = kb.getValueFactory();
final BlockingBuffer<IBindingSet[]> buffer =
new BlockingBuffer<IBindingSet[]>(globalBufferChunkOfChunksCapacity);
final FutureTask<Void> ft =
new FutureTask<Void>(new GeoSpatialServiceCallTask(
buffer, query.normalize(), kb, vars, context, globals, vf, geoSpatialCounters,
executor, numTasks, minDatapointsPerTask, threadLocalBufferCapacity, stats));
buffer.setFuture(ft); // set the future on the buffer
kb.getIndexManager().getExecutorService().submit(ft);
return new ChunkConsumerIterator<IBindingSet>(buffer.iterator());
}
private static Var<?> varFromIVar(IVariable<?> iVar) {
return iVar==null ? null : Var.var(iVar.getName());
}
/**
* A single serice call request, which may handle a list of
* {@link GeoSpatialQuery}s.
*
* @author msc
*/
private static class GeoSpatialServiceCallTask
extends Haltable<Void> implements Callable<Void> {
final BlockingBuffer<IBindingSet[]> buffer;
final private Executor executor;
final private List<IGeoSpatialQuery> queries;
final private AbstractTripleStore kb;
final private IVariable<?>[] vars;
final private GeoSpatialCounters geoSpatialCounters;
final private BOpContextBase context;
final private GlobalAnnotations globals;
final private BigdataValueFactory vf;
final private GeoSpatialConfig geoSpatialConfig;
private final int numTasks;
private final int minDatapointsPerTask;
private final int threadLocalBufferCapacity;
private final BaseJoinStats stats;
/**
* The list of tasks to execute. Execution of these tasks is carried out
* in parallel if an executor with parallel execution configuration turned
* on is provided. Otherwise, the tasks are processed one by one
*/
final private List<GeoSpatialServiceCallSubRangeTask> tasks;
/**
* Constructor creating a {@link GeoSpatialServiceCallTask}. Expects
* a list of normalized queries as input, see {@link IGeoSpatialQuery#isNormalized()}.
*/
public GeoSpatialServiceCallTask(
final BlockingBuffer<IBindingSet[]> buffer,
final List<IGeoSpatialQuery> queries,
final AbstractTripleStore kb, final IVariable<?>[] vars,
final BOpContextBase context,
final GlobalAnnotations globals, final BigdataValueFactory vf,
final GeoSpatialCounters geoSpatialCounters, final Executor executor,
final int numTasks, final int minDatapointsPerTask,
final int threadLocalBufferCapacity, final BaseJoinStats stats) {
this.buffer = buffer;
this.queries = queries;
this.kb = kb;
this.vars = vars;
this.context = context;
this.globals = globals;
this.vf = vf;
this.executor = executor;
this.geoSpatialCounters = geoSpatialCounters;
this.numTasks = numTasks;
this.minDatapointsPerTask = minDatapointsPerTask;
this.threadLocalBufferCapacity = threadLocalBufferCapacity;
this.stats = stats;
this.geoSpatialConfig =
kb.getLexiconRelation().getLexiconConfiguration().getGeoSpatialConfig();
tasks = getSubTasks();
// set stats
geoSpatialCounters.registerGeoSpatialServiceCallTask();
geoSpatialCounters.registerGeoSpatialServiceCallSubRangeTasks(tasks.size());
}
/**
* Decomposes the context path into subtasks according to the configuration.
* Each subtasks is a range scan backed by the buffer.
*/
protected List<GeoSpatialServiceCallSubRangeTask> getSubTasks() {
final List<GeoSpatialServiceCallSubRangeTask> subTasks =
new LinkedList<GeoSpatialServiceCallSubRangeTask>();
for (IGeoSpatialQuery query : queries) {
// queries must be normalized at this point
if (!query.isNormalized()) {
throw new IllegalArgumentException("Expected list of normalized query as input.");
}
/**
* Get the bounding boxes for the subsequent scan
*/
final Object[] southWestComponents = query.getLowerAndUpperBound().getLowerBound();
final Object[] northEastComponents = query.getLowerAndUpperBound().getUpperBound();
final AccessPath<ISPO> accessPath =
getAccessPath(southWestComponents, northEastComponents, query);
if (accessPath==null) // known unsatisfiable, e.g. if predicate unknown
continue;
// estimate the total number of points in the search range
final long totalPointsInRange = accessPath.rangeCount(false/* exact */);
stats.accessPathRangeCount.add(totalPointsInRange);
// set up datatype configuration for the datatype URI
final GeoSpatialDatatypeConfiguration datatypeConfig =
geoSpatialConfig.getConfigurationForDatatype(query.getSearchDatatype());
if (datatypeConfig==null) {
throw new GeoSpatialSearchException(
GeoSpatialSearchException.INVALID_PARAMETER_EXCEPTION + ": search datatype unknown.");
}
final GeoSpatialLiteralExtension<BigdataValue> litExt =
new GeoSpatialLiteralExtension<BigdataValue>(kb.getLexiconRelation(), datatypeConfig);
// this debug code (currently broken) might be re-enabled once needed
// if (log.isDebugEnabled()) {
//
// final LiteralExtensionIV lowerBorderIV = litExt.createIV(southWestComponents);
// final LiteralExtensionIV upperBorderIV = litExt.createIV(northEastComponents);
// log.debug("[OuterRange] Scanning from " + lowerBorderIV.getDelegate().integerValue()
// + " / " + litExt.toComponentString(0, 2, lowerBorderIV));
// log.debug("[OuterRange] to " + upperBorderIV.getDelegate().integerValue()
// + " / " + litExt.toComponentString(0, 2, upperBorderIV));
//
// }
// set up tasks for partitions
final SPOKeyOrder keyOrder = (SPOKeyOrder)accessPath.getKeyOrder(); // will be the same for all partitions
final int subjectPos = keyOrder.getPositionInIndex(SPOKeyOrder.S);
final int objectPos = keyOrder.getPositionInIndex(SPOKeyOrder.O);
// set up the search range and a partitioner
final GeoSpatialSearchRange searchRange =
new GeoSpatialSearchRange(datatypeConfig, litExt, southWestComponents, northEastComponents);
final GeoSpatialSearchRangePartitioner partitioner = new GeoSpatialSearchRangePartitioner(searchRange);
for (GeoSpatialSearchRange partition : partitioner.partition(numTasks, totalPointsInRange, minDatapointsPerTask)) {
final Object[] lowerBorder = partition.getLowerBorderComponents();
final Object[] upperBorder = partition.getUpperBorderComponents();
// set up a subtask for the partition
final GeoSpatialServiceCallSubRangeTask subTask =
getSubTask(query, lowerBorder, upperBorder, keyOrder, subjectPos, objectPos, stats);
if (subTask!=null) { // if satisfiable
subTasks.add(subTask);
}
// note: this is old debugging code, which is broken
// -> might be fixed once we need debugging here...
// if (log.isDebugEnabled()) {
//
// LiteralExtensionIV lowerBorderIVPart = litExt.createIV(lowerBorder);
// LiteralExtensionIV upperBorderIVPart = litExt.createIV(upperBorder);
//
// log.debug("[InnerRange] Scanning from " + lowerBorderIVPart.getDelegate().integerValue()
// + " / " + litExt.toComponentString(0, 2, lowerBorderIVPart)
// + " / " + BytesUtil.byteArrToBinaryStr(litExt.toZOrderByteArray(lowerBorder)));
// log.debug("[InnerRange] to " + upperBorderIVPart.getDelegate().integerValue()
// + " / " + litExt.toComponentString(0, 2, upperBorderIVPart)
// + " / " + BytesUtil.byteArrToBinaryStr(litExt.toZOrderByteArray(upperBorder)));
// }
}
}
return subTasks;
}
/**
* Sets up a subtask for the given configuration. The method may return null
* if it can be statically shown that the subtask produces no result, i.e. if
* it is trivially not satisfiable. It gets as input information about the outer
* range (i.e., the search rectangle surrounding *all* subtasks) and the
* range for the given subtask, plus some additional information about key order,
* subject, and object position.
*
* @param outerRangeUpperLeftWithTime the upper left geospatial+time point of the outer range
* @param outerRangeLowerRightWithTime the lower right geospatial+time point of the outer range
* @param subRangeUpperLeftWithTime the upper left geospatial+time point of sub range covered by the task
* @param subRangeLowerRightWithTime the lower right geospatial+time point of sub range covered by the task
* @param keyOrder the key order of the underlying access path
* @param subjectPos the position of the subject in the key
* @param objectPos the position of the object in the key
*
* @return the subtask or null
*/
protected GeoSpatialServiceCallSubRangeTask getSubTask(
final IGeoSpatialQuery query,
final Object[] lowerBorder, final Object[] upperBorder,
final SPOKeyOrder keyOrder, final int subjectPos,
final int objectPos, final BaseJoinStats stats) {
/**
* Compose the surrounding filter. The filter is based on the outer range.
*/
final GeoSpatialFilterBase filter;
// set up datatype configuration for the datatype URI
final GeoSpatialDatatypeConfiguration datatypeConfig = query.getDatatypeConfig();
final GeoSpatialLiteralExtension<BigdataValue> litExt =
new GeoSpatialLiteralExtension<BigdataValue>(kb.getLexiconRelation(), datatypeConfig);
switch (query.getSearchFunction()) {
case IN_CIRCLE:
{
// for circle queries, the filter retains those values that are indeed in the
// circle (the z-order borders we're using for scanning report values that are
// not inside this circle)
filter = new GeoSpatialInCircleFilter(
query.getSpatialCircleCenter(), query.getSpatialCircleRadius(),
query.getSpatialUnit(), query.getTimeStart(), query.getTimeEnd(),
new GeoSpatialLiteralExtension<BigdataValue>(kb.getLexiconRelation(), datatypeConfig), geoSpatialCounters);
}
break;
case IN_RECTANGLE:
case UNDEFINED:
{
// for a rectangle query, the z-order lower and upper border exactly coincide
// with what we're looking for, so we set up a dummy filter for that case;
// we also use such a dummy filter if the geospatial search function is undefined
// (which might be the case if we query an index without latitude and longitude)
filter = new AcceptAllSolutionsFilter(
new GeoSpatialLiteralExtension<BigdataValue>(kb.getLexiconRelation(), datatypeConfig), geoSpatialCounters);
}
break;
default:
throw new RuntimeException("Unknown geospatial search function.");
}
filter.setObjectPos(objectPos); // position of the object in the index
/**
* If the context is provided, we would need a key order such as
* PCOS or CPOS, but unfortunately these key order are not available.
* As a "workaround", we do not pass in the context into the predicate,
* but instead set an additional context check in the filter.
*
* This is, of course, not a good strategy when the context is very
* selective and the index access is not. We could do more sophisticated
* stuff here, but for now let's try with this solution.
*/
final TermNode ctxTermNode = query.getContext();
if (ctxTermNode!=null) {
final BigdataValue ctx =
ctxTermNode==null ? null : ctxTermNode.getValue();
if (ctx!=null && !(ctx instanceof BigdataURI)) {
throw new IllegalArgumentException(
"Context in GeoSpatial search must be a URI");
}
// register context check in the filter
filter.addContextCheck(
keyOrder.getPositionInIndex(SPOKeyOrder.C), (BigdataURI)ctx);
}
// get the access path for the sub range
final AccessPath<ISPO> accessPath = getAccessPath(lowerBorder, upperBorder, query);
if (accessPath==null) {
return null;
}
// set up a big min advancer for efficient extraction of relevant values from access path
final byte[] lowerZOrderKey = litExt.toZOrderByteArray(lowerBorder);
final byte[] upperZOrderKey = litExt.toZOrderByteArray(upperBorder);
final Advancer<SPO> bigMinAdvancer =
new ZOrderIndexBigMinAdvancer(
lowerZOrderKey, upperZOrderKey, litExt, objectPos, geoSpatialCounters);
// set up a value resolver
final Var<?> locationVar = varFromIVar(query.getLocationVar());
final Var<?> timeVar = varFromIVar(query.getTimeVar());
final Var<?> latVar = varFromIVar(query.getLatVar());
final Var<?> lonVar = varFromIVar(query.getLonVar());
final Var<?> coordSystemVar = varFromIVar(query.getCoordSystemVar());
final Var<?> customFieldsVar = varFromIVar(query.getCustomFieldsVar());
final Var<?> locationAndTimeVar = varFromIVar(query.getLocationAndTimeVar());
final Var<?> literalVar = varFromIVar(query.getLiteralVar());
final Var<?> distanceVar = varFromIVar(query.getDistanceVar());
final Var<?> var = Var.var(vars[0].getName());
final IBindingSet incomingBindingSet = query.getIncomingBindings();
// calculate center point, if any
final PointLatLon centerPoint = query.getSpatialCircleCenter();
final CoordinateDD centerPointDD =
centerPoint==null ? null : new CoordinateDD(centerPoint.getLat(), centerPoint.getLon());
final GeoSpatialServiceCallResolver resolver =
new GeoSpatialServiceCallResolver(var, incomingBindingSet, locationVar,
timeVar, locationAndTimeVar, latVar, lonVar, coordSystemVar,
customFieldsVar, literalVar, distanceVar, subjectPos, objectPos, vf,
new GeoSpatialLiteralExtension<BigdataValue>(kb.getLexiconRelation(), datatypeConfig),
query.getCustomFieldsConstraints().keySet(), centerPointDD, query.getSpatialUnit());
// and construct the sub range task
return new GeoSpatialServiceCallSubRangeTask(
buffer, accessPath, bigMinAdvancer, filter, resolver, threadLocalBufferCapacity, stats);
}
@SuppressWarnings({ "unchecked", "rawtypes" })
protected AccessPath<ISPO> getAccessPath(
final Object[] lowerBorderComponents, final Object[] upperBorderComponents,
final IGeoSpatialQuery query) {
// set up datatype configuration and literal extension object for the datatype URI
final GeoSpatialDatatypeConfiguration datatypeConfig = query.getDatatypeConfig();
final GeoSpatialLiteralExtension litExt =
new GeoSpatialLiteralExtension<BigdataValue>(kb.getLexiconRelation(), datatypeConfig);
// set up range scan
final Var oVar = Var.var(); // object position variable
final RangeNode range = new RangeNode(new VarNode(oVar),
new ConstantNode(litExt.createIV(lowerBorderComponents)),
new ConstantNode(litExt.createIV(upperBorderComponents)));
final RangeBOp rangeBop = ASTRangeOptimizer.toRangeBOp(context, range, globals);
// we support projection of fixed subjects into the SERVICE call
final IConstant<?> constSubject = query.getSubject();
// set up the predicate
final TermNode s = constSubject==null ?
new VarNode(vars[0].getName()) : new ConstantNode((IConstant<IV>)constSubject);
final TermNode p = query.getPredicate();
final VarNode o = new VarNode(oVar);
/**
* We call kb.getPredicate(), which has the nice feature that it
* returns null if the predicate is unsatisfiable (i.e., if the
* predicate does not appear in the data). This gives us an early
* exit point for the service (see null check below).
*/
IPredicate<ISPO> pred = (IPredicate<ISPO>) kb.getPredicate(
(URI) s.getValue(), /* subject */
p==null ? null : (URI)p.getValue(), /* predicate */
o.getValue(), /* object */
null, /* context */
null, /* filter */
rangeBop); /* rangeBop */
if (pred == null) {
return null;
}
pred = (IPredicate<ISPO>) pred.setProperty(
IPredicate.Annotations.TIMESTAMP, kb.getSPORelation().getTimestamp());
final IRelation<ISPO> relation = context.getRelation(pred);
final AccessPath<ISPO> accessPath = (AccessPath<ISPO>) context
.getAccessPath(relation, pred);
return accessPath;
}
@Override
public Void call() throws Exception {
if (DEBUG) {
log.debug("Number of service call tasks to execute: " + tasks.size());
}
// if there's no executor specified or only one subtask, we run the task in process
if (executor == null || tasks.size()==1) {
/*
* No Executor, so run each task in the caller's thread.
*/
for (GeoSpatialServiceCallSubRangeTask task : tasks) {
task.call();
}
buffer.flush();
buffer.close();
return null;
}
/*
* Build list of FutureTasks. This list is used to check all
* tasks for errors and ensure that any running tasks are
* cancelled.
*/
final List<FutureTask<Void>> futureTasks = new LinkedList<FutureTask<Void>>();
for (GeoSpatialServiceCallSubRangeTask task : tasks) {
final FutureTask<Void> ft = new FutureTask<Void>(task);
futureTasks.add(ft);
}
try {
/*
* Execute all tasks.
*/
for (FutureTask<Void> ft : futureTasks) {
halted();
// Queue for execution.
executor.execute(ft);
} // next task.
/*
* Wait for each task. If any task throws an exception, then
* [halt] will become true and any running tasks will error
* out quickly. Once [halt := true], we do not wait for any
* more tasks, but proceed to cancel all tasks in the
* finally {} clause below.
*/
for (FutureTask<Void> ft : futureTasks) {
// Wait for a task.
if (!isDone())
ft.get();
}
} finally {
/*
* Ensure that all tasks are cancelled, regardless of
* whether they were started or have already finished.
*/
for (FutureTask<Void> ft : futureTasks) {
ft.cancel(true/* mayInterruptIfRunning */);
}
}
buffer.flush();
buffer.close();
return null;
}
private static class GeoSpatialServiceCallSubRangeTask implements Callable<Void> {
final private UnsynchronizedArrayBuffer<IBindingSet> localBuffer;
final private AccessPath<ISPO> accessPath;
final private Advancer<SPO> bigMinAdvancer;
final private GeoSpatialFilterBase filter;
final private GeoSpatialServiceCallResolver resolver;
final private BaseJoinStats stats;
public GeoSpatialServiceCallSubRangeTask(
final BlockingBuffer<IBindingSet[]> backingBuffer,
final AccessPath<ISPO> accessPath, Advancer<SPO> bigMinAdvancer,
final GeoSpatialFilterBase filter, final GeoSpatialServiceCallResolver resolver,
final int threadLocalBufferCapacity, final BaseJoinStats stats) {
// create a local buffer linked to the backing, thread safe blocking buffer
localBuffer =
new UnsynchronizedArrayBuffer<IBindingSet>(
backingBuffer, IBindingSet.class, threadLocalBufferCapacity);
this.accessPath = accessPath;
this.bigMinAdvancer = bigMinAdvancer;
this.filter = filter;
this.resolver = resolver;
this.stats = stats;
}
@SuppressWarnings("unchecked")
@Override
public Void call() throws Exception {
final Iterator<IBindingSet> itr =
new Striterator(accessPath.getIndex().rangeIterator(
accessPath.getFromKey(), accessPath.getToKey(), 0/* capacity */,
IRangeQuery.KEYS | IRangeQuery.CURSOR, bigMinAdvancer))
.addFilter(filter)
.addFilter(resolver);
// consume and flush the buffer
stats.accessPathCount.increment();
while (itr.hasNext()) {
stats.accessPathUnitsIn.increment();
localBuffer.add(itr.next());
}
localBuffer.flush();
return null;
}
}
/**
* Builds a number of partitions over a given geospatial search range, effectively
* splitting up the search range into fully covering smaller search ranges.
*/
public static class GeoSpatialSearchRangePartitioner {
private final GeoSpatialSearchRange geoSpatialSearchRange;
public GeoSpatialSearchRangePartitioner(final GeoSpatialSearchRange geoSpatialSearchRange) {
this.geoSpatialSearchRange = geoSpatialSearchRange;
}
/**
* Computes the partitions based on the configuration. For now, we always partition along the
* last dimension, e.g. for a ternary datatype such as LAT+LON+TIME we would partition on TIME.
*
* The number of partitions is computed by computeNumberOfPartitions() with the given parameters,
* see the documentation of the latter method for a detailed explanation.
*
* @param numTasks desired number of access path tasks generated per thread in case the range
* is large enough to be split, see {@link PipelineJoin.Annotations.NUM_TASKS_PER_THREAD}
* @param minDatapointsPerTask the minimum number of data points assigned to a a given task, which
* essentially should be the threshold on which parallelization starts
* to pay out, see {@link PipelineJoin.Annotations.MIN_DATAPOINTS_PER_TASK}
* @param totalPointsInRange the estimated number of total points in the range.
*
* @return the partitions, fully and exactly covering this search range
*
* TODO: what's the rational for choosing the last component? wouldn't the one with the most significant
* bit make more sense? would need some experimental validation to figure that out...
* TODO: the bit shift/precision logics might be encapsulated in methods in the geospatial literal; it
* unnecessarily blows up this method here and doesn't really belong here...
*/
public List<GeoSpatialSearchRange> partition(int numTasks, long totalPointsInRange, int minDatapointsPerTask) {
final List<GeoSpatialSearchRange> partitions = new ArrayList<GeoSpatialSearchRange>();
final long numPartitions = computeNumberOfPartitions(numTasks, totalPointsInRange, minDatapointsPerTask);
final GeoSpatialDatatypeConfiguration datatypeConfig = geoSpatialSearchRange.getDatatypeConfig();
final int numDimensions = datatypeConfig.getNumDimensions();
final Object[] lowerBorderComponents = geoSpatialSearchRange.getLowerBorderComponents();
final Object[] upperBorderComponents = geoSpatialSearchRange.getUpperBorderComponents();
assert (numDimensions == lowerBorderComponents.length &&
numDimensions == upperBorderComponents.length);
// the component we partition on is the last component in multi-dimensional index
final GeoSpatialDatatypeFieldConfiguration fieldToPartitionOn =
datatypeConfig.getFields().get(numDimensions-1);
final ValueType vt = fieldToPartitionOn.getValueType();
// initialize the value value representing the component on which we split
// -> note that we convert to long, applying the precision adjustment
final long lowerComponentLongValue;
final long upperComponentLongValue;
switch (vt) {
case LONG:
{
lowerComponentLongValue = (Long)lowerBorderComponents[numDimensions-1];
upperComponentLongValue = (Long)upperBorderComponents[numDimensions-1];
break;
}
case DOUBLE:
{
lowerComponentLongValue =
(Double.valueOf((Double)lowerBorderComponents[numDimensions-1]*fieldToPartitionOn.getMultiplier())).longValue();
upperComponentLongValue =
(Double.valueOf((Double)upperBorderComponents[numDimensions-1]*fieldToPartitionOn.getMultiplier())).longValue();
break;
}
default:
throw new RuntimeException("Unsupported value type: " + vt);
}
final long diff = upperComponentLongValue - lowerComponentLongValue;
final long dist = diff / numPartitions;
final List<Long> breakPoints = new ArrayList<Long>();
/**
* Compute the break points
*/
long lastConsidered = -1;
breakPoints.add(lowerComponentLongValue -1); // one value on the left -> will add +1 below
// points in-between (ignoring first and last)
for (long i = 1; i < numPartitions; i++) {
long breakPoint = lowerComponentLongValue + i * dist;
if (lastConsidered == breakPoint) {
break;
}
if (breakPoint > lowerComponentLongValue && breakPoint < upperComponentLongValue) {
breakPoints.add(breakPoint);
}
lastConsidered = breakPoint;
}
breakPoints.add(upperComponentLongValue); // last point
/**
* Setup partitions for the given breakpoints
*/
final int finalPosition = lowerBorderComponents.length-1;
for (int i = 0; i < breakPoints.size() - 1; i++) {
final Object[] breakpointLowerBorder = new Object[lowerBorderComponents.length];
final Object[] breakpointUpperBorder = new Object[upperBorderComponents.length];
for (int j=0; j<lowerBorderComponents.length-1; j++) {
breakpointLowerBorder[j] = lowerBorderComponents[j];
breakpointUpperBorder[j] = upperBorderComponents[j];
}
final long startLongVal = breakPoints.get(i) + 1;
final long endLongVal = breakPoints.get(i + 1);
switch (vt) {
case LONG:
{
// just copy over value specified by breakpoint
breakpointLowerBorder[finalPosition] = startLongVal;
breakpointUpperBorder[finalPosition] = endLongVal;
break;
}
case DOUBLE:
{
// apply precision adjustment
breakpointLowerBorder[finalPosition] =
(double)startLongVal/(double)fieldToPartitionOn.getMultiplier();
breakpointUpperBorder[finalPosition] =
(double)endLongVal/(double)fieldToPartitionOn.getMultiplier();
break;
}
default:
throw new RuntimeException("Unsupported value type: " + vt);
}
final GeoSpatialSearchRange searchRange =
new GeoSpatialSearchRange(
geoSpatialSearchRange.getDatatypeConfig(), geoSpatialSearchRange.getLitExt(),
breakpointLowerBorder, breakpointUpperBorder);
partitions.add(searchRange);
}
return partitions;
}
/**
* The number of partitions is calculated based on two parameters. First, there is a
* minumum number of datapoints per task, which is considered a hard limit. This means,
* we generate *at most* (totalPointsInRange/minDatapointsPerTask) tasks.
*
* The second parameter is the numTasks parameter, which tells us the "desired"
* number of tasks.
*
* The number of subranges is then defined as follows:
* - If the desired number of tasks is larger than the hard limit, we choose the hard limit
* - Otherwise, we choose the desired number of tasks
*
* Effectively, this means choosing the smaller of the two values (MIN), while
* making sure that the value returned is >= 1.
*
* @param numTasks desired number of access path tasks generated per thread in case the range
* is large enough to be split, see {@link PipelineJoin.Annotations.NUM_TASKS_PER_THREAD}
* @param minDatapointsPerTask the minimum number of data points assigned to a a given task, which
* essentially should be the threshold on which parallelization starts
* to pay out, see {@link PipelineJoin.Annotations.MIN_DATAPOINTS_PER_TASK}
* @param totalPointsInRange the estimated number of total points in the range.
*
* @return the resulting number of partitions
**/
private long computeNumberOfPartitions(int numTasks, long totalPointsInRange, int minDatapointsPerTask) {
final long maxTasksByDatapointRestriction = totalPointsInRange/minDatapointsPerTask;
final long desiredNumTasks = Math.max(numTasks, 1); /* at least one subrange */
long nrSubRanges = Math.min(maxTasksByDatapointRestriction, desiredNumTasks);
return Math.max(1, nrSubRanges); // at least one
}
}
public static class GeoSpatialSearchRange {
final GeoSpatialDatatypeConfiguration datatypeConfig;
final GeoSpatialLiteralExtension<BigdataValue> litExt;
private final Object[] lowerBorderComponents;
private final Object[] upperBorderComponents;
public GeoSpatialSearchRange(
final GeoSpatialDatatypeConfiguration datatypeConfig,
final GeoSpatialLiteralExtension<BigdataValue> litExt,
final Object[] lowerBorderComponents, final Object[] upperBorderComponents) {
this.datatypeConfig = datatypeConfig;
this.litExt = litExt;
this.lowerBorderComponents = lowerBorderComponents;
this.upperBorderComponents = upperBorderComponents;
}
public GeoSpatialLiteralExtension<BigdataValue> getLitExt() {
return litExt;
}
public GeoSpatialDatatypeConfiguration getDatatypeConfig() {
return datatypeConfig;
}
public Object[] getLowerBorderComponents() {
return lowerBorderComponents;
}
public Object[] getUpperBorderComponents() {
return upperBorderComponents;
}
}
}
private static class GeoSpatialServiceCallResolver extends Resolver {
private static final long serialVersionUID = 1L;
final private Var<?> var;
final private IBindingSet incomingBindingSet;
final private Var<?> locationVar;
final private Var<?> timeVar;
final private Var<?> locationAndTimeVar;
final private Var<?> latVar;
final private Var<?> lonVar;
final private Var<?> coordSystemVar;
final private Var<?> customFieldsVar;
final private Var<?> literalVar;
final private Var<?> distanceVar;
final private int subjectPos;
final private int objectPos;
final private int latIdx;
final private int lonIdx;
final private int timeIdx;
final private int coordSystemIdx;
final private int[] idxsOfCustomFields;
final private CoordinateDD centerPoint;
final private UNITS unit;
final private BigdataValueFactory vf;
final private GeoSpatialLiteralExtension<BigdataValue> litExt;
final private IGeoSpatialLiteralSerializer literalSerializer;
// true if the resolver needs to dereference the object, e.g. because
// the object's value or the distance to the object is reported back
final boolean requiresObjectDereferencing;
// the position up to which we need to extract IVs
final private int extractToPosition;
public GeoSpatialServiceCallResolver(final Var<?> var, final IBindingSet incomingBindingSet,
final Var<?> locationVar, final Var<?> timeVar, final Var<?> locationAndTimeVar,
final Var<?> latVar, final Var<?> lonVar, final Var<?> coordSystemVar,
final Var<?> customFieldsVar, final Var<?> literalVar, final Var<?> distanceVar,
final int subjectPos, final int objectPos, final BigdataValueFactory vf,
final GeoSpatialLiteralExtension<BigdataValue> litExt, final Set<String> customFieldStrings,
final CoordinateDD centerPoint, final UNITS unit) {
this.var = var;
this.incomingBindingSet = incomingBindingSet;
this.locationVar = locationVar;
this.timeVar = timeVar;
this.locationAndTimeVar = locationAndTimeVar;
this.latVar = latVar;
this.lonVar = lonVar;
this.coordSystemVar = coordSystemVar;
this.customFieldsVar = customFieldsVar;
this.literalVar = literalVar;
this.distanceVar = distanceVar;
this.subjectPos = subjectPos;
this.objectPos = objectPos;
this.vf = vf;
this.litExt = litExt;
this.centerPoint = centerPoint;
this.unit = unit;
literalSerializer = litExt.getDatatypeConfig().getLiteralSerializer();
requiresObjectDereferencing =
locationVar!=null || timeVar!=null || locationAndTimeVar!=null ||coordSystemVar!=null
||customFieldsVar!=null || latVar!=null || lonVar!=null || literalVar!=null || distanceVar!=null;
extractToPosition =
requiresObjectDereferencing ?
Math.max(objectPos, subjectPos) + 1:
subjectPos + 1;
final GeoSpatialDatatypeConfiguration datatypeConfig = litExt.getDatatypeConfig();
latIdx = datatypeConfig.idxOfField(ServiceMapping.LATITUDE);
lonIdx = datatypeConfig.idxOfField(ServiceMapping.LONGITUDE);
timeIdx = datatypeConfig.idxOfField(ServiceMapping.TIME);
coordSystemIdx = datatypeConfig.idxOfField(ServiceMapping.COORD_SYSTEM);
idxsOfCustomFields = datatypeConfig.idxsOfCustomFields(customFieldStrings);
}
/**
* Resolve tuple to IV.
*/
@SuppressWarnings("rawtypes")
@Override
protected IBindingSet resolve(final Object obj) {
final byte[] key = ((ITuple<?>) obj).getKey();
// if results are reported, we need to decode up to subject + object,
// otherwise decoding up to the subject position is sufficient
final IV[] ivs = IVUtility.decode(key, extractToPosition);
final IBindingSet bs = incomingBindingSet.clone();
bs.set(var, new Constant<IV>(ivs[subjectPos]));
// handle request for binding index components
if (requiresObjectDereferencing) {
final Object[] componentArr =
litExt.toComponentArray((LiteralExtensionIV)ivs[objectPos]);
if (locationVar!=null) {
bs.set(locationVar,
new Constant<IV>(
literalSerializer.serializeLocation(
vf, componentArr[latIdx], componentArr[lonIdx])));
}
if (locationAndTimeVar!=null) {
bs.set(locationAndTimeVar,
new Constant<IV>(
literalSerializer.serializeLocationAndTime(
vf,
componentArr[latIdx],
componentArr[lonIdx],
componentArr[timeIdx])));
}
if (timeVar!=null) {
bs.set(timeVar,
new Constant<IV>(
literalSerializer.serializeTime(vf, componentArr[timeIdx])));
}
if (latVar!=null) {
bs.set(latVar,
new Constant<IV>(
literalSerializer.serializeLatitude(vf, componentArr[latIdx])));
}
if (lonVar!=null) {
bs.set(lonVar,
new Constant<IV>(
literalSerializer.serializeLongitude(vf, componentArr[lonIdx])));
}
if (coordSystemVar!=null) {
bs.set(coordSystemVar,
new Constant<IV>(literalSerializer.serializeCoordSystem(vf, componentArr[coordSystemIdx])));
}
if (customFieldsVar!=null) {
final Object[] customFieldsValues = new Object[idxsOfCustomFields.length];
for (int i=0; i<idxsOfCustomFields.length; i++) {
customFieldsValues[i] = componentArr[idxsOfCustomFields[i]];
}
bs.set(customFieldsVar,
new Constant<IV>(literalSerializer.serializeCustomFields(vf, customFieldsValues)));
}
if (literalVar!=null) {
bs.set(literalVar,
new Constant<IV>(
DummyConstantNode.toDummyIV(
vf.createLiteral(literalSerializer.fromComponents(componentArr),
litExt.getDatatypeConfig().getUri()))));
}
if (distanceVar!=null) {
// set up coordinate for the given point and calculate distance
final Double curLatValue = componentArr[latIdx] instanceof Double ?
(Double)componentArr[latIdx] : ((Long)componentArr[latIdx]).doubleValue();
final Double curLonValue = componentArr[lonIdx] instanceof Double ?
(Double)componentArr[lonIdx] : ((Long)componentArr[lonIdx]).doubleValue();
final CoordinateDD cur = new CoordinateDD(curLatValue, curLonValue);
bs.set(distanceVar,
new Constant<IV>(
literalSerializer.serializeDistance(vf, centerPoint.distance(cur, unit), unit)));
}
}
return bs;
}
}
}
/**
* Iterates a geospatial search over a set of input bindings. This is done
* incrementally, in a binding by binding fashion.
*/
public static class GeoSpatialInputBindingsIterator implements
ICloseableIterator<IBindingSet> {
private final IBindingSet[] bindingSet;
private final GeoSpatialServiceCallConfiguration gssConfig;
final AbstractTripleStore kb;
final GeoSpatialServiceCall serviceCall;
int nextBindingSetItr = 0;
ICloseableIterator<IBindingSet> curDelegate;
public GeoSpatialInputBindingsIterator(final IBindingSet[] bindingSet,
final GeoSpatialServiceCallConfiguration gssConfig,
final AbstractTripleStore kb, final GeoSpatialServiceCall serviceCall) {
this.bindingSet = bindingSet;
this.gssConfig = gssConfig;
this.kb = kb;
this.serviceCall = serviceCall;
init();
}
/**
* Checks whether there are more results available.
*
* @return
*/
public boolean hasNext() {
/*
* Delegate will be set to null once all binding sets have been
* processed
*/
if (curDelegate == null) {
return false;
}
/*
* If there is a delegate set, ask him if there are results
*/
if (curDelegate.hasNext()) {
return true;
} else {
// if not, we set the next delegate
if (nextDelegate()) { // in case there is one ...
return hasNext(); // go into recursion
}
}
return false; // fallback
}
public IBindingSet next() {
if (curDelegate == null) {
return null; // no more results
}
if (curDelegate.hasNext()) {
return curDelegate.next();
} else {
if (nextDelegate()) {
return next();
}
}
return null; // reached the end
}
/**
* @return true if a new delegate has been set successfully, false
* otherwise
*/
private boolean nextDelegate() {
if (bindingSet == null || nextBindingSetItr >= bindingSet.length) {
curDelegate = null;
return false;
}
final IBindingSet bs = bindingSet[nextBindingSetItr++];
final GeoSpatialQuery sq = gssConfig.toGeoSpatialQuery(bs);
curDelegate = serviceCall.search(sq, kb);
return true;
}
private void init() {
nextDelegate();
}
@Override
public void remove() {
if (curDelegate != null) {
curDelegate.remove();
}
}
@Override
public void close() {
if (curDelegate != null) {
curDelegate.close();
}
}
}
/**
* Default values for geospatial service, such as unit definitions.
*
* @author msc
*/
public static class GeoSpatialDefaults {
private final String defaultFunction;
private final String defaultSpatialUnit;
public GeoSpatialDefaults(final Properties p) {
this.defaultFunction = p.getProperty(GeoSpatial.Options.GEO_FUNCTION);
this.defaultSpatialUnit = p
.getProperty(GeoSpatial.Options.GEO_SPATIAL_UNIT);
}
public String getDefaultFunction() {
return defaultFunction;
}
public String getDefaultSpatialDistanceUnit() {
return defaultSpatialUnit;
}
}
@SuppressWarnings("rawtypes")
public abstract static class GeoSpatialFilterBase extends TupleFilter {
private static final long serialVersionUID = 2271038531634362860L;
protected final GeoSpatialLiteralExtension<BigdataValue> litExt;
protected final GeoSpatialDatatypeConfiguration datatypeConfig;
// position of the subject in the tuples
protected int objectPos = -1;
// position of the constrained context in the tuple (only used in
// quads mode if geo:context predicate is used)
Integer contextPos = null;
// value of the constrained context (only used in quads mode if
// geo:context predicate is used)
BigdataURI context = null;
// counters objects
GeoSpatialCounters geoSpatialCounters;
public GeoSpatialFilterBase(
final GeoSpatialLiteralExtension<BigdataValue> litExt,
final GeoSpatialCounters geoSpatialCounters) {
this.litExt = litExt;
this.datatypeConfig = litExt.getDatatypeConfig();
this.geoSpatialCounters = geoSpatialCounters;
}
// sets member variables that imply an additional context check
public void addContextCheck(
final Integer contextPos, final BigdataURI context) {
this.contextPos = contextPos;
this.context = context;
}
public void setObjectPos(int objectPos) {
this.objectPos = objectPos;
}
/**
* Return the {@link GeoSpatialLiteralExtension} object associated
* with this filter.
*
* @return the object
*/
public GeoSpatialLiteralExtension<BigdataValue> getGeoSpatialLiteralExtension() {
return litExt;
}
@Override
final protected boolean isValid(final ITuple tuple) {
final long filterStartTime = System.nanoTime();
// check that the context constraint is satisfied, if any
if (!contextIsValid(tuple)) {
return false;
}
// delegate to subclass
boolean isValid = isValidInternal(tuple);
final long filterEndTime = System.nanoTime();
geoSpatialCounters.addFilterCalculationTime(filterEndTime-filterStartTime);
return isValid;
}
/**
* Check if the context is valid. If no context constraint is
* imposed (which means, if contextPos or context are null),
* then the method just returns true.
*/
private boolean contextIsValid(final ITuple tuple) {
// do not filter if not both the contextPos and context are set
if (contextPos==null || context==null) {
return true;
}
final byte[] key = ((ITuple<?>) tuple).getKey();
final IV[] ivs = IVUtility.decode(key, contextPos+1);
final IV contextIV= ivs[contextPos];
return contextIV.equals(context.getIV());
}
/**
* Check if the iv array is valid. To be implemented by subclasses.
*/
protected abstract boolean isValidInternal(final ITuple tuple);
}
/**
* Filter asserting that a given point lies into a specified square, defined
* by its lower and upper border, plus time frame.
*/
public static class GeoSpatialInCircleFilter extends GeoSpatialFilterBase {
private static final long serialVersionUID = -346928614528045113L;
final private double spatialPointLat;
final private double spatialPointLon;
final private Double distanceInMeters;
final private int idxOfLat;
final private int idxOfLon;
final boolean latLonIndicesValid;
public GeoSpatialInCircleFilter(
final PointLatLon spatialPoint, Double distance,
final UNITS unit, final Long timeMin, final Long timeMax,
final GeoSpatialLiteralExtension<BigdataValue> litExt,
final GeoSpatialCounters geoSpatialCounters) {
super(litExt, geoSpatialCounters);
this.spatialPointLat = spatialPoint.getLat();
this.spatialPointLon = spatialPoint.getLon();
this.distanceInMeters = CoordinateUtility.unitsToMeters(distance, unit);
this.idxOfLat = datatypeConfig.idxOfField(ServiceMapping.LATITUDE);
this.idxOfLon = datatypeConfig.idxOfField(ServiceMapping.LONGITUDE);
latLonIndicesValid = idxOfLat>=0 && idxOfLon>=0;
}
@Override
@SuppressWarnings("rawtypes")
protected boolean isValidInternal(final ITuple tuple) {
if (!latLonIndicesValid)
return false; // something's wrong here, reject all tuples
// Note: this is possibly called over and over again, so we choose a
// low-level implementation to get best performance out of it
try {
final byte[] key = ((ITuple<?>) tuple).getKey();
final IV[] ivs = IVUtility.decode(key, objectPos + 1);
final IV oIV = ivs[objectPos];
final double lat;
final double lon;
if (oIV instanceof LiteralExtensionIV) {
final LiteralExtensionIV lit = (LiteralExtensionIV) oIV;
long[] longArr = litExt.asLongArray(lit);
final Object[] components = litExt.longArrAsComponentArr(longArr);
lat = (double)components[idxOfLat];
lon = (double)components[idxOfLon];
} else {
throw new IllegalArgumentException("Invalid IV cannot be cast to LiteralExtensionIV");
}
return
CoordinateUtility.distanceInMeters(lat, spatialPointLat, lon, spatialPointLon) <= distanceInMeters;
} catch (Exception e) {
if (INFO) {
log.info(
"Something went wrong extracting the object: " + e.getMessage() +
"Rejecting unprocessable value.");
}
}
return false; // exception code path -> reject value
}
}
/**
* Dummy filter asserting that a point delivered by the advancer lies into
* a given rectangle. Given the current advancer implementation, there's
* nothing to do for the filter, since the advancer delivers exactly those
* points that are actually in this range.
*/
public static class AcceptAllSolutionsFilter extends GeoSpatialFilterBase {
private static final long serialVersionUID = -314581671447912352L;
public AcceptAllSolutionsFilter(
final GeoSpatialLiteralExtension<BigdataValue> litExt,
final GeoSpatialCounters geoSpatialCounters) {
super(litExt, geoSpatialCounters);
}
@SuppressWarnings("rawtypes")
@Override
protected boolean isValidInternal(final ITuple tuple) {
// the advancer by design already returns only those values that
// are in the rectangle, so there's nothing we need to do here
return true;
}
}
/**
* Returns the statement patterns contained in the service node.
*
* @param serviceNode
* @return
*/
Collection<StatementPatternNode> getStatementPatterns(final ServiceNode serviceNode) {
final List<StatementPatternNode> statementPatterns =
new ArrayList<StatementPatternNode>();
for (IGroupMemberNode child : serviceNode.getGraphPattern()) {
if (child instanceof StatementPatternNode) {
statementPatterns.add((StatementPatternNode)child);
} else {
throw new GeoSpatialSearchException("Nested groups are not allowed.");
}
}
return statementPatterns;
}
@Override
public Set<IVariable<?>> getRequiredBound(final ServiceNode serviceNode) {
/**
* This method extracts exactly those variables that are incoming,
* i.e. must be bound before executing the execution of the service.
*/
final Set<IVariable<?>> requiredBound = new HashSet<IVariable<?>>();
for (StatementPatternNode sp : getStatementPatterns(serviceNode)) {
final URI predicate = (URI) (sp.p()).getValue();
final IVariableOrConstant<?> object = sp.o().getValueExpression();
if (object instanceof IVariable<?>) {
if (predicate.equals(GeoSpatial.PREDICATE)
|| predicate.equals(GeoSpatial.SEARCH)
|| predicate.equals(GeoSpatial.SEARCH_DATATYPE)
|| predicate.equals(GeoSpatial.CONTEXT)
|| predicate.equals(GeoSpatial.SPATIAL_CIRCLE_CENTER)
|| predicate.equals(GeoSpatial.SPATIAL_CIRCLE_RADIUS)
|| predicate.equals(GeoSpatial.SPATIAL_RECTANGLE_NORTH_EAST)
|| predicate.equals(GeoSpatial.SPATIAL_RECTANGLE_SOUTH_WEST)
|| predicate.equals(GeoSpatial.SPATIAL_UNIT)
|| predicate.equals(GeoSpatial.TIME_START)
|| predicate.equals(GeoSpatial.TIME_END)
|| predicate.equals(GeoSpatial.COORD_SYSTEM)
|| predicate.equals(GeoSpatial.CUSTOM_FIELDS)
|| predicate.equals(GeoSpatial.CUSTOM_FIELDS_LOWER_BOUNDS)
|| predicate.equals(GeoSpatial.CUSTOM_FIELDS_UPPER_BOUNDS)) {
requiredBound.add((IVariable<?>)object); // the subject var is what we return
}
}
}
return requiredBound;
}
/**
* Wrapper class representing a geospatial service call configuration.
*
* @author msc
*/
public static class GeoSpatialServiceCallConfiguration {
private GeoSpatialDefaults defaults = null;
private GeoSpatialConfig geoSpatialConfig = null;
private TermNode searchFunction = null;
private TermNode predicate = null;
private TermNode searchDatatype = null;
private TermNode context = null;
private TermNode spatialCircleCenter = null;
private TermNode spatialCircleRadius = null;
private TermNode spatialRectangleSouthWest = null;
private TermNode spatialRectangleNorthEast = null;
private TermNode spatialUnit = null;
private TermNode timeStart = null;
private TermNode timeEnd = null;
private TermNode coordSystem = null;
private TermNode customFields = null;
private TermNode customFieldsLowerBounds = null;
private TermNode customFieldsUpperBounds = null;
private IVariable<?> searchVar = null;
private IVariable<?> locationVar = null;
private IVariable<?> locationAndTimeVar = null;
private IVariable<?> timeVar = null;
private IVariable<?> latVar = null;
private IVariable<?> lonVar = null;
private IVariable<?> coordSystemVar = null;
private IVariable<?> customFieldsVar = null;
private IVariable<?> literalVar = null;
private IVariable<?> distanceVar = null;
public GeoSpatialServiceCallConfiguration(
final GeoSpatialDefaults defaults, final GeoSpatialConfig geoSpatialConfig,
final IVariable<?> searchVar, final Map<URI, StatementPatternNode> sps) {
this.geoSpatialConfig = geoSpatialConfig;
this.defaults = defaults;
this.searchVar = searchVar;
if (sps.containsKey(GeoSpatial.SEARCH)) {
this.searchFunction = sps.get(GeoSpatial.SEARCH).o();
}
if (sps.containsKey(GeoSpatial.PREDICATE)) {
this.predicate = sps.get(GeoSpatial.PREDICATE).o();
}
if (sps.containsKey(GeoSpatial.SEARCH_DATATYPE)) {
this.searchDatatype = sps.get(GeoSpatial.SEARCH_DATATYPE).o();
}
if (sps.containsKey(GeoSpatial.CONTEXT)) {
this.context = sps.get(GeoSpatial.CONTEXT).o();
}
if (sps.containsKey(GeoSpatial.SPATIAL_CIRCLE_CENTER)) {
this.spatialCircleCenter = sps.get(GeoSpatial.SPATIAL_CIRCLE_CENTER).o();
}
if (sps.containsKey(GeoSpatial.SPATIAL_CIRCLE_RADIUS)) {
this.spatialCircleRadius = sps.get(GeoSpatial.SPATIAL_CIRCLE_RADIUS).o();
}
if (sps.containsKey(GeoSpatial.SPATIAL_RECTANGLE_SOUTH_WEST)) {
this.spatialRectangleSouthWest =
sps.get(GeoSpatial.SPATIAL_RECTANGLE_SOUTH_WEST).o();
}
if (sps.containsKey(GeoSpatial.SPATIAL_RECTANGLE_NORTH_EAST)) {
this.spatialRectangleNorthEast =
sps.get(GeoSpatial.SPATIAL_RECTANGLE_NORTH_EAST).o();
}
if (sps.containsKey(GeoSpatial.SPATIAL_UNIT)) {
this.spatialUnit = sps.get(GeoSpatial.SPATIAL_UNIT).o();
}
if (sps.containsKey(GeoSpatial.TIME_START)) {
this.timeStart = sps.get(GeoSpatial.TIME_START).o();
}
if (sps.containsKey(GeoSpatial.TIME_END)) {
this.timeEnd = sps.get(GeoSpatial.TIME_END).o();
}
if (sps.containsKey(GeoSpatial.COORD_SYSTEM)) {
this.coordSystem = sps.get(GeoSpatial.COORD_SYSTEM).o();
}
if (sps.containsKey(GeoSpatial.CUSTOM_FIELDS)) {
this.customFields = sps.get(GeoSpatial.CUSTOM_FIELDS).o();
}
if (sps.containsKey(GeoSpatial.CUSTOM_FIELDS_LOWER_BOUNDS)) {
this.customFieldsLowerBounds =
sps.get(GeoSpatial.CUSTOM_FIELDS_LOWER_BOUNDS).o();
}
if (sps.containsKey(GeoSpatial.CUSTOM_FIELDS_UPPER_BOUNDS)) {
this.customFieldsUpperBounds =
sps.get(GeoSpatial.CUSTOM_FIELDS_UPPER_BOUNDS).o();
}
if (sps.containsKey(GeoSpatial.LOCATION_VALUE)) {
final StatementPatternNode sp = sps.get(GeoSpatial.LOCATION_VALUE);
if (!sp.o().isVariable()) {
throw new GeoSpatialSearchException(
"locationValue property must point to a variable");
}
this.locationVar = (IVariable<?>) sp.o().getValueExpression();
}
if (sps.containsKey(GeoSpatial.TIME_VALUE)) {
final StatementPatternNode sp = sps.get(GeoSpatial.TIME_VALUE);
if (!sp.o().isVariable()) {
throw new GeoSpatialSearchException(
"timeValue property must point to a variable");
}
this.timeVar = (IVariable<?>) sp.o().getValueExpression();
}
if (sps.containsKey(GeoSpatial.LAT_VALUE)) {
final StatementPatternNode sp = sps.get(GeoSpatial.LAT_VALUE);
if (!sp.o().isVariable()) {
throw new GeoSpatialSearchException(
"latValue property must point to a variable");
}
this.latVar = (IVariable<?>) sp.o().getValueExpression();
}
if (sps.containsKey(GeoSpatial.LON_VALUE)) {
final StatementPatternNode sp = sps.get(GeoSpatial.LON_VALUE);
if (!sp.o().isVariable()) {
throw new GeoSpatialSearchException(
"lonValue property must point to a variable");
}
this.lonVar = (IVariable<?>) sp.o().getValueExpression();
}
if (sps.containsKey(GeoSpatial.COORD_SYSTEM_VALUE)) {
final StatementPatternNode sp = sps.get(GeoSpatial.COORD_SYSTEM_VALUE);
if (!sp.o().isVariable()) {
throw new GeoSpatialSearchException(
"coordSystemValue property must point to a variable");
}
this.coordSystemVar = (IVariable<?>) sp.o().getValueExpression();
}
if (sps.containsKey(GeoSpatial.CUSTOM_FIELDS_VALUES)) {
final StatementPatternNode sp = sps.get(GeoSpatial.CUSTOM_FIELDS_VALUES);
if (!sp.o().isVariable()) {
throw new GeoSpatialSearchException(
"customFieldsValues property must point to a variable");
}
this.customFieldsVar = (IVariable<?>) sp.o().getValueExpression();
}
if (sps.containsKey(GeoSpatial.LOCATION_AND_TIME_VALUE)) {
final StatementPatternNode sp = sps.get(GeoSpatial.LOCATION_AND_TIME_VALUE);
if (!sp.o().isVariable()) {
throw new GeoSpatialSearchException(
"locationAndTimeValue property must point to a variable");
}
this.locationAndTimeVar = (IVariable<?>) sp.o().getValueExpression();
}
if (sps.containsKey(GeoSpatial.LITERAL_VALUE)) {
final StatementPatternNode sp = sps.get(GeoSpatial.LITERAL_VALUE);
if (!sp.o().isVariable()) {
throw new GeoSpatialSearchException(
"locationAndTimeValue property must point to a variable");
}
this.literalVar = (IVariable<?>) sp.o().getValueExpression();
}
if (sps.containsKey(GeoSpatial.DISTANCE_VALUE)) {
final StatementPatternNode sp = sps.get(GeoSpatial.DISTANCE_VALUE);
if (!sp.o().isVariable()) {
throw new GeoSpatialSearchException(
"distanceValue property must point to a variable");
}
this.distanceVar = (IVariable<?>) sp.o().getValueExpression();
}
}
/**
* Converts the configuration into a query over the given binding set
* @param bs
*/
public GeoSpatialQuery toGeoSpatialQuery(final IBindingSet bs) {
final URI searchDatatypeUri = resolveSearchDatatype(
this.searchDatatype, bs);
final GeoFunction searchFunction = resolveAsGeoFunction(
this.searchFunction, bs);
final PointLatLon spatialCircleCenter = resolveAsPoint(
this.spatialCircleCenter, bs);
final Double spatialCircleRadius = resolveAsDouble(
this.spatialCircleRadius, bs);
final PointLatLon spatialRectangleUpperLeft = resolveAsPoint(
this.spatialRectangleSouthWest, bs);
final PointLatLon spatialRectangleLowerRight = resolveAsPoint(
this.spatialRectangleNorthEast, bs);
final UNITS spatialUnit = resolveAsSpatialDistanceUnit(
this.spatialUnit, bs);
final Long coordSystem = resolveAsLong(this.coordSystem, bs);
final Long timeStart = resolveAsLong(this.timeStart, bs);
final Long timeEnd = resolveAsLong(this.timeEnd, bs);
final String[] customFields = resolveAsStringArr(this.customFields, bs);
// gather value types for custom fields
final GeoSpatialDatatypeConfiguration datatypeConfig =
geoSpatialConfig.getConfigurationForDatatype(searchDatatypeUri);
if (datatypeConfig==null) {
throw new GeoSpatialSearchException(
"Datatype " + searchDatatypeUri + " is not a registered geospatial "
+ "datatype. Query cannot be executed.");
}
final ValueType[] customFieldsVTs = new ValueType[customFields.length];
for (int i=0; i<customFields.length; i++) {
final ValueType vt = datatypeConfig.getValueTypeOfCustomField(customFields[i]);
if (vt!=null) {
customFieldsVTs[i] = vt;
} else {
throw new GeoSpatialSearchException(
"Undefined custom field used in query: " + customFields[i]);
}
}
final Object[] customFieldsLowerBounds =
resolveAsLongDoubleArr(this.customFieldsLowerBounds, customFieldsVTs, bs);
final Object[] customFieldsUpperBounds =
resolveAsLongDoubleArr(this.customFieldsUpperBounds, customFieldsVTs, bs);
final GeoSpatialQuery sq =
new GeoSpatialQuery(geoSpatialConfig, searchFunction, searchDatatypeUri,
bs.get(searchVar), predicate, context, spatialCircleCenter,
spatialCircleRadius, spatialRectangleUpperLeft,
spatialRectangleLowerRight, spatialUnit, timeStart, timeEnd, coordSystem,
GeoSpatialQuery.toValidatedCustomFieldsConstraints(
customFields, customFieldsLowerBounds, customFieldsUpperBounds),
locationVar, timeVar, locationAndTimeVar, latVar, lonVar,
coordSystemVar, customFieldsVar, literalVar, distanceVar, bs);
return sq;
}
GeoFunction resolveAsGeoFunction(final TermNode termNode, final IBindingSet bs) {
if (termNode==null) {
return GeoFunction.UNDEFINED; // no geo function defined
}
String geoFunctionStr = resolveAsString(termNode, bs);
// try override with system default, if not set
if (geoFunctionStr == null) {
geoFunctionStr = defaults.getDefaultFunction();
}
if (geoFunctionStr != null && !geoFunctionStr.isEmpty()) {
final GeoFunction gf = GeoFunction.forName(geoFunctionStr);
if (gf==null) {
throw new GeoSpatialSearchException("Geo function '" + geoFunctionStr + "' not known.");
}
return gf;
}
return GeoFunction.UNDEFINED; // fallback
}
UNITS resolveAsSpatialDistanceUnit(final TermNode termNode, final IBindingSet bs) {
String spatialUnitStr = resolveAsString(termNode, bs);
// try override with system default, if not set
if (spatialUnitStr == null) {
spatialUnitStr = defaults.getDefaultSpatialDistanceUnit();
}
if (spatialUnitStr != null && !spatialUnitStr.isEmpty()) {
final UNITS u = UNITS.valueOf(spatialUnitStr);
if (u==null) {
throw new GeoSpatialSearchException("Input could not be parsed as unit: '" + spatialUnitStr + "'.");
}
return u;
}
return GeoSpatial.Options.DEFAULT_GEO_SPATIAL_UNIT; // fallback
}
Double resolveAsDouble(final TermNode termNode, final IBindingSet bs) {
String s = resolveAsString(termNode, bs);
if (s == null || s.isEmpty()) {
return null;
}
try {
return Double.valueOf(s);
} catch (NumberFormatException e) {
throw new GeoSpatialSearchException("Input could not be resolved as double value: '" + s + "'.");
}
}
Long resolveAsLong(final TermNode termNode, final IBindingSet bs) {
String s = resolveAsString(termNode, bs);
if (s == null || s.isEmpty()) {
return null;
}
try {
return Long.valueOf(s);
} catch (NumberFormatException e) {
throw new GeoSpatialSearchException("Input could not be resolved as long value: '" + s + "'.");
}
}
Literal resolveAsLiteral(final TermNode termNode, final IBindingSet bs) {
if (termNode == null) { // term node not set explicitly
return null;
}
if (termNode.isConstant()) {
return (Literal) termNode.getValue();
} else {
if (bs == null) {
return null; // shouldn't happen, but just in case...
}
final IVariable<?> var = (IVariable<?>) termNode .getValueExpression();
if (bs.isBound(var)) {
final IConstant<?> c = bs.get(var);
if (c == null || c.get() == null) {
return null;
}
final Object obj = c.get();
if (obj instanceof TermId<?>) {
return ((TermId<?>) obj);
} else if (obj instanceof IV) {
@SuppressWarnings("rawtypes")
final BigdataValue bdVal = ((IV)obj).getValue();
if (bdVal!=null) {
return (Literal)bdVal;
}
}
// should never end up here
throw new GeoSpatialSearchException("Value for literal could not be retrieved.");
} else {
throw new GeoSpatialSearchException(
GeoSpatialSearchException.SERVICE_VARIABLE_UNBOUND + ":" + var);
}
}
}
PointLatLon resolveAsPoint(final TermNode termNode, final IBindingSet bs) {
final Literal lit = resolveAsLiteral(termNode, bs);
if (lit == null || lit.stringValue().isEmpty()) {
return null;
}
final String pointAsStr = lit.stringValue();
IGeoSpatialLiteralSerializer serializer = null;
GeoSpatialDatatypeConfiguration pconfig = null;
if (lit.getDatatype() != null) {
// If we have datatype that can extract coordinates, use it to exteract
pconfig = geoSpatialConfig.getConfigurationForDatatype(lit.getDatatype());
if (pconfig.hasLat() && pconfig.hasLon()) {
serializer = pconfig.getLiteralSerializer();
}
}
try {
if (serializer == null) {
return new PointLatLon(pointAsStr);
} else {
final String[] comps = serializer.toComponents(pointAsStr);
final Double lat = Double.parseDouble(comps[pconfig.idxOfField(ServiceMapping.LATITUDE)]);
final Double lon = Double.parseDouble(comps[pconfig.idxOfField(ServiceMapping.LONGITUDE)]);
return new PointLatLon(lat, lon);
}
} catch (NumberFormatException e) {
throw new GeoSpatialSearchException("Input could not be resolved as point: '" + pointAsStr + "'.");
}
}
String resolveAsString(final TermNode termNode, final IBindingSet bs) {
if (termNode == null) { // term node not set explicitly
return null;
}
if (termNode.isConstant()) {
final Literal lit = (Literal) termNode.getValue();
return lit == null ? null : lit.stringValue();
} else {
if (bs == null) {
return null; // shouldn't happen, but just in case...
}
final IVariable<?> var = (IVariable<?>) termNode .getValueExpression();
if (bs.isBound(var)) {
IConstant<?> c = bs.get(var);
if (c == null || c.get() == null) {
return null;
}
final Object obj = c.get();
if (obj instanceof TermId<?>) {
return ((TermId<?>) obj).stringValue();
} else if (obj instanceof IV) {
@SuppressWarnings("rawtypes")
final BigdataValue bdVal = ((IV)obj).getValue();
if (bdVal!=null) {
return bdVal.stringValue();
}
}
// should never end up here
throw new GeoSpatialSearchException("Value for literal could not be retrieved.");
} else {
throw new GeoSpatialSearchException(
GeoSpatialSearchException.SERVICE_VARIABLE_UNBOUND + ":" + var);
}
}
}
String[] resolveAsStringArr(final TermNode termNode, final IBindingSet bs) {
final String toSplit = resolveAsString(termNode, bs);
if (toSplit==null)
return new String[0];
return toSplit.split(GeoSpatial.CUSTOM_FIELDS_SEPARATOR);
}
Object[] resolveAsLongDoubleArr(final TermNode termNode, final ValueType[] valueTypes, final IBindingSet bs) {
final String[] stringArr = resolveAsStringArr(termNode, bs);
if (stringArr.length!=valueTypes.length) {
throw new GeoSpatialSearchException(
"Magic predicates geo:customFields, geo:customFieldsLowerBounds, and "
+ "geo:customFieldsUpperBounds must all be given and have same length.");
}
final Object[] objArr = new Object[stringArr.length];
for (int i=0; i<stringArr.length; i++) {
switch (valueTypes[i]) {
case DOUBLE:
{
final Double val = Double.valueOf(stringArr[i]);
objArr[i] = val;
break;
}
case LONG:
{
final Long val = Long.valueOf(stringArr[i]);
objArr[i] = val;
break;
}
default:
throw new GeoSpatialSearchException("Unhandled value type: " + valueTypes[i]);
}
}
return objArr;
}
URI resolveSearchDatatype(final TermNode searchDatatype, final IBindingSet bs) {
if (searchDatatype==null) {
final URI datatype = geoSpatialConfig.getDefaultDatatype();
if (datatype==null) {
throw new GeoSpatialSearchException(
"No default datatype set in configuration. Please specify the datatype "
+ "that you want to query using magic predicate " + GeoSpatial.SEARCH_DATATYPE + ".");
}
return datatype;
}
if (searchDatatype.isConstant()) {
URI uri = (URI) searchDatatype.getValue();
if (uri==null) {
uri = geoSpatialConfig.getDefaultDatatype();
if (uri==null) {
throw new GeoSpatialSearchException(
"No default datatype set in configuration. Please specify the datatype "
+ "that you want to query using magic predicate " + GeoSpatial.SEARCH_DATATYPE + ".");
}
}
return uri;
} else {
if (bs==null) {
return null; // shouldn't happen, but just in case...
}
final IVariable<?> var = (IVariable<?>) searchDatatype
.getValueExpression();
if (bs.isBound(var)) {
IConstant<?> c = bs.get(var);
if (c == null || c.get() == null
|| !(c.get() instanceof TermId<?>)) {
return null;
}
TermId<?> cAsTerm = (TermId<?>) c.get();
return (URI)cAsTerm.getValue();
} else {
throw new GeoSpatialSearchException(
GeoSpatialSearchException.SERVICE_VARIABLE_UNBOUND + ":" + var);
}
}
}
}
}