0

I have template class for spatial indexing which by default should work for any 2d object which implements function void boundingBox( Rect2d * box ) using std::vector<OBJECT*> as container of objects inserted in particular grid tile.

template <class OBJECT, class TILE = std::vector<OBJECT*> >
class GridMap2D {

public:
double    step, invStep;
int       nx, ny, nxy;
TILE *    tiles;

// ==== functions

inline int getIx( double x ){ return (int)( invStep * x ); };
inline int getIy( double y ){ return (int)( invStep * y ); };

inline double getX( int ix ){ return step * ix ; };
inline double getY( int iy ){ return step * iy ; };

inline int getIndex ( int ix,   int iy   ){ return nx*iy + ix;                     };
inline int getIndex ( double x, double y ){ return getIndex( getIx(x), getIy(y) ); };
inline TILE* getTile( double x, double y ){ return  tiles + getIndex( x, y );      };

inline void insert( OBJECT* p, int i ){
    tiles[ i ].push_back( p );
}
inline void insert( OBJECT* p, int ix, int iy ){ insert( p, getIndex( ix,iy ) ); };

// this is very general method to insert any object with bounding box
// but for many object it is not very efficient
// some objects suchb as Point2d does not even implement boundingBox()
inline void insert( OBJECT* p ){
    Rect2d bbox;
    p.boundingBox( &bbox );
    int ix0 = getIx( bbox.x0 ); // TODO: bound check ?
    int iy0 = getIy( bbox.y0 );
    int ix1 = getIx( bbox.x1 );
    int iy1 = getIy( bbox.y1 );
    for( int iy=iy0; iy<=iy1; iy++ ){
        for( int ix=ix0; ix<=ix1; ix++ ){
            insert( p, ix, iy );
        }
    }
}

void init( int nx_, int ny_, double step_, int tile_n0 ){
    step    = step_;
    invStep = 1/step;
    nx = nx_; ny=ny_;
    nxy = nx*ny;
    tiles  = new TILE[ nxy ];
    for (int i=0; i<nxy; i++){
        if ( tile_n0 != 0 ){
            tiles[i].reserve( tile_n0 );
        }
    }
}

};

And its specialization for Segment2d which does not implement boundingBox() but has own insert algorithm based on line rasterization:

template<> class GridMap2D< Segment2d, std::vector<Segment2d*> >{
public:
inline void insert( Segment2d* l ){
    Vec2d* a = l->a;
    Vec2d* b = l->b;
    double ax  = a->x;
    double ay  = a->y;
    double bx  = b->x;
    double by  = b->y;

    double dx = fabs( bx - ax );
    double dy = fabs( by - ay );
    int dix = ( ax < bx ) ? 1 : -1;
    int diy = ( ay < by ) ? 1 : -1;
    int ix    = getIx( ax );
    int iy    = getIy( ay );
    int ixb   = getIx( bx );
    int iyb   = getIy( by );
    double x=0, y=0;
    int i=0;
    insert( l, ix, iy   );
    insert( l, ixb, iyb );
    while ( ( ix != ixb ) && ( iy != iyb  ) ) {
        if ( x < y ) {
            x  += dy;
            ix += dix;
        } else {
            y  += dx;
            iy += diy;
        }
        insert( l, ix, iy );
    }
}

};

which will insert line into grid tiles trough which it goes ... like this: enter image description here

but I have several problems with how templates work:

  1. In the specialization for Segment2d I got error: ‘getIx’ was not declared in this scope. Does it mean that the specialized template does not know functions defined in base template ? Or I probably do the specialization wrongly. I really do not want to rewrite the code several times, then the template approach would be pointless.

  2. I'm not sure what happen when I instantiate or specialize the template by some parameter which does not implement some methods which the base template use. e.g.

    1. consider I use different container type argument as TILE which does not implement .push_back()
    2. my Segment2d does not implement boundingBox() can does the specialization solve this problem ?

background:

I have two goals:

  1. I want to create very fast spatial indexing for acceleration of ray-casting and collisions for any 2d shape.

    • Because it should be as fast as possible I want o use templates (resolved in compile-time ) rather than some class inherience hierarchy with virtual methods.
  2. I want to learn how to use templates effectively

This question is related to this " Generic Quadtree ", where is recommendation to use templates for similar task. Tried to implement that ... but I perhaps my understanding of templates is not good enough.

NOTE: my GridMap2d is not a QuadTree, but I still added QuadTree as an keyword, because the question is relavant to it. QuadTree is very common spatial indexing data-structure, and implementing it using templates would have the same issue.

Community
  • 1
  • 1
Prokop Hapala
  • 2,424
  • 2
  • 30
  • 59
  • 1
    I think you are looking for a spatial index, not a spatial database. Indexes are for fast search in data, databases are for storing data on disk. Maybe you want to adjust the title? – TilmannZ Jan 10 '16 at 12:47

1 Answers1

1

"I really do not want to rewrite the code several times, then the template approach would be pointless."

When you specialize a class, you do not "inherit" any of the member fields or methods. So you need some special tricks here to get what you want.

What you can do instead is to essentially move the behavior of your insert() method into a separate template class. That way, when you specialize that class's behavior, you don't end up clobbering your other fields and methods. This requires some clever restructuring of your code.

This code I believe should do the job:

struct GridMap2D_base {
  double    step, invStep;
  int       nx, ny, nxy;

  int getIx( double x ) const { return (int)( invStep * x ); };
  int getIy( double y ) const { return (int)( invStep * y ); };

  double getX( int ix ) const { return step * ix ; };
  double getY( int iy ) const { return step * iy ; };

