Search code examples
c++boostboost-geometryboost-range

How to create a Boost.Range that hides multiple layers of vectors and exposes it as a single Range?


I have a legacy class hierarchy which I can not modify. Because of requirements of an external library, I need to define Boost.Ranges for the Line and Ring, where both only expose the points in a single run (i.e. it should, both for Line and Ring, be a Boost.Range of Points).

Pseudo-code to illustrate:

Line l1 = Line{{1.0,2.0},{3.0,4.0},{5.0,6.0}} // init Line with three Points
Line l2 = Line{{7.0,8.0},{9.0,10.0},{11.0,12.0}} // init Line with three Points

auto lit = boost::begin(l1); // points to the Point{1.0,2.0}
++lit; // points to the Point{3.0,4.0}

Ring r1 = Ring{l1,l2} // init Ring with two Lines

auto rit = boost::begin(r1); // points to the Point{1.0,2.0}
++rit; // points to the Point{3.0,4.0}
++rit; // points to the Point{5.0,6.0}
++rit; // points to the Point{7.0,8.0}
++rit; // points to the Point{9.0,10.0}
// etc...

The Line is easy, since the Points are stored directly (I have done this successfully with Boost.Range see the example). However, I don't know how to do this with Ring, since I need to get to each line's points directly.

class Point 
{
  public:
  double x, y;
}

class Line
{
  public:
  std::vector<Point> points;
}

class Ring
{
  public:
  std::vector<Line> lines;
}

Solution

  • You need to extend Boost.Range so that it recognizes Ring as a valid range. But before you can do that, you need to define a custom iterator that flattens a vector< vector<T> > into a 1D range.

    This example uses Method 2 to extend Boost.Range. It also uses boost::iterator_facade to facilitate writing a custom iterator, and assumes that the iterator only needs to support forward traversal.

    #include <iostream>
    #include <vector>
    #include <boost/assign/std/vector.hpp> // for 'operator+=()'
    #include <boost/foreach.hpp>
    #include <boost/iterator/iterator_facade.hpp>
    #include <boost/range.hpp>
    
    struct Point
    {
        Point(double x, double y) : x(x), y(y) {}
        double x, y;
    };
    
    struct Line {std::vector<Point> points;};
    
    struct Ring {std::vector<Line> lines;};
    
    
    /* Custom iterator type that flattens a 2D array into a 1D array */
    template <class I, // Line iterator type
              class R  // Point reference type
             >
    class RingIteratorImpl : public boost::iterator_facade<
            RingIteratorImpl<I,R>, Point, boost::forward_traversal_tag, R>
    {
    public:
        RingIteratorImpl() : lineIter_(0), pointIndex_(0) {}
    
        explicit RingIteratorImpl(I lineIter)
        :   lineIter_(lineIter), pointIndex_(0) {}
    
    private:
        friend class boost::iterator_core_access;
    
        void increment()
        {
            ++pointIndex_;
            if (pointIndex_ >= lineIter_->points.size())
            {
                ++lineIter_;
                pointIndex_ = 0;
            }
        }
    
        bool equal(const RingIteratorImpl& other) const
        {
            return (lineIter_ == other.lineIter_) &&
                   (pointIndex_ == other.pointIndex_);
        }
    
        R dereference() const {return lineIter_->points[pointIndex_];}
    
        I lineIter_;
        size_t pointIndex_;
    };
    
    typedef RingIteratorImpl<std::vector<Line>::iterator, Point&> RingIterator;
    typedef RingIteratorImpl<std::vector<Line>::const_iterator, const Point&>
            ConstRingIterator;
    
    namespace boost
    {
        // Specialize metafunctions. We must include the range.hpp header.
        // We must open the 'boost' namespace.
    
        template <>
        struct range_mutable_iterator<Ring> { typedef RingIterator type; };
    
        template<>
        struct range_const_iterator<Ring> { typedef ConstRingIterator type; };
    
    } // namespace 'boost'
    
    
    // The required Range functions. These should be defined in the same namespace
    // as Ring.
    
    inline RingIterator range_begin(Ring& r)
        {return RingIterator(r.lines.begin());}
    
    inline ConstRingIterator range_begin(const Ring& r)
        {return ConstRingIterator(r.lines.begin());}
    
    inline RingIterator range_end(Ring& r)
        {return RingIterator(r.lines.end());}
    
    inline ConstRingIterator range_end(const Ring& r)
        {return ConstRingIterator(r.lines.end());}
    
    
    int main()
    {
        Line l1, l2;
        Ring ring;
    
        {
            using namespace boost::assign; // bring 'operator+=()' into scope
            typedef Point P;
            l1.points += P(1.1,1.2), P(1.3,1.4), P(1.5,1.6);
            l2.points += P(2.1,2.2), P(2.3,2.4), P(2.5,2.6);
            ring.lines += l1, l2;
        }
    
        // Boost Foreach treats ring as a Boost Range.
        BOOST_FOREACH(Point p, ring)
        {
            std::cout << "(" << p.x << ", " << p.y << ") ";
        }
        std::cout << "\n";
    }
    

    I get the following output:

    (1.1, 1.2) (1.3, 1.4) (1.5, 1.6) (2.1, 2.2) (2.3, 2.4) (2.5, 2.6)