Program Listing for File convex_hull2D.hpp

Return to documentation for file (rcppsw/algorithm/convex_hull2D.hpp)

#pragma once

/*******************************************************************************
 * Includes
 ******************************************************************************/
#include <vector>
#include <list>
#include <algorithm>
#include <utility>
#include <boost/optional.hpp>

#include "rcppsw/math/math.hpp"
#include "rcppsw/er/client.hpp"
#include "rcppsw/math/vector2.hpp"

/*******************************************************************************
 * Namespaces/Decls
 ******************************************************************************/
namespace rcppsw::algorithm {

/*******************************************************************************
 * Class Definitions
 ******************************************************************************/
template<typename TCoord>
class convex_hull2D : public rer::client<convex_hull2D<TCoord>> {
 public:
  using coord_type = TCoord;
  using coord_value_type = typename coord_type::value_type;

  convex_hull2D(void) : ER_CLIENT_INIT() {}

  /* Not move/copy constructable/assignable by default */
  convex_hull2D(const convex_hull2D&) = delete;
  convex_hull2D& operator=(const convex_hull2D&) = delete;
  convex_hull2D(convex_hull2D&&) = delete;
  convex_hull2D& operator=(convex_hull2D&&) = delete;

  boost::optional<std::list<coord_type>> operator()(
      std::vector<coord_type>&& points) {
    ER_DEBUG("Calculate convex hull for %zu 2D points", points.size());
    initialize(points);

    /*
     * If two or more points make same angle with p0, Remove all but the one
     * that is farthest from p0. Points that make the same angle and are closer
     * cannot be part of the hull by definition.
     */
    size_t m = 1;
    for (size_t i = 1; i < points.size(); ++i) {
      /* Keep removing i while angle of i and i+1 is same with respect to p0 */
      while (i < points.size() - 1 &&
             orientation::ekCOLLINEAR == orientation(m_p0,
                                                     points[i],
                                                     points[i+1])) {
        ++i;
      } /* while() */

      points[m] = points[i];
      ++m;
    }
    /*
     * We could resize the vector here, but that really doesn't buy us
     * anything, because we already have moved the elements down that we want
     * to keep.
     */
    ER_DEBUG("Have %zu points/%zu total to process after filtering",
             m,
             points.size());

    /*
     * If modified array of points has less than 3 points, convex hull is not
     * possible.
     */
    if (m < 3) {
      return boost::none;
    }

    /* Create an empty stack and push first three points to it */
    std::list<coord_type> hull;
    hull.push_front(points[0]);
    hull.push_front(points[1]);
    hull.push_front(points[2]);

    /* Compute hull */
    for (size_t i = 3; i < m; ++i) {
      /* Keep removing top of stack while the angle formed by points
       * next-to-top, top, and points[i] makes a non-counterclockwise turn.
       */
      while (hull.size() > 1 &&
             orientation::ekCOUNTER_CLOCKWISE != orientation(*std::next(hull.begin()),
                                                             hull.front(),
                                                             points[i])) {
        hull.pop_front();
      } /* while(hull...) */
      hull.push_front(points[i]);
    } /* for(i..) */

    return boost::make_optional(hull);
  } /* operator() */

 private:
  enum orientation {
    ekCOLLINEAR,
    ekCLOCKWISE,
    ekCOUNTER_CLOCKWISE
  };

  orientation orientation(const coord_type& p_in,
                          const coord_type& q_in,
                          const coord_type& r_in) {
    /*
     * We copy each of the vectors into vector2d so that the orientation is
     * correct for unsigned vectors (e.g., vector2z). If the coord_type is
     * already signed, then this is unecessary, but does not hurt. The compiler
     * will probably optimize it away in such cases.
     */
    rmath::vector2d p(p_in.x(), p_in.y());
    rmath::vector2d q(q_in.x(), q_in.y());
    rmath::vector2d r(r_in.x(), r_in.y());

    auto val = (q.y() - p.y()) * (r.x() - q.x()) -
               (q.x() - p.x()) * (r.y() - q.y());
    if (val > 0) { /* clockwise */
      return orientation::ekCLOCKWISE;
    } else if (val < 0) {  /* counterclockwise */
      return orientation::ekCOUNTER_CLOCKWISE;
    } else {  /* collinear */
      return orientation::ekCOLLINEAR;
    }
  }

  void initialize(std::vector<coord_type>& points) {
    /*
     * First, bind the bottommost point in Y. If there are multiple points with
     * the same minimum in Y, sort by X.
     */
    coord_value_type ymin = points[0].y();
    size_t min = 0;
    for (size_t i = 1; i < points.size(); ++i) {
      auto y = points[i].y();

      if ((y < ymin) ||
          (rmath::is_equal(ymin, y) && points[i].x() < points[min].x())) {
        ymin = points[i].y();
        min = i;
      }
    } /* for(i..) */

    /* Smallest point goes at index 0 */
    std::swap(points[0], points[min]);

    /*
     * Sort all points with respect to the smallest point by polar angle,
     * measured counterclockwise. If there are ties in angle (collinear), then
     * sort by ascending distance from the smallest point.
     */
    m_p0 = points[0];
    std::sort(points.begin() + 1,
              points.end(),
              [this](const coord_type& lhs, const coord_type& rhs) {
                auto o = orientation(m_p0, lhs, rhs);
                if (orientation::ekCOLLINEAR == o) {
                  return (m_p0 - rhs).square_length() >= (m_p0 - lhs).square_length();
                }

                return (orientation::ekCLOCKWISE != o);
              });
  }

  /* clang-format off */
  coord_type m_p0{};
  /* clang-format on */
};

} /* namespace rcppsw::algorithm */