  int getIndex ( int ix,   int iy   ) const { return nx*iy + ix;                     };
  int getIndex ( double x, double y ) const { return getIndex( getIx(x), getIy(y) ); };
};

struct PushBackHelper {
  // add versions of this for std::list, etc., as needed
  template <typename OBJECT>
  static void push_back(std::vector<OBJECT*>& tile, OBJECT* p) {
    tile.push_back(p);
  }
};

template<typename OBJECT>
struct InsertAlgorithm {
  int ix0, iy0, ix1, iy1, ix, iy;

  InsertAlgorithm(const GridMap2D_base& base, OBJECT* p) {
    Rect2d bbox;
    p->boundingBox( &bbox );
    ix0 = base.getIx( bbox.x0 ); // TODO: bound check ?
    iy0 = base.getIy( bbox.y0 );
    ix1 = base.getIx( bbox.x1 );
    iy1 = base.getIy( bbox.y1 );
    ix = 0;
    iy = 0;
  }

  bool should_preinsert1(const GridMap2D_base& base, int& ix2, int& iy2) { return false; }
  bool should_preinsert2(const GridMap2D_base& base, int& ix2, int& iy2) { return false; }

  bool should_insert(const GridMap2D_base& base, int& ix2, int& iy2)
  {
    while (ix<=ix1) {
      ix2 = ix;
      iy2 = iy;
      ix++;
      return true;
    }
    iy++;
    if (iy>iy1) return false;
    ix = 0;
    return should_insert(base, ix2, iy2);
  }
};

template<> struct InsertAlgorithm<Segment2d> {
  Vec2d* a;
  Vec2d* b;
  double ax, ay, bx, by, dx, dy, x, y;
  int dix, diy, ix, iy, ixb, iyb;

  InsertAlgorithm(const GridMap2D_base& base, Segment2d* l) {
    a = l->a;
    b = l->b;
    ax  = a->x;
    ay  = a->y;
    bx  = b->x;
    by  = b->y;
    x = 0;
    y = 0;

    dx = fabs( bx - ax );
    dy = fabs( by - ay );
    dix = ( ax < bx ) ? 1 : -1;
    diy = ( ay < by ) ? 1 : -1;
    ix    = base.getIx( ax );
    iy    = base.getIy( ay );
    ixb   = base.getIx( bx );
    iyb   = base.getIy( by );
  }

  bool should_preinsert1(const GridMap2D_base& base, int& ix2, int& iy2) {
    ix2 = ix;
    iy2 = iy;
    return true;
  }

  bool should_preinsert2(const GridMap2D_base& base, int& ix2, int& iy2) {
    ix2 = ixb;
    iy2 = iyb;
    return true;
  }

  bool should_insert(const GridMap2D_base& base, int& ix2, int& iy2)
  {
    if (ix==ixb && iy==iyb) return false;

    if ( x < y ) {
      x  += dy;
      ix += dix;
    } else {
      y  += dx;
      iy += diy;
    }

    ix2 = ix;
    iy2 = iy;
    return true;
  }
};

template<class OBJECT, typename TILE=std::vector<OBJECT*> >
class GridMap2D : public GridMap2D_base {
public:
  TILE* tiles;

  TILE* getTile( double x, double y ){ return  tiles + getIndex( x, y );      };

  void insert( OBJECT* p ){
    InsertAlgorithm<OBJECT> algo(*this, p);
    int ix = 0;
    int iy = 0;
    if (algo.should_preinsert1(*this, ix, iy)) {
      PushBackHelper::push_back(tiles[getIndex(ix, iy)], p);
    }
    if (algo.should_preinsert2(*this, ix, iy)) {
      PushBackHelper::push_back(tiles[getIndex(ix, iy)], p);
    }
    while (algo.should_insert(*this, ix, iy)) {
      PushBackHelper::push_back(tiles[getIndex(ix, iy)], p);
    }
  }

  void init( int nx_, int ny_, double step_, int tile_n0 ){ ... }
};

Btw, the inline keyword has no effect when used within the class declaration.

dshin
  • 2,354
  • 19
  • 29
  • hi, I was a bit confused what actually the `should_insert()` and `should_preinsert()` do. Now I probably understand that they are used to put inserting into general scheme of `GridMap2D::insert()` conditional. However, it very much breaks readability of the *line rasterization algorithm*, So I probably don't like to fragment the insert (rasterization) algorithm like that. – Prokop Hapala Jan 11 '16 at 09:00
  • btw. do you have idea if this way ( with the more complicated logic ) has some performance advantage over using class inheriance with `virtual` functions ( which was my oroginal motivation why to use `templates` instead of `virtual` functions ) ? – Prokop Hapala Jan 11 '16 at 09:02
  • Your `insert (Segment2d*)` has 3 different calls to `insert()` - one in the main-loop, and two before it. That's why I have 3 calls to `push_back()` in my `GridMap2D::insert()` method. You should know that some of the code complexity here comes from your desire to support other containers that do not have a `push_back()` method. That forces us to separate out the logic of *what* to insert (which `InsertAlgorithm` does) vs *how* to insert (which `PushBackHelper` does). If you relax that requirement, the code would look simpler. – dshin Jan 11 '16 at 09:30
  • As far as performance goes, this should be faster than one using inheritance with `virtual` functions. How much faster depends on (1) how often you call `insert(OBJECT*)`, and (2) whether you wish to support arbitrary container types. I suspect the answer to (1) will be "not often enough to see a meaningful gain", so if readability is a big concern, it might make sense to make `insert(OBJECT*)` virtual and just use templates for support of arbitrary container types. – dshin Jan 11 '16 at 09:37