Browse files

working exact distance checks for 2d queries SERVER-2115

  • Loading branch information...
1 parent fbbd378 commit 4e4a1c2b282d071568553f92e9c40f3db7c71ff0 gregs committed Apr 7, 2011
Showing with 615 additions and 84 deletions.
  1. +368 −73 db/geo/2d.cpp
  2. +33 −0 db/geo/core.h
  3. +32 −0 jstests/geo_fiddly_box2.js
  4. +74 −11 jstests/geo_small_large.js
  5. +108 −0 jstests/slowNightly/geo_axis_aligned.js
View
441 db/geo/2d.cpp
@@ -154,7 +154,18 @@ namespace mongo {
return b.obj();
}
- virtual void getKeys( const BSONObj &obj, BSONObjSetDefaultOrder &keys ) const {
+ /** Finds the key objects to put in an index */
+ virtual void getKeys( const BSONObj& obj, BSONObjSetDefaultOrder& keys ) const {
+ getKeys( obj, &keys, NULL );
+ }
+
+ /** Finds all locations in a geo-indexed object */
+ void getKeys( const BSONObj& obj, vector< BSONObj >& locs ) const {
+ getKeys( obj, NULL, &locs );
+ }
+
+ /** Finds the key objects and/or locations for a geo-indexed object */
+ void getKeys( const BSONObj &obj, BSONObjSetDefaultOrder* keys, vector< BSONObj >* locs ) const {
BSONElementSet bSet;
// Get all the nested location fields, but don't return individual elements from
@@ -210,6 +221,16 @@ namespace mongo {
BSONObjBuilder b(64);
+ // Remember the actual location object if needed
+ if( locs )
+ locs->push_back( locObj );
+
+ // Stop if we don't need to get anything but location objects
+ if( ! keys ) {
+ if( singleElement ) break;
+ else continue;
+ }
+
_hash( locObj ).append( b , "" );
// Go through all the other index keys
@@ -242,7 +263,7 @@ namespace mongo {
}
- keys.insert( b.obj() );
+ keys->insert( b.obj() );
if( singleElement ) break;
@@ -404,6 +425,10 @@ namespace mongo {
return val + fudge >= min && val <= max + fudge;
}
+ bool onBoundary( double bound, double val, double fudge = 0 ) const {
+ return ( val >= bound - fudge && val <= bound + fudge );
+ }
+
bool mid( double amin , double amax , double bmin , double bmax , bool min , double& res ) const {
assert( amin <= amax );
assert( bmin <= bmax );
@@ -445,6 +470,13 @@ namespace mongo {
( _min._y + _max._y ) / 2 );
}
+ bool onBoundary( Point p, double fudge = 0 ) {
+ return onBoundary( _min._x, p._x, fudge ) ||
+ onBoundary( _max._x, p._x, fudge ) ||
+ onBoundary( _min._y, p._y, fudge ) ||
+ onBoundary( _max._y, p._y, fudge );
+ }
+
bool inside( Point p , double fudge = 0 ) {
bool res = inside( p._x , p._y , fudge );
//cout << "is : " << p.toString() << " in " << toString() << " = " << res << endl;
@@ -717,31 +749,65 @@ namespace mongo {
}
} geoUnitTest;
+ class GeoHopper;
+
class GeoPoint {
public:
GeoPoint() {
}
- GeoPoint( const KeyNode& node , double distance )
- : _key( node.key ) , _loc( node.recordLoc ) , _o( node.recordLoc.obj() ) , _distance( distance ) {
+ //// Distance not used ////
+
+ GeoPoint( const KeyNode& node )
+ : _key( node.key ) , _loc( node.recordLoc ) , _o( node.recordLoc.obj() ) , _distance( -1 ), _exactDistance( -1 ), _hopper( NULL ) {
+ }
+
+ //// Lazy initialization of exact distance ////
+
+ GeoPoint( const KeyNode& node , double distance, GeoHopper* hopper )
+ : _key( node.key ) , _loc( node.recordLoc ) , _o( node.recordLoc.obj() ) , _distance( distance ), _exactDistance( -1 ), _hopper( hopper ) {
}
- GeoPoint( const BSONObj& key , DiskLoc loc , double distance )
- : _key(key) , _loc(loc) , _o( loc.obj() ) , _distance( distance ) {
+ GeoPoint( const BSONObj& key , DiskLoc loc , double distance, GeoHopper* hopper )
+ : _key(key) , _loc(loc) , _o( loc.obj() ) , _distance( distance ), _exactDistance( -1 ), _hopper( hopper ) {
}
- bool operator<( const GeoPoint& other ) const {
- return _distance < other._distance;
+ //// Immediate initialization of exact distance ////
+
+ GeoPoint( const KeyNode& node , double distance, double exactDistance, bool exactWithin, GeoHopper* hopper )
+ : _key( node.key ) , _loc( node.recordLoc ) , _o( node.recordLoc.obj() ) , _distance( distance ), _exactDistance( exactDistance ), _exactWithin( exactWithin ), _hopper( hopper ) {
}
+ GeoPoint( const BSONObj& key , DiskLoc loc , double distance, double exactDistance, bool exactWithin, GeoHopper* hopper )
+ : _key(key) , _loc(loc) , _o( loc.obj() ) , _distance( distance ), _exactDistance( exactDistance ), _exactWithin( exactWithin ), _hopper( hopper ) {
+ }
+
+ double exactDistance();
+
+ bool exactWithin();
+
+ bool operator<( const GeoPoint& other ) const;
+
bool isEmpty() const {
return _o.isEmpty();
}
+ string toString() const {
+
+ return str::stream() << "Point from " << _o.toString() << " approx : " << _distance << " exact : " << _exactDistance
+ << " within ? " << _exactWithin;
+
+ }
+
BSONObj _key;
DiskLoc _loc;
BSONObj _o;
double _distance;
+
+ double _exactDistance;
+ bool _exactWithin;
+ GeoHopper* _hopper;
+
};
class GeoAccumulator {
@@ -770,7 +836,7 @@ namespace mongo {
// distance check
double d = 0;
- if ( ! checkDistance( GeoHash( node.key.firstElement() ) , d ) ) {
+ if ( ! checkDistance( node , d ) ) {
GEODEBUG( "\t\t\t\t bad distance : " << node.recordLoc.obj() << "\t" << d );
return;
}
@@ -811,7 +877,7 @@ namespace mongo {
}
virtual void addSpecific( const KeyNode& node , double d, bool newDoc ) = 0;
- virtual bool checkDistance( const GeoHash& node , double& d ) = 0;
+ virtual bool checkDistance( const KeyNode& node , double& d ) = 0;
long long found() const {
return _found;
@@ -831,56 +897,208 @@ namespace mongo {
public:
typedef multiset<GeoPoint> Holder;
- GeoHopper( const Geo2dType * g , unsigned max , const Point& n , const BSONObj& filter = BSONObj() , double maxDistance = numeric_limits<double>::max() , GeoDistType type=GEO_PLAIN)
- : GeoAccumulator( g , filter ) , _max( max ) , _near( n ), _maxDistance( maxDistance ), _type( type ), _farthest(-1)
+ GeoHopper( const Geo2dType * g , unsigned max , const Point& n , const BSONObj& filter = BSONObj() , double maxDistance = numeric_limits<double>::max() , GeoDistType type=GEO_PLAIN, bool lazyExact = true )
+ : GeoAccumulator( g , filter ) , _max( max ) , _near( n ), _maxDistance( maxDistance ), _type( type ), _farthest(-1), _lazyExact( lazyExact )
{}
- virtual bool checkDistance( const GeoHash& h , double& d ) {
+ virtual bool checkDistance( const KeyNode& node, double& d ) {
+
+ // Always check approximate distance, since it lets us avoid doing
+ // checks of the rest of the object if it succeeds
+ // TODO: Refactor so that we can check exact distance and within if we are going to
+ // anyway.
+ d = approxDistance( node );
+
+ // If we're in the error range, return true
+ if( d >= _maxDistance - _g->_error && d <= _maxDistance + _g->_error ) {
+ // Should check exact point later in addSpecific()
+ return true;
+ }
+
+ // Out of the error range, see how close we are to the furthest points
+ bool good = d < _maxDistance && ( _points.size() < _max || d < farthest() );
+
+ GEODEBUG( "\t\t\t\t\t\t\t checkDistance " << _near.toString()
+ << "\t" << GeoHash( node.key.firstElement() ) << "\t" << d
+ << " ok: " << good << " farthest: " << farthest() );
+
+ return good;
+ }
+
+ double approxDistance( const KeyNode& node ) {
+ return approxDistance( GeoHash( node.key.firstElement() ) );
+ }
+
+ double approxDistance( const GeoHash& h ) {
+
+ double approxDistance = -1;
switch (_type) {
case GEO_PLAIN:
- d = _near.distance( Point(_g, h) );
+ approxDistance = _near.distance( Point( _g, h ) );
break;
case GEO_SPHERE:
- d = spheredist_deg(_near, Point(_g, h));
+ approxDistance = spheredist_deg( _near, Point( _g, h ) );
break;
- default:
- assert(0);
+ default: assert( false );
}
- bool good = d < _maxDistance && ( _points.size() < _max || d < farthest() );
- GEODEBUG( "\t\t\t\t\t\t\t checkDistance " << _near.toString() << "\t" << h << "\t" << d
- << " ok: " << good << " farthest: " << farthest() );
- return good;
+
+ return approxDistance;
+ }
+
+
+ /** Evaluates exact distance for keys and documents, storing which locations were used so far */
+ double exactDistance( const KeyNode& node, bool& exactWithin ) {
+ return exactDistance( node.key, node.recordLoc.obj(), exactWithin );
+ }
+
+ double exactDistance( const BSONObj& key, const BSONObj& doc, bool& exactWithin ) {
+
+ GEODEBUG( "Finding exact distance for " << key.toString() << " and " << doc.toString() );
+
+ // Find all the location objects from the keys
+ vector< BSONObj > locs;
+ _g->getKeys( doc, locs );
+
+ double exactDistance = -1;
+
+ // Find the particular location we want
+ BSONObj loc;
+ GeoHash keyHash( key.firstElement(), _g->_bits );
+ for( vector< BSONObj >::iterator i = locs.begin(); i != locs.end(); ++i ) {
+
+ loc = *i;
+
+ // Ignore all locations we've used
+ if( _usedLocs.end() != _usedLocs.find( loc.objdata() ) ) continue;
+ // Ignore all locations not hashed to the key's hash
+ if( _g->_hash( loc ) != keyHash ) continue;
+
+ // Get the appropriate distance for the type
+ switch ( _type ) {
+ case GEO_PLAIN:
+ exactDistance = _near.distance( Point( loc ) );
+ exactWithin = _near.distanceWithin( Point( loc ), _maxDistance );
+ break;
+ case GEO_SPHERE:
+ exactDistance = spheredist_deg( _near, Point( loc ) );
+ exactWithin = ( exactDistance <= _maxDistance );
+ break;
+ default: assert( false );
+ }
+
+ break;
+ }
+
+ assert( exactDistance >= 0 );
+
+ // Remember we used this sub-doc for this query
+ _usedLocs.insert( loc.objdata() );
+
+ return exactDistance;
+ }
+
+ double farthest() const {
+ return _farthest;
}
virtual void addSpecific( const KeyNode& node , double d, bool newDoc ) {
+
GEODEBUG( "\t\t" << GeoHash( node.key.firstElement() ) << "\t" << node.recordLoc.obj() << "\t" << d );
- _points.insert( GeoPoint( node.key , node.recordLoc , d ) );
+
+ bool inErrorBounds = ( d >= _maxDistance - _g->_error && d <= _maxDistance + _g->_error );
+
+ if( _lazyExact && ! inErrorBounds ) {
+
+ // Haven't yet looked up exact points, will look it up later
+ _points.insert( GeoPoint( node.key , node.recordLoc , d , this ) );
+
+ }
+ else {
+ // Look up exact point now.
+ bool exactWithin;
+ double exactD = exactDistance( node, exactWithin );
+
+ // Don't add the point if it's not actually in the range
+ if( ! exactWithin ) return;
+
+ _points.insert( GeoPoint( node.key , node.recordLoc , d , exactD , exactWithin, this ) );
+ }
+
+ // Recalculate the current furthest point.
if ( _points.size() > _max ) {
_points.erase( --_points.end() );
Holder::iterator i = _points.end();
i--;
- _farthest = i->_distance;
+
+ // Need to compensate for error both in the center point and in the current
+ // point, hence 2 * error
+ _farthest = i->_distance + 2 * _g->_error;
}
else {
if (d > _farthest)
_farthest = d;
}
}
- double farthest() const {
- return _farthest;
- }
-
-
unsigned _max;
Point _near;
Holder _points;
double _maxDistance;
GeoDistType _type;
double _farthest;
+
+ bool _lazyExact;
+ set< const char* > _usedLocs;
+
};
+ /** Caches and returns the exact distance of a point from the target location */
+ double GeoPoint::exactDistance() {
+ if( _exactDistance >= 0 ) return _exactDistance;
+
+ assert( _hopper );
+ _exactDistance = _hopper->exactDistance( _key, _o, _exactWithin );
+ assert( _exactDistance >= 0 );
+
+ return _exactDistance;
+ }
+
+
+ /** Caches and returns whether a point was found to be exactly in the bounds */
+ bool GeoPoint::exactWithin() {
+ if( _exactDistance >= 0 ) return _exactWithin;
+
+ // Get exact distance & within
+ exactDistance();
+
+ return _exactWithin;
+ }
+
+ bool GeoPoint::operator<( const GeoPoint& other ) const {
+
+ // We can only compare points when a distance was added!
+ assert( _distance >= 0 );
+
+ // log() << "Comparing " << *this << " and " << other << " result : " << endl;
+
+ // Check if we need to use exact distance. Our distances may be off by up to 2 * the single-box error
+ if( abs( _distance - other._distance ) <= 2 * _hopper->_g->_error ) {
+
+ // log() << " exact : " << (((GeoPoint*) this)->exactDistance() < ((GeoPoint&) other).exactDistance()) << " diff : " << (((GeoPoint*) this)->exactDistance() - ((GeoPoint&) other).exactDistance()) << endl;
+ // cout << fixed << setprecision(20) << " Comparing " << ((GeoPoint*) this)->exactDistance() << " and " << ((GeoPoint&) other).exactDistance() << " result " << (((GeoPoint*) this)->exactDistance() < ((GeoPoint&) other).exactDistance()) << endl;
+
+ return ((GeoPoint*) this)->exactDistance() < ((GeoPoint&) other).exactDistance();
+ }
+
+ // cout << fixed << setprecision(20) << "Inexact comparing " << _distance << " (" << ((GeoPoint*) this)->exactDistance() << ") and " << other._distance << " (" << ((GeoPoint&) other).exactDistance() << ") result : " << ( _distance < other._distance ) << " error : " << _hopper->_g->_error << endl;
+ // log() << " inexact : " << (_distance < other._distance) << " diff : " << (_distance - other._distance) << endl;
+
+ return _distance < other._distance;
+ }
+
+
+
struct BtreeLocation {
int pos;
bool found;
@@ -961,24 +1179,24 @@ namespace mongo {
class GeoSearch {
public:
- GeoSearch( const Geo2dType * g , const GeoHash& n , int numWanted=100 , BSONObj filter=BSONObj() , double maxDistance = numeric_limits<double>::max() , GeoDistType type=GEO_PLAIN)
- : _spec( g ) ,_startPt(g,n), _start( n ) ,
+ GeoSearch( const Geo2dType * g , const Point& startPt , int numWanted=100 , BSONObj filter=BSONObj() , double maxDistance = numeric_limits<double>::max() , GeoDistType type=GEO_PLAIN, bool lazyExact = true )
+ : _spec( g ) ,_startPt( startPt ), _start( g->hash( startPt._x, startPt._y ) ) ,
_numWanted( numWanted ) , _filter( filter ) , _maxDistance( maxDistance ) ,
- _hopper( new GeoHopper( g , numWanted , _startPt , filter , maxDistance, type ) ), _type(type) {
+ _hopper( new GeoHopper( g , numWanted , _startPt , filter , maxDistance, type, lazyExact ) ), _type(type) {
assert( g->getDetails() );
_nscanned = 0;
_found = 0;
if (type == GEO_PLAIN) {
- _scanDistance = maxDistance;
+ _scanDistance = _maxDistance + _spec->_error;
}
else if (type == GEO_SPHERE) {
- if (maxDistance == numeric_limits<double>::max()) {
- _scanDistance = maxDistance;
+ if (_maxDistance == numeric_limits<double>::max()) {
+ _scanDistance = _maxDistance;
}
else {
//TODO: consider splitting into x and y scan distances
- _scanDistance = computeXScanDistance(_startPt._y, rad2deg(maxDistance));
+ _scanDistance = computeXScanDistance(_startPt._y, rad2deg( _maxDistance + _spec->_error ));
}
}
else {
@@ -1002,11 +1220,12 @@ namespace mongo {
GeoHopper * hopper = _hopper.get();
_prefix = _start;
+
BtreeLocation min,max;
{
- // 1 regular geo hash algorithm
-
+ // TODO : Refactor this into the other, tested, geohash algorithm
+ // 1 regular geo hash algorithm
if ( ! BtreeLocation::initial( id , _spec , min , max , _start , _found , NULL ) )
return;
@@ -1041,7 +1260,7 @@ namespace mongo {
farthest = std::min(_scanDistance, computeXScanDistance(_startPt._y, rad2deg(farthest)));
}
GEODEBUGPRINT(farthest);
-
+
Box want( _startPt._x - farthest , _startPt._y - farthest , farthest * 2 );
GEODEBUGPRINT(want.toString());
@@ -1095,8 +1314,8 @@ namespace mongo {
GEODEBUGPRINT(testBox.toString());
}
- if (_alreadyScanned.contains(testBox, _spec->_error)) {
- GEODEBUG("skipping box: already scanned");
+ if ( _alreadyScanned.area() > 0 && _alreadyScanned.contains( testBox ) ) {
+ GEODEBUG( "skipping box : already scanned box " << _alreadyScanned.toString() );
return; // been here, done this
}
@@ -1195,7 +1414,30 @@ namespace mongo {
virtual ~GeoSearchCursor() {}
virtual bool ok() {
- return _cur != _end;
+ if( _cur == _end ) return false;
+
+ // If we're near the bounds of our range and did lazy distance loading,
+ // test the exact locations.
+
+ if( _s->_hopper->_lazyExact ) {
+
+ GeoPoint& point = (GeoPoint&) *_cur;
+
+ double distanceLeft = abs( _s->_maxDistance - point._distance );
+ if( abs( distanceLeft ) <= 2 * _s->_spec->_error ) {
+
+ GEODEBUG( point.toString() );
+
+ // Must check within, not distance, b/c of double precision errors
+ if( ! point.exactWithin() ) {
+ GEODEBUG( "Final point : " ); //<< _cur->_o.toString() << _cur->_distance << " exact : " << ((GeoPoint) _cur).exactDistance() );
+ _cur = _end;
+ return false;
+ }
+ }
+ }
+
+ return true;
}
virtual Record* _current() { assert(ok()); return _cur->_loc.rec(); }
@@ -1422,13 +1664,13 @@ namespace mongo {
int j = (_neighbor % 3) - 1;
if ( ( i == 0 && j == 0 ) ||
- ( i < 0 && _centerBox._min._x <= _g->_min ) ||
- ( j < 0 && _centerBox._min._y <= _g->_min ) ||
- ( i > 0 && _centerBox._max._x >= _g->_max ) ||
- ( j > 0 && _centerBox._max._y >= _g->_max ) ){
- continue; // main box or wrapped edge
- // TODO: We may want to enable wrapping in future, probably best as layer on top of
- // this search.
+ ( i < 0 && _centerBox._min._x <= _g->_min ) ||
+ ( j < 0 && _centerBox._min._y <= _g->_min ) ||
+ ( i > 0 && _centerBox._max._x >= _g->_max ) ||
+ ( j > 0 && _centerBox._max._y >= _g->_max ) ) {
+ continue; // main box or wrapped edge
+ // TODO: We may want to enable wrapping in future, probably best as layer on top of
+ // this search.
}
// Make sure we've got a reasonable center
@@ -1495,9 +1737,9 @@ namespace mongo {
if( ! newDoc ) return;
if ( _cur.isEmpty() )
- _cur = GeoPoint( node , d );
+ _cur = GeoPoint( node );
else
- _stack.push_back( GeoPoint( node , d ) );
+ _stack.push_back( GeoPoint( node ) );
}
virtual long long nscanned() {
@@ -1554,21 +1796,20 @@ namespace mongo {
_maxDistance = i.next().numberDouble();
uassert( 13061 , "need a max distance > 0 " , _maxDistance > 0 );
- _maxDistance += g->_error;
if (type == "$center") {
// Look in box with bounds of maxDistance in either direction
_type = GEO_PLAIN;
- _xScanDistance = _maxDistance;
- _yScanDistance = _maxDistance;
+ _xScanDistance = _maxDistance + _g->_error;
+ _yScanDistance = _maxDistance + _g->_error;
}
else if (type == "$centerSphere") {
// Same, but compute maxDistance using spherical transform
uassert(13461, "Spherical MaxDistance > PI. Are you sure you are using radians?", _maxDistance < M_PI);
_type = GEO_SPHERE;
- _yScanDistance = rad2deg(_maxDistance);
+ _yScanDistance = rad2deg( _maxDistance + _g->_error );
_xScanDistance = computeXScanDistance(_startPt._y, _yScanDistance);
uassert(13462, "Spherical distance would require wrapping, which isn't implemented yet",
@@ -1584,7 +1825,7 @@ namespace mongo {
_bBox._min = Point( _startPt._x - _xScanDistance, _startPt._y - _yScanDistance );
_bBox._max = Point( _startPt._x + _xScanDistance, _startPt._y + _yScanDistance );
- GEODEBUG( "Bounding box for circle query : " << _bBox.toString() << " (max distance : " << _maxDistance << ")" );
+ GEODEBUG( "Bounding box for circle query : " << _bBox.toString() << " (max distance : " << _maxDistance << ")" << " starting from " << _startPt.toString() );
ok();
}
@@ -1601,19 +1842,50 @@ namespace mongo {
return _bBox.intersects( cur );
}
- virtual bool checkDistance( const GeoHash& h , double& d ) {
+ virtual bool checkDistance( const KeyNode& node, double& d ) {
- // TODO: Inexact hash distance checks.
+ GeoHash h( node.key.firstElement(), _g->_bits );
+ // Inexact hash distance checks.
switch (_type) {
case GEO_PLAIN:
d = _g->distance( _start , h );
break;
case GEO_SPHERE:
- d = spheredist_deg(_startPt, Point(_g, h));
+ d = spheredist_deg( _startPt, Point( _g, h ) );
break;
- default:
- assert(0);
+ default: assert( false );
+ }
+
+ // If our distance is in the error bounds...
+ if( d >= _maxDistance - _g->_error && d <= _maxDistance + _g->_error ) {
+
+ // Do exact check
+ vector< BSONObj > locs;
+ _g->getKeys( node.recordLoc.obj(), locs );
+
+ for( vector< BSONObj >::iterator i = locs.begin(); i != locs.end(); ++i ) {
+
+ GEODEBUG( "Inexact distance : " << d << " vs " << _maxDistance << " from " << ( *i ).toString() << " due to error " << _g->_error );
+
+ // Exact distance checks.
+ switch (_type) {
+ case GEO_PLAIN: {
+ if( _startPt.distanceWithin( Point( *i ), _maxDistance ) ) return true;
+ break;
+ }
+ case GEO_SPHERE:
+ // Ignore all locations not hashed to the key's hash, since spherical calcs are
+ // more expensive.
+ if( _g->_hash( *i ) != h ) break;
+ if( spheredist_deg( _startPt , Point( *i ) ) <= _maxDistance ) return true;
+ break;
+ default: assert( false );
+ }
+
+ }
+
+ return false;
}
GEODEBUG( "\t " << h << "\t" << d );
@@ -1693,17 +1965,39 @@ namespace mongo {
return _want.intersects( cur );
}
- virtual bool checkDistance( const GeoHash& h , double& d ) {
+ virtual bool checkDistance( const KeyNode& node, double& d ) {
+
+ GeoHash h( node.key.firstElement() );
+ Point approxPt( _g, h );
+
+ bool approxInside = _want.inside( approxPt, _fudge );
+
+ if( approxInside && _want.onBoundary( approxPt, _fudge ) ) {
+
+ // Do exact check
+ vector< BSONObj > locs;
+ _g->getKeys( node.recordLoc.obj(), locs );
+
+ for( vector< BSONObj >::iterator i = locs.begin(); i != locs.end(); ++i ) {
+ if( _want.inside( Point( *i ) ) ) {
- // Check whether hash of point is inside box.
- // TODO: This is only exact up to the fudge factor
- bool res = _want.inside( Point( _g , h ) , _fudge );
+ GEODEBUG( "found exact point : " << _want.toString()
+ << " exact point : " << Point( *i ).toString()
+ << " approx point : " << approxPt.toString()
+ << " because of error: " << _fudge );
+
+ return true;
+ }
+ }
+
+ return false;
+ }
GEODEBUG( "checking point : " << _want.toString()
- << " point: " << Point( _g , h ).toString()
- << " in : " << res );
+ << " point: " << approxPt.toString()
+ << " in : " << _want.inside( approxPt, _fudge ) );
- return res;
+ return approxInside;
}
Box _want;
@@ -1762,7 +2056,7 @@ namespace mongo {
if ( e.isNumber() )
maxDistance = e.numberDouble();
}
- shared_ptr<GeoSearch> s( new GeoSearch( this , _tohash(e) , numWanted , query , maxDistance, type ) );
+ shared_ptr<GeoSearch> s( new GeoSearch( this , Point( e ) , numWanted , query , maxDistance, type ) );
s->exec();
shared_ptr<Cursor> c;
c.reset( new GeoSearchCursor( s ) );
@@ -1839,8 +2133,8 @@ namespace mongo {
numWanted = cmdObj["num"].numberInt();
uassert(13046, "'near' param missing/invalid", !cmdObj["near"].eoo());
- const GeoHash n = g->_tohash( cmdObj["near"] );
- result.append( "near" , n.toString() );
+ const Point n( cmdObj["near"] );
+ result.append( "near" , g->_tohash( cmdObj["near"] ).toString() );
BSONObj filter;
if ( cmdObj["query"].type() == Object )
@@ -1854,7 +2148,8 @@ namespace mongo {
if ( cmdObj["spherical"].trueValue() )
type = GEO_SPHERE;
- GeoSearch gs( g , n , numWanted , filter , maxDistance , type);
+ // We're returning exact distances, so don't evaluate lazily.
+ GeoSearch gs( g , n , numWanted , filter , maxDistance , type, false );
if ( cmdObj["start"].type() == String) {
GeoHash start ((string) cmdObj["start"].valuestr());
@@ -1873,9 +2168,9 @@ namespace mongo {
BSONObjBuilder arr( result.subarrayStart( "results" ) );
int x = 0;
for ( GeoHopper::Holder::iterator i=gs._hopper->_points.begin(); i!=gs._hopper->_points.end(); i++ ) {
- const GeoPoint& p = *i;
- double dis = distanceMultiplier * p._distance;
+ const GeoPoint& p = *i;
+ double dis = distanceMultiplier * ((GeoPoint&) p).exactDistance();
totalDistance += dis;
BSONObjBuilder bb( arr.subobjStart( BSONObjBuilder::numStr( x++ ) ) );
View
33 db/geo/core.h
@@ -269,6 +269,10 @@ namespace mongo {
return _hash == h._hash && _bits == h._bits;
}
+ bool operator!=(const GeoHash& h ) {
+ return !( *this == h );
+ }
+
GeoHash& operator+=( const char * s ) {
unsigned pos = _bits * 2;
_bits += strlen(s) / 2;
@@ -378,9 +382,38 @@ namespace mongo {
double distance( const Point& p ) const {
double a = _x - p._x;
double b = _y - p._y;
+
+ // Avoid numerical error if possible...
+ if( a == 0 ) return abs( _y - p._y );
+ if( b == 0 ) return abs( _x - p._x );
+
return sqrt( ( a * a ) + ( b * b ) );
}
+ /**
+ * Distance method that compares x or y coords when other direction is zero,
+ * avoids numerical error when distances are very close to radius but axis-aligned.
+ *
+ * An example of the problem is:
+ * (52.0 - 51.9999) - 0.0001 = 3.31965e-15 and 52.0 - 51.9999 > 0.0001 in double arithmetic
+ * but:
+ * 51.9999 + 0.0001 <= 52.0
+ *
+ * This avoids some (but not all!) suprising results in $center queries where points are
+ * ( radius + center.x, center.y ) or vice-versa.
+ */
+ bool distanceWithin( const Point& p, double radius ) const {
+ double a = _x - p._x;
+ double b = _y - p._y;
+
+ if( a == 0 )
+ return _y > p._y ? p._y + radius >= _y : _y + radius >= p._y;
+ if( b == 0 )
+ return _x > p._x ? p._x + radius >= _x : _x + radius >= p._x;
+
+ return sqrt( ( a * a ) + ( b * b ) ) <= radius;
+ }
+
string toString() const {
StringBuilder buf(32);
buf << "(" << _x << "," << _y << ")";
View
32 jstests/geo_fiddly_box2.js
@@ -0,0 +1,32 @@
+// Reproduces simple test for SERVER-2115
+
+// The setup to reproduce is to create a set of points and a really big bounds so that we are required to do
+// exact lookups on the points to get correct results.
+
+t = db.tiles
+t.drop()
+
+t.insert( { "letter" : "S", "position" : [ -3, 0 ] } )
+t.insert( { "letter" : "C", "position" : [ -2, 0 ] } )
+t.insert( { "letter" : "R", "position" : [ -1, 0 ] } )
+t.insert( { "letter" : "A", "position" : [ 0, 0 ] } )
+t.insert( { "letter" : "B", "position" : [ 1, 0 ] } )
+t.insert( { "letter" : "B", "position" : [ 2, 0 ] } )
+t.insert( { "letter" : "L", "position" : [ 3, 0 ] } )
+t.insert( { "letter" : "E", "position" : [ 4, 0 ] } )
+
+t.ensureIndex( { position : "2d" } )
+result = t.find( { "position" : { "$within" : { "$box" : [ [ -3, -1 ], [ 0, 1 ] ] } } } )
+assert.eq( 4, result.count() )
+
+t.dropIndex( { position : "2d" } )
+t.ensureIndex( { position : "2d" }, { min : -10000000, max : 10000000 } )
+
+result = t.find( { "position" : { "$within" : { "$box" : [ [ -3, -1 ], [ 0, 1 ] ] } } } )
+assert.eq( 4, result.count() )
+
+t.dropIndex( { position : "2d" } )
+t.ensureIndex( { position : "2d" }, { min : -1000000000, max : 1000000000 } )
+
+result = t.find( { "position" : { "$within" : { "$box" : [ [ -3, -1 ], [ 0, 1 ] ] } } } )
+assert.eq( 4, result.count() )
View
85 jstests/geo_small_large.js
@@ -45,14 +45,17 @@ for ( var i = 0; i < scales.length; i++ ) {
var max = 10 * scale;
var min = -max;
var range = max - min;
-
+ var bits = 2 + Math.random() * 30
+
var t = db["geo_small_large"]
t.drop();
- t.ensureIndex( { p : "2d" }, { min : min, max : max, bits : 20 + Math.random() * 10 })
+ t.ensureIndex( { p : "2d" }, { min : min, max : max, bits : bits })
var outPoints = 0;
var inPoints = 0;
+ printjson({ eps : eps, radius : radius, max : max, min : min, range : range, bits : bits })
+
// Put a point slightly inside and outside our range
for ( var j = 0; j < 2; j++ ) {
var currRad = ( j % 2 == 0 ? radius + eps : radius - eps );
@@ -65,24 +68,84 @@ for ( var i = 0; i < scales.length; i++ ) {
assert.eq( t.count( { p : { $within : { $center : [[0, 0], radius ] } } } ), 1, "Incorrect center points found!" )
assert.eq( t.count( { p : { $within : { $box : [ [ -radius, -radius ], [ radius, radius ] ] } } } ), 1,
"Incorrect box points found!" )
-
- for ( var j = 0; j < 30; j++ ) {
-
- var x = Math.random() * ( range - eps ) + eps + min;
- var y = Math.random() * ( range - eps ) + eps + min;
+
+ shouldFind = []
+ randoms = []
+
+ for ( var j = 0; j < 2; j++ ) {
+
+ var randX = Math.random(); // randoms[j].randX
+ var randY = Math.random(); // randoms[j].randY
+
+ randoms.push({ randX : randX, randY : randY })
+
+ var x = randX * ( range - eps ) + eps + min;
+ var y = randY * ( range - eps ) + eps + min;
t.insert( { p : [ x, y ] } );
- if ( x * x + y * y > radius * radius )
+ if ( x * x + y * y > radius * radius ){
+ // print( "out point ");
+ // printjson({ x : x, y : y })
outPoints++
- else
+ }
+ else{
+ // print( "in point ");
+ // printjson({ x : x, y : y })
inPoints++
+ shouldFind.push({ x : x, y : y, radius : Math.sqrt( x * x + y * y ) })
+ }
}
-
+
+ /*
+ function printDiff( didFind, shouldFind ){
+
+ for( var i = 0; i < shouldFind.length; i++ ){
+ var beenFound = false;
+ for( var j = 0; j < didFind.length && !beenFound ; j++ ){
+ beenFound = shouldFind[i].x == didFind[j].x &&
+ shouldFind[i].y == didFind[j].y
+ }
+
+ if( !beenFound ){
+ print( "Could not find: " )
+ shouldFind[i].inRadius = ( radius - shouldFind[i].radius >= 0 )
+ printjson( shouldFind[i] )
+ }
+ }
+ }
+
+ print( "Finding random pts... ")
+ var found = t.find( { p : { $within : { $center : [[0, 0], radius ] } } } ).toArray()
+ var didFind = []
+ for( var f = 0; f < found.length; f++ ){
+ //printjson( found[f] )
+ var x = found[f].p.x != undefined ? found[f].p.x : found[f].p[0]
+ var y = found[f].p.y != undefined ? found[f].p.y : found[f].p[1]
+ didFind.push({ x : x, y : y, radius : Math.sqrt( x * x + y * y ) })
+ }
+
+ print( "Did not find but should: ")
+ printDiff( didFind, shouldFind )
+ print( "Found but should not have: ")
+ printDiff( shouldFind, didFind )
+ */
+
assert.eq( t.count( { p : { $within : { $center : [[0, 0], radius ] } } } ), 1 + inPoints,
- "Incorrect random center points found!" )
+ "Incorrect random center points found!\n" + tojson( randoms ) )
print("Found " + inPoints + " points in and " + outPoints + " points out.");
+
+ var found = t.find( { p : { $near : [0, 0], $maxDistance : radius } } ).toArray()
+ var dist = 0;
+ for( var f = 0; f < found.length; f++ ){
+ var x = found[f].p.x != undefined ? found[f].p.x : found[f].p[0]
+ var y = found[f].p.y != undefined ? found[f].p.y : found[f].p[1]
+ print( "Dist: x : " + x + " y : " + y + " dist : " + Math.sqrt( x * x + y * y) + " radius : " + radius )
+ }
+
+ assert.eq( t.count( { p : { $near : [0, 0], $maxDistance : radius } } ), 1 + inPoints,
+ "Incorrect random center points found near!\n" + tojson( randoms ) )
}
View
108 jstests/slowNightly/geo_axis_aligned.js
@@ -0,0 +1,108 @@
+// Axis aligned circles - hard-to-find precision errors possible with exact distances here
+
+t = db.axisaligned
+t.drop();
+
+scale = [ 1, 10, 1000, 10000 ]
+bits = [ 2, 3, 4, 5, 6, 7, 8, 9 ]
+radius = [ 0.0001, 0.001, 0.01, 0.1 ]
+center = [ [ 5, 52 ], [ 6, 53 ], [ 7, 54 ], [ 8, 55 ], [ 9, 56 ] ]
+
+bound = []
+for( var j = 0; j < center.length; j++ ) bound.push( [-180, 180] );
+
+// Scale all our values to test different sizes
+radii = []
+centers = []
+bounds = []
+
+for( var s = 0; s < scale.length; s++ ){
+ for ( var i = 0; i < radius.length; i++ ) {
+ radii.push( radius[i] * scale[s] )
+ }
+
+ for ( var j = 0; j < center.length; j++ ) {
+ centers.push( [ center[j][0] * scale[s], center[j][1] * scale[s] ] )
+ bounds.push( [ bound[j][0] * scale[s], bound[j][1] * scale[s] ] )
+ }
+
+}
+
+radius = radii
+center = centers
+bound = bounds
+
+
+for ( var b = 0; b < bits.length; b++ ) {
+
+
+ printjson( radius )
+ printjson( centers )
+
+ for ( var i = 0; i < radius.length; i++ ) {
+ for ( var j = 0; j < center.length; j++ ) {
+
+ printjson( { center : center[j], radius : radius[i], bits : bits[b] } );
+
+ t.drop()
+
+ // Make sure our numbers are precise enough for this test
+ if( (center[j][0] - radius[i] == center[j][0]) || (center[j][1] - radius[i] == center[j][1]) )
+ continue;
+
+ t.save( { "_id" : 1, "loc" : { "x" : center[j][0] - radius[i], "y" : center[j][1] } } );
+ t.save( { "_id" : 2, "loc" : { "x" : center[j][0], "y" : center[j][1] } } );
+ t.save( { "_id" : 3, "loc" : { "x" : center[j][0] + radius[i], "y" : center[j][1] } } );
+ t.save( { "_id" : 4, "loc" : { "x" : center[j][0], "y" : center[j][1] + radius[i] } } );
+ t.save( { "_id" : 5, "loc" : { "x" : center[j][0], "y" : center[j][1] - radius[i] } } );
+ t.save( { "_id" : 6, "loc" : { "x" : center[j][0] - radius[i], "y" : center[j][1] + radius[i] } } );
+ t.save( { "_id" : 7, "loc" : { "x" : center[j][0] + radius[i], "y" : center[j][1] + radius[i] } } );
+ t.save( { "_id" : 8, "loc" : { "x" : center[j][0] - radius[i], "y" : center[j][1] - radius[i] } } );
+ t.save( { "_id" : 9, "loc" : { "x" : center[j][0] + radius[i], "y" : center[j][1] - radius[i] } } );
+
+ t.ensureIndex( { loc : "2d" }, { max : bound[j][1], min : bound[j][0], bits : bits[b] } );
+
+ if( db.getLastError() ) continue;
+
+ print( "DOING WITHIN QUERY ")
+ r = t.find( { "loc" : { "$within" : { "$center" : [ center[j], radius[i] ] } } } );
+
+ //printjson( r.toArray() );
+
+ assert.eq( 5, r.count() );
+
+ // FIXME: surely code like this belongs in utils.js.
+ a = r.toArray();
+ x = [];
+ for ( k in a )
+ x.push( a[k]["_id"] )
+ x.sort()
+ assert.eq( [ 1, 2, 3, 4, 5 ], x );
+
+ print( " DOING NEAR QUERY ")
+ //printjson( center[j] )
+ r = t.find( { loc : { $near : center[j], $maxDistance : radius[i] } }, { _id : 1 } )
+ assert.eq( 5, r.count() );
+
+ print( " DOING DIST QUERY ")
+
+ a = db.runCommand({ geoNear : "axisaligned", near : center[j], maxDistance : radius[i] }).results
+ assert.eq( 5, a.length );
+
+ //printjson( a );
+
+ var distance = 0;
+ for( var k = 0; k < a.length; k++ ){
+ //print( a[k].dis )
+ //print( distance )
+ assert.gte( a[k].dis, distance );
+ //printjson( a[k].obj )
+ //print( distance = a[k].dis );
+ }
+
+ r = t.find( { loc : { $within : { $box : [ [ center[j][0] - radius[i], center[j][1] - radius[i] ], [ center[j][0] + radius[i], center[j][1] + radius[i] ] ] } } }, { _id : 1 } )
+ assert.eq( 9, r.count() );
+
+ }
+ }
+}

0 comments on commit 4e4a1c2

Please sign in to comment.