blob: 634359fd193ac85bab59a15edc808d3875a4b1d7 [file] [log] [blame]
package org.apache.rya.indexing.mongodb.geo;
/*
* 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 java.util.ArrayList;
import java.util.List;
import org.apache.log4j.Logger;
import org.apache.rya.api.domain.RyaStatement;
import org.apache.rya.api.resolver.RyaToRdfConversions;
import org.apache.rya.indexing.accumulo.geo.GeoParseUtils;
import org.apache.rya.indexing.mongodb.IndexingMongoDBStorageStrategy;
import org.bson.Document;
import org.openrdf.model.Statement;
import org.openrdf.query.MalformedQueryException;
import com.mongodb.BasicDBObject;
import com.mongodb.BasicDBObjectBuilder;
import com.mongodb.DBCollection;
import com.mongodb.DBObject;
import com.vividsolutions.jts.geom.Coordinate;
import com.vividsolutions.jts.geom.Geometry;
import com.vividsolutions.jts.geom.Point;
import com.vividsolutions.jts.geom.Polygon;
import com.vividsolutions.jts.io.ParseException;
import com.vividsolutions.jts.io.WKTReader;
public class GeoMongoDBStorageStrategy extends IndexingMongoDBStorageStrategy {
private static final Logger LOG = Logger.getLogger(GeoMongoDBStorageStrategy.class);
private static final String GEO = "location";
public enum GeoQueryType {
INTERSECTS {
@Override
public String getKeyword() {
return "$geoIntersects";
}
}, WITHIN {
@Override
public String getKeyword() {
return "$geoWithin";
}
}, EQUALS {
@Override
public String getKeyword() {
return "$near";
}
}, NEAR {
@Override
public String getKeyword() {
return "$near";
}
};
public abstract String getKeyword();
}
public static class GeoQuery {
private final GeoQueryType queryType;
private final Geometry geo;
private final Double maxDistance;
private final Double minDistance;
public GeoQuery(final GeoQueryType queryType, final Geometry geo) {
this(queryType, geo, 0, 0);
}
public GeoQuery(final GeoQueryType queryType, final Geometry geo, final double maxDistance,
final double minDistance) {
this.queryType = queryType;
this.geo = geo;
this.maxDistance = maxDistance;
this.minDistance = minDistance;
}
public GeoQueryType getQueryType() {
return queryType;
}
public Geometry getGeo() {
return geo;
}
public Double getMaxDistance() {
return maxDistance;
}
public Double getMinDistance() {
return minDistance;
}
}
private final Double maxDistance;
public GeoMongoDBStorageStrategy(final Double maxDistance) {
this.maxDistance = maxDistance;
}
@Override
public void createIndices(final DBCollection coll){
coll.createIndex(new BasicDBObject(GEO, "2dsphere"));
}
public DBObject getQuery(final GeoQuery queryObj) throws MalformedQueryException {
final Geometry geo = queryObj.getGeo();
final GeoQueryType queryType = queryObj.getQueryType();
if (queryType == GeoQueryType.WITHIN && !(geo instanceof Polygon)) {
//They can also be applied to MultiPolygons, but those are not supported either.
throw new MalformedQueryException("Mongo Within operations can only be performed on Polygons.");
} else if(queryType == GeoQueryType.NEAR && !(geo instanceof Point)) {
//They can also be applied to Point, but those are not supported either.
throw new MalformedQueryException("Mongo near operations can only be performed on Points.");
}
BasicDBObject query;
if (queryType.equals(GeoQueryType.EQUALS)){
if(geo.getNumPoints() == 1) {
final List circle = new ArrayList();
circle.add(getPoint(geo));
circle.add(maxDistance);
final BasicDBObject polygon = new BasicDBObject("$centerSphere", circle);
query = new BasicDBObject(GEO, new BasicDBObject(GeoQueryType.WITHIN.getKeyword(), polygon));
} else {
query = new BasicDBObject(GEO, getCorrespondingPoints(geo));
}
} else if(queryType.equals(GeoQueryType.NEAR)) {
final BasicDBObject geoDoc = new BasicDBObject("$geometry", getDBPoint(geo));
if(queryObj.getMaxDistance() != 0) {
geoDoc.append("$maxDistance", queryObj.getMaxDistance());
}
if(queryObj.getMinDistance() != 0) {
geoDoc.append("$minDistance", queryObj.getMinDistance());
}
query = new BasicDBObject(GEO, new BasicDBObject(queryType.getKeyword(), geoDoc));
} else {
final BasicDBObject geoDoc = new BasicDBObject("$geometry", getCorrespondingPoints(geo));
query = new BasicDBObject(GEO, new BasicDBObject(queryType.getKeyword(), geoDoc));
}
return query;
}
@Override
public DBObject serialize(final RyaStatement ryaStatement) {
// if the object is wkt, then try to index it
// write the statement data to the fields
try {
final Statement statement = RyaToRdfConversions.convertStatement(ryaStatement);
final Geometry geo = (new WKTReader()).read(GeoParseUtils.getWellKnownText(statement));
if(geo == null) {
LOG.error("Failed to parse geo statement: " + statement.toString());
return null;
}
final BasicDBObject base = (BasicDBObject) super.serialize(ryaStatement);
if (geo.getNumPoints() > 1) {
base.append(GEO, getCorrespondingPoints(geo));
} else {
base.append(GEO, getDBPoint(geo));
}
return base;
} catch(final ParseException e) {
LOG.error("Could not create geometry for statement " + ryaStatement, e);
return null;
}
}
public Document getCorrespondingPoints(final Geometry geo) {
//Polygons must be a 3 dimensional array.
//polygons must be a closed loop
final Document geoDoc = new Document();
if (geo instanceof Polygon) {
final Polygon poly = (Polygon) geo;
final List<List<List<Double>>> DBpoints = new ArrayList<>();
// outer shell of the polygon
final List<List<Double>> ring = new ArrayList<>();
for (final Coordinate coord : poly.getExteriorRing().getCoordinates()) {
ring.add(getPoint(coord));
}
DBpoints.add(ring);
// each hold in the polygon
for (int ii = 0; ii < poly.getNumInteriorRing(); ii++) {
final List<List<Double>> holeCoords = new ArrayList<>();
for (final Coordinate coord : poly.getInteriorRingN(ii).getCoordinates()) {
holeCoords.add(getPoint(coord));
}
DBpoints.add(holeCoords);
}
geoDoc.append("coordinates", DBpoints)
.append("type", "Polygon");
} else {
final List<List<Double>> points = getPoints(geo);
geoDoc.append("coordinates", points)
.append("type", "LineString");
}
return geoDoc;
}
private List<List<Double>> getPoints(final Geometry geo) {
final List<List<Double>> points = new ArrayList<>();
for (final Coordinate coord : geo.getCoordinates()) {
points.add(getPoint(coord));
}
return points;
}
public Document getDBPoint(final Geometry geo) {
return new Document()
.append("coordinates", getPoint(geo))
.append("type", "Point");
}
private List<Double> getPoint(final Coordinate coord) {
final List<Double> point = new ArrayList<>();
point.add(coord.x);
point.add(coord.y);
return point;
}
private List<Double> getPoint(final Geometry geo) {
final List<Double> point = new ArrayList<>();
point.add(geo.getCoordinate().x);
point.add(geo.getCoordinate().y);
return point;
}
}