diff options
author | Siyuan Zhou <siyuan.zhou@mongodb.com> | 2014-08-12 14:29:11 -0400 |
---|---|---|
committer | Siyuan Zhou <siyuan.zhou@mongodb.com> | 2014-08-21 16:21:49 -0400 |
commit | 528ddd163be6d93f839d57cd068bd17ab55440cd (patch) | |
tree | b8e1dc6e5a885ccaeae1584c2384a3f118392b1f /src/mongo/db/matcher/expression_geo.cpp | |
parent | 4cdeada1cc17bdf3fec7b39b2ec66faf8b84054d (diff) | |
download | mongo-528ddd163be6d93f839d57cd068bd17ab55440cd.tar.gz |
SERVER-14508 Break header dependencies of geo stuffs in query framework.
Diffstat (limited to 'src/mongo/db/matcher/expression_geo.cpp')
-rw-r--r-- | src/mongo/db/matcher/expression_geo.cpp | 302 |
1 files changed, 298 insertions, 4 deletions
diff --git a/src/mongo/db/matcher/expression_geo.cpp b/src/mongo/db/matcher/expression_geo.cpp index ee3142d1120..1fbc834bdb6 100644 --- a/src/mongo/db/matcher/expression_geo.cpp +++ b/src/mongo/db/matcher/expression_geo.cpp @@ -30,14 +30,308 @@ #include "mongo/pch.h" #include "mongo/db/matcher/expression_geo.h" +#include "mongo/db/geo/geoparser.h" +#include "mongo/util/mongoutils/str.h" +#include "mongo/util/log.h" namespace mongo { + + using mongoutils::str::equals; + + // + // GeoExpression + // + + // Put simple constructors here for scoped_ptr. + GeoExpression::GeoExpression() : field(""), predicate(INVALID) {} + GeoExpression::GeoExpression(const std::string& f) : field(f), predicate(INVALID) {} + + bool GeoExpression::parseLegacyQuery(const BSONObj &obj) { + // The only legacy syntax is {$within: {.....}} + BSONObjIterator outerIt(obj); + if (!outerIt.more()) { return false; } + BSONElement withinElt = outerIt.next(); + if (outerIt.more()) { return false; } + if (!withinElt.isABSONObj()) { return false; } + if (!equals(withinElt.fieldName(), "$within") && !equals(withinElt.fieldName(), "$geoWithin")) { + return false; + } + BSONObj withinObj = withinElt.embeddedObject(); + + bool hasGeometry = false; + + BSONObjIterator withinIt(withinObj); + while (withinIt.more()) { + BSONElement elt = withinIt.next(); + if (equals(elt.fieldName(), "$uniqueDocs")) { + warning() << "deprecated $uniqueDocs option: " << obj.toString() << endl; + // return false; + } + else if (elt.isABSONObj()) { + hasGeometry = geoContainer->parseFrom(elt.wrap()); + } + else { + warning() << "bad geo query: " << obj.toString() << endl; + return false; + } + } + + predicate = GeoExpression::WITHIN; + + return hasGeometry; + } + + bool GeoExpression::parseNewQuery(const BSONObj &obj) { + // pointA = { "type" : "Point", "coordinates": [ 40, 5 ] } + // t.find({ "geo" : { "$intersect" : { "$geometry" : pointA} } }) + // t.find({ "geo" : { "$within" : { "$geometry" : polygon } } }) + // where field.name is "geo" + BSONElement e = obj.firstElement(); + if (!e.isABSONObj()) { return false; } + + BSONObj::MatchType matchType = static_cast<BSONObj::MatchType>(e.getGtLtOp()); + if (BSONObj::opGEO_INTERSECTS == matchType) { + predicate = GeoExpression::INTERSECT; + } else if (BSONObj::opWITHIN == matchType) { + predicate = GeoExpression::WITHIN; + } else { + return false; + } + + bool hasGeometry = false; + BSONObjIterator argIt(e.embeddedObject()); + while (argIt.more()) { + BSONElement e = argIt.next(); + if (mongoutils::str::equals(e.fieldName(), "$geometry")) { + if (e.isABSONObj()) { + BSONObj embeddedObj = e.embeddedObject(); + if (geoContainer->parseFrom(embeddedObj)) { + hasGeometry = true; + } + } + } + } + + // Don't want to give the error below if we could not pull any geometry out. + if (!hasGeometry) { return false; } + + if (GeoExpression::WITHIN == predicate) { + // Why do we only deal with $within {polygon}? + // 1. Finding things within a point is silly and only valid + // for points and degenerate lines/polys. + // + // 2. Finding points within a line is easy but that's called intersect. + // Finding lines within a line is kind of tricky given what S2 gives us. + // Doing line-within-line is a valid yet unsupported feature, + // though I wonder if we want to preserve orientation for lines or + // allow (a,b),(c,d) to be within (c,d),(a,b). Anyway, punt on + // this for now. + uassert(16672, "$within not supported with provided geometry: " + obj.toString(), + geoContainer->supportsContains()); + } + + return hasGeometry; + } + + bool GeoExpression::parseFrom(const BSONObj &obj) { + geoContainer.reset(new GeometryContainer()); + if (!(parseLegacyQuery(obj) || parseNewQuery(obj))) + return false; + + // Big polygon with strict winding order is represented as an S2Loop in SPHERE CRS. + // So converting the query to SPHERE CRS makes things easier than projecting all the data + // into STRICT_SPHERE CRS. + if (STRICT_SPHERE == geoContainer->getNativeCRS()) { + if (!geoContainer->supportsProject(SPHERE)) + return false; + geoContainer->projectInto(SPHERE); + } + + // $geoIntersect queries are hardcoded to *always* be in SPHERE CRS + // TODO: This is probably bad semantics, should not do this + if (GeoExpression::INTERSECT == predicate) { + if (!geoContainer->supportsProject(SPHERE)) + return false; + geoContainer->projectInto(SPHERE); + } + + return true; + } + + // + // GeoNearExpression + // + + GeoNearExpression::GeoNearExpression() + : minDistance(0), + maxDistance(std::numeric_limits<double>::max()), + isNearSphere(false), + unitsAreRadians(false), + isWrappingQuery(false) { } + + GeoNearExpression::GeoNearExpression(const std::string& f) + : field(f), + minDistance(0), + maxDistance(std::numeric_limits<double>::max()), + isNearSphere(false), + unitsAreRadians(false), + isWrappingQuery(false) { } + + bool GeoNearExpression::parseLegacyQuery(const BSONObj &obj) { + + bool hasGeometry = false; + + // First, try legacy near, e.g.: + // t.find({ loc : { $nearSphere: [0,0], $minDistance: 1, $maxDistance: 3 }}) + // t.find({ loc : { $nearSphere: [0,0] }}) + // t.find({ loc : { $near : [0, 0, 1] } }); + // t.find({ loc : { $near: { someGeoJSONPoint}}) + // t.find({ loc : { $geoNear: { someGeoJSONPoint}}) + BSONObjIterator it(obj); + while (it.more()) { + BSONElement e = it.next(); + if (equals(e.fieldName(), "$near") || equals(e.fieldName(), "$geoNear") + || equals(e.fieldName(), "$nearSphere")) { + if (!e.isABSONObj()) { return false; } + BSONObj embeddedObj = e.embeddedObject(); + + if ((GeoParser::isPoint(embeddedObj) && GeoParser::parsePoint(embeddedObj, centroid.get())) + || GeoParser::parsePointWithMaxDistance(embeddedObj, centroid.get(), &maxDistance)) { + uassert(18522, "max distance must be non-negative", maxDistance >= 0.0); + hasGeometry = true; + isNearSphere = equals(e.fieldName(), "$nearSphere"); + } + } else if (equals(e.fieldName(), "$minDistance")) { + uassert(16893, "$minDistance must be a number", e.isNumber()); + minDistance = e.Number(); + uassert(16894, "$minDistance must be non-negative", minDistance >= 0.0); + } else if (equals(e.fieldName(), "$maxDistance")) { + uassert(16895, "$maxDistance must be a number", e.isNumber()); + maxDistance = e.Number(); + uassert(16896, "$maxDistance must be non-negative", maxDistance >= 0.0); + } else if (equals(e.fieldName(), "$uniqueDocs")) { + warning() << "ignoring deprecated option $uniqueDocs"; + } + } + + return hasGeometry; + } + + Status GeoNearExpression::parseNewQuery(const BSONObj &obj) { + bool hasGeometry = false; + + BSONObjIterator objIt(obj); + if (!objIt.more()) { + return Status(ErrorCodes::BadValue, "empty geo near query object"); + } + BSONElement e = objIt.next(); + // Just one arg. to $geoNear. + if (objIt.more()) { + return Status(ErrorCodes::BadValue, mongoutils::str::stream() << + "geo near accepts just one argument when querying for a GeoJSON " << + "point. Extra field found: " << objIt.next()); + } + + // Parse "new" near: + // t.find({"geo" : {"$near" : {"$geometry": pointA, $minDistance: 1, $maxDistance: 3}}}) + // t.find({"geo" : {"$geoNear" : {"$geometry": pointA, $minDistance: 1, $maxDistance: 3}}}) + if (!e.isABSONObj()) { + return Status(ErrorCodes::BadValue, "geo near query argument is not an object"); + } + BSONObj::MatchType matchType = static_cast<BSONObj::MatchType>(e.getGtLtOp()); + if (BSONObj::opNEAR != matchType) { + return Status(ErrorCodes::BadValue, mongoutils::str::stream() << + "invalid geo near query operator: " << e.fieldName()); + } + + // Iterate over the argument. + BSONObjIterator it(e.embeddedObject()); + while (it.more()) { + BSONElement e = it.next(); + if (equals(e.fieldName(), "$geometry")) { + if (e.isABSONObj()) { + BSONObj embeddedObj = e.embeddedObject(); + uassert(16885, "$near requires a point, given " + embeddedObj.toString(), + GeoParser::isPoint(embeddedObj)); + if (!GeoParser::parsePoint(embeddedObj, centroid.get())) { + return Status(ErrorCodes::BadValue, mongoutils::str::stream() << + "invalid point in geo near query $geometry argument: " << + embeddedObj); + } + uassert(16681, "$near requires geojson point, given " + embeddedObj.toString(), + (SPHERE == centroid->crs)); + hasGeometry = true; + } + } else if (equals(e.fieldName(), "$minDistance")) { + uassert(16897, "$minDistance must be a number", e.isNumber()); + minDistance = e.Number(); + uassert(16898, "$minDistance must be non-negative", minDistance >= 0.0); + } else if (equals(e.fieldName(), "$maxDistance")) { + uassert(16899, "$maxDistance must be a number", e.isNumber()); + maxDistance = e.Number(); + uassert(16900, "$maxDistance must be non-negative", maxDistance >= 0.0); + } + } + + if (!hasGeometry) { + return Status(ErrorCodes::BadValue, "$geometry is required for geo near query"); + } + + return Status::OK(); + } + + + Status GeoNearExpression::parseFrom(const BSONObj &obj) { + + Status status = Status::OK(); + centroid.reset(new PointWithCRS()); + + if (!parseLegacyQuery(obj)) { + // Clear out any half-baked data. + minDistance = 0; + isNearSphere = false; + maxDistance = std::numeric_limits<double>::max(); + // ...and try parsing new format. + status = parseNewQuery(obj); + } + + if (!status.isOK()) + return status; + + // Fixup the near query for anonoyances caused by $nearSphere + if (isNearSphere) { + + // The user-provided point can be flat for a spherical query - needs to be projectable + uassert(17444, + "Legacy point is out of bounds for spherical query", + ShapeProjection::supportsProject(*centroid, SPHERE)); + + unitsAreRadians = SPHERE != centroid->crs; + // GeoJSON points imply wrapping queries + isWrappingQuery = SPHERE == centroid->crs; + + // Project the point to a spherical CRS now that we've got the settings we need + // We need to manually project here since we aren't using GeometryContainer + ShapeProjection::projectInto(centroid.get(), SPHERE); + } + else { + unitsAreRadians = false; + isWrappingQuery = SPHERE == centroid->crs; + } + + return status; + } + + // + // GeoMatchExpression and GeoNearMatchExpression + // + // // Geo queries we don't need an index to answer: geoWithin and geoIntersects // - Status GeoMatchExpression::init( const StringData& path, const GeoQuery* query, + Status GeoMatchExpression::init( const StringData& path, const GeoExpression* query, const BSONObj& rawObj ) { _query.reset(query); _rawObj = rawObj; @@ -62,11 +356,11 @@ namespace mongo { geometry.projectInto(_query->getGeometry().getNativeCRS()); - if (GeoQuery::WITHIN == _query->getPred()) { + if (GeoExpression::WITHIN == _query->getPred()) { return _query->getGeometry().contains(geometry); } else { - verify(GeoQuery::INTERSECT == _query->getPred()); + verify(GeoExpression::INTERSECT == _query->getPred()); return _query->getGeometry().intersects(geometry); } } @@ -114,7 +408,7 @@ namespace mongo { // Parse-only geo expressions: geoNear (formerly known as near). // - Status GeoNearMatchExpression::init( const StringData& path, const NearQuery* query, + Status GeoNearMatchExpression::init( const StringData& path, const GeoNearExpression* query, const BSONObj& rawObj ) { _query.reset(query); _rawObj = rawObj; |