/* +------------------------------------------------------------------------+
   |                     Mobile Robot Programming Toolkit (MRPT)            |
   |                          https://www.mrpt.org/                         |
   |                                                                        |
   | Copyright (c) 2005-2024, Individual contributors, see AUTHORS file     |
   | See: https://www.mrpt.org/Authors - All rights reserved.               |
   | Released under BSD License. See: https://www.mrpt.org/License          |
   +------------------------------------------------------------------------+ */

#pragma once

#include <mrpt/config/CConfigFileBase.h>
#include <mrpt/config/CLoadableOptions.h>
#include <mrpt/graphs/CHypothesisNotFoundException.h>
#include <mrpt/graphs/THypothesis.h>
#include <mrpt/graphslam/interfaces/CRangeScanEdgeRegistrationDecider.h>
#include <mrpt/graphslam/misc/TNodeProps.h>
#include <mrpt/graphslam/misc/TSlidingWindow.h>
#include <mrpt/graphslam/misc/TUncertaintyPath.h>
#include <mrpt/img/TColor.h>
#include <mrpt/math/CMatrixF.h>
#include <mrpt/obs/CActionCollection.h>
#include <mrpt/obs/CObservation2DRangeScan.h>
#include <mrpt/obs/CSensoryFrame.h>
#include <mrpt/slam/CICP.h>
#include <mrpt/slam/CIncrementalMapPartitioner.h>

#include <map>
#include <set>
#include <string>
#include <utility>
#include <vector>

namespace mrpt::graphslam::deciders
{
/**\brief Edge Registration Decider scheme specialized in Loop Closing.
 *
 * ## Description
 *
 * Current decider is implemented based on the following papers:
 *
 * - [1] <a
 *
href="http://ieeexplore.ieee.org/xpl/login.jsp?tp=&arnumber=1641810&url=http%3A%2F%2Fieeexplore.ieee.org%2Fxpls%2Fabs_all.jsp%3Farnumber%3D1641810">Consistent
 * Observation Grouping for Generating Metric-Topological Maps that Improves
 * Robot Localization</a> - J. Blanco, J. Gonzalez, J. Antonio Fernandez
 * Madrigal, 2006
 * - [2] <a
href="https://april.eecs.umich.edu/pdfs/olson2009ras.pdf">Recognizing
 * places using spectrally clustered local matches</a> - E. Olson, 2009
 *
 * ### Specifications
 *
 * - Map type: 2D
 * - MRPT rawlog format: #1, #2
 * - Graph Type: CPosePDFGaussianInf
 * - Observations: CObservation2DRangeScan
 * - Edge Registration Strategy: ICP Scan-matching between nearby nodes
 * - Loop Registration Strategy: Pair-wise Consistency of ICP Edges
 *
 * ### Loop Closing Strategy
 *
 * The Loop closure registration strategy is described below:
 *
 * - We split the graph under-construction into groups of nodes. The groups are
 *   formatted based on the observations gathered in each node position. The
 *   actual split between the groups is decided by the minimum normalized Cut
 *   (minNcut) as described in [1].
 * - Having assembled the groups of nodes, we find the groups that might
 *   contain loop closure edges (these groups contain successive nodes with
large
 *   difference in their IDs). These groups are then split into two subgroups
 *   A, B, with the former containing the lower NodeIDs and the latter the
 *   higher. To minimize computational cost as well as the possibility of wrong
 *   loop closure registration, we search for loop closures only between the
 *   <a>last</a> nodes of group B and the <a>first</a> nodes of group A. Based
on [2] the
 *   potential loop closure edges are not evaluated individually but rather in
 *   sets. Refer to [2] for insight on the algorithm and to
 *   evaluatePartitionsForLC method for the actual implementation. See below
 *   for images of potential Loop closure edges.
 *
 * <div>
 *   <div>
 *     <img src="graphslam-engine_loop_closing_full.png">
 *     <caption align="bottom">
 *     Loop closing schematic. blue nodes belong to group A and have lower
 *     NodeIDs, while red nodes belong to group B and have higher NodeIDs.
 *     Nodes of both groups A and B belong to the same partition based on their
 *     corresponding 2DRangeScan observations.
 *     </caption>
 *   </div>
 *   <div>
 *     <img src="graphslam-engine_loop_closing_consistency_element.png">
 *     <caption align="bottom">
 *     Rigid body transformation of hypotheses and two corresponding
 *     dijkstra links
 *     </caption>
 *   </div>
 * </div>
 *
 * \note
 * Olson uses the following formula for evaluating the pairwise
 * consistency between two hypotheses i,j:
 * <br><center> \f$ A_{i,j} = e^{T \Sigma_T^{-1} T^T} \f$ </center>
 *
 * \note
 * Where:
 * - T is the rigid body transformation using the two hypotheses and the two
 * Dijkstra Links connecting the nodes that form the hypotheses
 * - \f$ \Sigma_T \f$ is the covariance matrix of the aforementioned rigid
 * body transformation
 *
 * \note
 * However this formula is <a>inconsistent with the rest of the paper
 * explanations and mathematical formulas </a>:
 * - The author states that:
 *   \verbatim
"This quantity is proportional to the probability density that the rigid-body
transformation is the identity matrix (i.e., T = [0 0 0])"
\endverbatim
 *   This is \a inconsistent with the given formula. Suppose that a
 *   wrong loop closure is included in the \f$ A_{i,j} \f$, therefore the
 *   pairwise-consistency element should have a low value. For this to hold
 *   true the exponent of the consistency element shoud be small and
 *   neglecting the covariance matrix of rigid-body transformation  (e.g. unit
 *   covariance matrix), \f$ T T^T \f$ should be small.
 *   When a wrong loop closure is evaluated the aforementioned quantity
 *   increases since the hypotheses do not form a correct loop. Therefore the
 *   worse the rigid-body transformation the higher the exponent term,
 *   therefore the higher the consistency element
 * - Author uses the information matrix \f$ \Sigma_T^{-1} \f$ in the
exponential.
 *   However in the optimal case (high certainty of two correct loop closure
 *   hypotheses) information matrix and rigid body transformation vector T
 *   have opposite effects in the exponent term:
 *   - \f$ \text{Correct loop closure} \Rightarrow T \rightarrow [0, 0, 0]
 *   \Rightarrow \text{exponent} \downarrow \f$
 *   - \f$ \text{Correct loop closure} \Rightarrow
 *   \text{diagonal\_terms}(\Sigma_T^{-1}) \uparrow \Rightarrow \text{exponent}
 *   \uparrow \f$
 *
 * \note
 * Based on the previous comments the following formula is used in the decider:
 * <br><center> \f$ A_{i,j} = e^{-T \Sigma_T T^T} \f$ </center>
 *
 * ### .ini Configuration Parameters
 *
 * \htmlinclude graphslam-engine_config_params_preamble.txt
 *
 * - \b class_verbosity
 *   + \a Section       : EdgeRegistrationDeciderParameters
 *   + \a Default value : 1 (mrpt::system::LVL_INFO)
 *   + \a Required      : FALSE
 *
 * - \b use_scan_matching
 *   + \a Section       : EdgeRegistrationDeciderParameters
 *   + \a Default value : TRUE
 *   + \a Required      : FALSE
 *   + \a Description   :Indicates whether the decider uses scan matching
 *   between the current and previous laser scans to correct the robot
 *   trajectory.
 *
 * - \b visualize_laser_scans
 *   + \a Section       : VisualizationParameters
 *   + \a Default value : 10
 *   + \a Required      : FALSE
 *
 * - \b LC_min_nodeid_diff
 *  + \a Section       : GeneralConfiguration
 *  + \a Default value : 30
 *  + \a Required      : FALSE
 *  + \a Description   : Minimum NodeID difference for an edge to be considered
 *  a loop closure.
 *
 * - \b LC_min_remote_nodes
 *   + \a Section       : EdgeRegistrationDeciderParameters
 *   + \a Default value : 3
 *   + \a Required      : FALSE
 *   + \a Description   : Number of remote nodes that must exist in order for
 *   the Loop Closure procedure to commence
 *
 * - \b LC_eigenvalues_ratio_thresh
 *   + \a Section       : EdgeRegistrationDeciderParameters
 *   + \a Default value : 2
 *   + \a Required      : FALSE
 *   + \a Description   : Minimum ratio of the two dominant eigenvalues for a
 *   loop closing hypotheses set to be considered valid
 *
 * - \b LC_check_curr_partition_only
 *   + \a Section       : EdgeRegistrationDeciderParameters
 *   + \a Default value : TRUE
 *   + \a Required      : FALSE
 *   + \a Description   : Boolean flag indicating whether to check for loop
 *   closures only in the current node's partition
 *
 * - \b visualize_map_partitions
 *   + \a Section       : VisualizationParameters
 *   + \a Default value : TRUE
 *   + \a Required      : FALSE
  *
 * \note Class contains an instance of the
 * mrpt::slam::CIncrementalMapPartitioner class and it parses the configuration
 * parameters of the latter from the "EdgeRegistrationDeciderParameters"
 * section. Refer to mrpt::slam::CIncrementalMapPartitioner documentation for
 * its list of configuration parameters
 *
 * \sa mrpt::slam::CIncrementalMapPartitioner
 * \ingroup mrpt_graphslam_grp
 */
template <class GRAPH_T = typename mrpt::graphs::CNetworkOfPoses2DInf>
class CLoopCloserERD :
    public virtual mrpt::graphslam::deciders::CRangeScanEdgeRegistrationDecider<GRAPH_T>
{
 public:
  /**\brief Edge Registration Decider */
  using parent_t = CRangeScanEdgeRegistrationDecider<GRAPH_T>;

  using constraint_t = typename GRAPH_T::constraint_t;
  using pose_t = typename GRAPH_T::constraint_t::type_value;
  using global_pose_t = typename GRAPH_T::global_pose_t;
  using decider_t = CLoopCloserERD<GRAPH_T>; /**< self type */
  using range_ops_t = typename parent_t::range_ops_t;
  using nodes_to_scans2D_t = typename parent_t::nodes_to_scans2D_t;
  using partitions_t = std::vector<std::vector<uint32_t>>;
  using edges_citerator = typename GRAPH_T::edges_map_t::const_iterator;
  using edges_iterator = typename GRAPH_T::edges_map_t::iterator;
  using hypot_t = typename mrpt::graphs::detail::THypothesis<GRAPH_T>;
  using hypots_t = std::vector<hypot_t>;
  using hypotsp_t = std::vector<hypot_t*>;
  using hypotsp_to_consist_t = std::map<std::pair<hypot_t*, hypot_t*>, double>;
  using path_t = mrpt::graphslam::TUncertaintyPath<GRAPH_T>;
  using paths_t = std::vector<path_t>;
  using node_props_t = mrpt::graphslam::detail::TNodeProps<GRAPH_T>;
  /**\}*/

  CLoopCloserERD();
  ~CLoopCloserERD() override;

  bool updateState(
      mrpt::obs::CActionCollection::Ptr action,
      mrpt::obs::CSensoryFrame::Ptr observations,
      mrpt::obs::CObservation::Ptr observation) override;

  void setWindowManagerPtr(mrpt::graphslam::CWindowManager* win_manager) override;
  void notifyOfWindowEvents(const std::map<std::string, bool>& events_occurred) override;
  void getEdgesStats(std::map<std::string, int>* edge_types_to_num) const override;

  void initializeVisuals() override;
  void updateVisuals() override;
  void loadParams(const std::string& source_fname) override;
  void printParams() const override;

  void getDescriptiveReport(std::string* report_str) const override;
  void getCurrentPartitions(partitions_t& partitions_out) const;
  const partitions_t& getCurrentPartitions() const;
  /**\brief Return the minimum number of nodes that should exist in the graph
   * prior to running Dijkstra
   */
  size_t getDijkstraExecutionThresh() const { return m_dijkstra_node_count_thresh; }
  void setDijkstraExecutionThresh(size_t new_thresh) { m_dijkstra_node_count_thresh = new_thresh; }

  /**\name Helper structs */
  /**\{ */

  /**
   * \brief Struct for passing additional parameters to the getICPEdge call
   *
   * Handy for overriding the search to the \a GRAPH_T::nodes map or the
   * search for the node's LaserScan
   */
  struct TGetICPEdgeAdParams
  {
    using self_t = TGetICPEdgeAdParams;

    node_props_t from_params; /**< Ad. params for the from_node */
    node_props_t to_params;   /**< Ad. params for the to_node */
    pose_t init_estim;        /**< Initial ICP estimation */

    void getAsString(std::string* str) const
    {
      str->clear();
      *str += mrpt::format("from_params: %s", from_params.getAsString().c_str());
      *str += mrpt::format("to_params: %s", to_params.getAsString().c_str());
      *str += mrpt::format("init_estim: %s\n", init_estim.asString().c_str());
    }
    std::string getAsString() const
    {
      std::string str;
      this->getAsString(&str);
      return str;
    }
    friend std::ostream& operator<<(std::ostream& o, const self_t& params)
    {
      o << params.getAsString() << endl;
      return o;
    }
  };
  /**\brief Struct for passing additional parameters to the
   * generateHypotsPool call
   */
  struct TGenerateHypotsPoolAdParams
  {
    using group_t = std::map<mrpt::graphs::TNodeID, node_props_t>;

    /**\brief  Ad. params for groupA */
    group_t groupA_params;
    /**\brief  Ad. params for groupB */
    group_t groupB_params;
  };
  /**\} */

  /**\brief Generate the hypothesis pool for all the inter-group constraints
   * between two groups of nodes.
   *
   * \param[in] groupA First group to be tested
   * \param[in] groupB Second group to be tested
   * \param[out] generated_hypots Pool of generated hypothesis. Hypotheses
   * are generated in the heap, so the caller is responsible of afterwards
   * calling \a delete.
   */
  void generateHypotsPool(
      const std::vector<uint32_t>& groupA,
      const std::vector<uint32_t>& groupB,
      hypotsp_t* generated_hypots,
      const TGenerateHypotsPoolAdParams* ad_params = nullptr);
  /**\brief Compute the pair-wise consistencies Matrix.
   *
   * \param[in] groupA First group to be used
   * \param[in] groupB Second group to be used
   * \param[in] hypots_pool Pool of hypothesis that has been generated
   * between the two groups
   * \pram[out] consist_matrix Pointer to Pair-wise consistencies matrix that
   * is to be filled

   * \param[in] groupA_opt_paths Pointer to vector of optimal paths that can
   * be used instead of making queries to the m_node_optimal_paths class
   * vector. See corresponding argument in generatePWConsistencyElement
   * method
   * \param[in] groupB_opt_paths
   *
   * \sa generatePWConsistencyElement
   * \sa evalPWConsistenciesMatrix
   */
  void generatePWConsistenciesMatrix(
      const std::vector<uint32_t>& groupA,
      const std::vector<uint32_t>& groupB,
      const hypotsp_t& hypots_pool,
      mrpt::math::CMatrixDouble* consist_matrix,
      const paths_t* groupA_opt_paths = nullptr,
      const paths_t* groupB_opt_paths = nullptr);
  /**\brief Evalute the consistencies matrix, fill the valid hypotheses
   *
   * Call to this method should be made right after generating the
   * consistencies matrix using the generatePWConsistenciesMatrix method
   *
   * \sa generatePWConsistenciesMatrix
   */
  void evalPWConsistenciesMatrix(
      const mrpt::math::CMatrixDouble& consist_matrix,
      const hypotsp_t& hypots_pool,
      hypotsp_t* valid_hypots);
  // Public variables
  // ////////////////////////////
 protected:
  // protected functions
  //////////////////////////////////////////////////////////////

  /**\brief Fill the TNodeProps instance using the parameters from the map
   *
   * \param[in] nodeID ID of node corresponding to the TNodeProps struct that
   * is to be filled
   * \param[in] group_params Map of TNodeID to corresponding TNodeProps
   * instance.
   * \param[out] node_props Pointer to the TNodeProps struct to be filled.
   *
   *
   * \return True if operation was successful, false otherwise.
   */
  bool fillNodePropsFromGroupParams(
      const mrpt::graphs::TNodeID& nodeID,
      const std::map<mrpt::graphs::TNodeID, node_props_t>& group_params,
      node_props_t* node_props);
  /**\brief Fill the pose and LaserScan for the given nodeID.
   * Pose and LaserScan are either fetched from the TNodeProps struct if it
   * contains valid data, otherwise from the corresponding class vars
   *
   * \return True if operation was successful and pose, scan contain valid
   * data.
   */
  bool getPropsOfNodeID(
      const mrpt::graphs::TNodeID& nodeID,
      global_pose_t* pose,
      mrpt::obs::CObservation2DRangeScan::Ptr& scan,
      const node_props_t* node_props = nullptr) const;

  /**\brief Struct for storing together the parameters needed for ICP
   * matching, laser scans visualization etc.
   */
  struct TLaserParams : public mrpt::config::CLoadableOptions
  {
   public:
    TLaserParams();
    ~TLaserParams() override;

    void loadFromConfigFile(
        const mrpt::config::CConfigFileBase& source, const std::string& section) override;
    void dumpToTextStream(std::ostream& out) const override;
    mrpt::slam::CICP icp;
    /**\brief How many nodes back to check ICP against?
     */
    int prev_nodes_for_ICP;

    /** see Constructor for initialization */
    const mrpt::img::TColor laser_scans_color = mrpt::img::TColor(0, 20, 255);
    bool visualize_laser_scans;
    // keystroke to be used by the user to toggle the LaserScans from
    // the CDisplayWindow
    std::string keystroke_laser_scans = "l";

    /**\brief Indicate whethet to use scan-matching at all during
     * graphSLAM [on by default].
     *
     * \warning It is strongly recommended that the user does not set this
     * to false (via the .ini file). graphSLAM may diverge significantly if
     * no scan-matching is not used.
     */
    bool use_scan_matching;
    bool has_read_config = false;
    ;
    /**\brief Keep track of the mahalanobis distance between the initial
     * pose
     * difference and the suggested new edge for the pairs of checked
     * nodes.
     */
    TSlidingWindow mahal_distance_ICP_odom_win;
    /**\brief Keep track of ICP Goodness values for ICP between nearby
     * nodes and adapt the Goodness threshold based on the median of the
     * recorded Goodness values.
     */
    TSlidingWindow goodness_threshold_win;
  };

  /**\brief Struct for storing together the loop-closing related parameters.
   */
  struct TLoopClosureParams : public mrpt::config::CLoadableOptions
  {
   public:
    TLoopClosureParams();
    ~TLoopClosureParams() override;

    void loadFromConfigFile(
        const mrpt::config::CConfigFileBase& source, const std::string& section) override;
    void dumpToTextStream(std::ostream& out) const override;
    /**\brief flag indicating whether to check only the partition of the
     * last
     * registered node for potential loop closures
     */
    bool LC_check_curr_partition_only;
    /**\brief nodeID difference for detecting potential loop closure in a
     * partition.
     *
     * If this difference is surpassed then the partition should be
     * investigated for loop closures using Olson's strategy.
     */
    size_t LC_min_nodeid_diff;
    /**\brief Eigenvalues ratio for accepting/rejecting a hypothesis set.
     *
     * By default this is set to 2.
     */
    double LC_eigenvalues_ratio_thresh;
    /**\brief how many remote nodes (large nodID difference should there be
     * before I consider the potential loop closure.
     */
    int LC_min_remote_nodes;
    /**\brief Full partition of map only afer X new nodes have been
     * registered
     */
    int full_partition_per_nodes;
    bool visualize_map_partitions;
    std::string keystroke_map_partitions;

    double offset_y_map_partitions;
    int text_index_map_partitions;

    // map partitioning  - visualization window parameters
    const double balloon_elevation{3};
    const double balloon_radius{0.5};
    const mrpt::img::TColor balloon_std_color;
    const mrpt::img::TColor balloon_curr_color;
    const mrpt::img::TColor connecting_lines_color;

    bool has_read_config{false};
  };
  TLaserParams m_laser_params;
  TLoopClosureParams m_lc_params;

  /**brief Compare the suggested ICP edge against the initial node
   * difference.
   *
   * If this difference is significantly larger than the rest of of the
   * recorded mahalanobis distances, reject the suggested ICP edge.
   *
   * \return True if suggested ICP edge is accepted
   * \note Method updates the Mahalanobis Distance TSlidingWindow which
   * keep track of the recorded mahalanobis distance values.
   * \sa getICPEdge
   */
  bool mahalanobisDistanceOdometryToICPEdge(
      const mrpt::graphs::TNodeID& from,
      const mrpt::graphs::TNodeID& to,
      const constraint_t& rel_edge);
  /**\brief Wrapper around the registerNewEdge method which accepts a
   * THypothesis object instead.
   */
  void registerHypothesis(const hypot_t& h);
  void registerNewEdge(
      const mrpt::graphs::TNodeID& from,
      const mrpt::graphs::TNodeID& to,
      const constraint_t& rel_edge) override;
  /**\brief Fetch a list of nodes with regards prior to the given nodeID for
   * which to try and add scan matching edges
   *
   * \sa addScanMatchingEdges
   */
  virtual void fetchNodeIDsForScanMatching(
      const mrpt::graphs::TNodeID& curr_nodeID, std::set<mrpt::graphs::TNodeID>* nodes_set);
  /**\brief Addd ICP constraints from X previous nodeIDs up to the given
   * nodeID.
   *
   * X is set by the user in the .ini configuration file (see
   * TLaserParams::prev_nodes_for_ICP)
   *
   * \sa fetchNodeIDsForScanMatching
   */
  virtual void addScanMatchingEdges(const mrpt::graphs::TNodeID& curr_nodeID);
  void initLaserScansVisualization();
  void updateLaserScansVisualization();
  /**\brief togle the LaserScans visualization on and off
   */
  void toggleLaserScansVisualization();
  void dumpVisibilityErrorMsg(std::string viz_flag, int sleep_time = 500 /* ms */);
  /**\brief Split the currently registered graph nodes into partitions.  */
  void updateMapPartitions(bool full_update = false, bool is_first_time_node_reg = false);
  /**\brief Initialize the visualization of the map partition objects. */
  void initMapPartitionsVisualization();
  /**\brief Update the map partitions visualization. */
  void updateMapPartitionsVisualization();
  /**\brief Toggle the map partitions visualization objects.
   *
   * To be called  upon relevant keystroke press by the user (see \b
   * TLoopClosureParams::keystroke_map_partitions)
   */
  void toggleMapPartitionsVisualization();
  void initCurrCovarianceVisualization();
  void updateCurrCovarianceVisualization();
  /**\brief Compute the Centroid of a group of a vector of node positions.
   *
   * \param[in] nodes_list List of node IDs whose positions are taken into
   * account
   * \param[out] centroid_coords Contains the Centroid coordinates as a pair
   * [x,y]
   *
   * \note Method is used during the visualization of the map partitions.
   */
  void computeCentroidOfNodesVector(
      const std::vector<uint32_t>& nodes_list, std::pair<double, double>* centroid_coords) const;
  /**\brief Check the registered so far partitions for potential loop
   * closures.
   *
   * Practically checks whether there exist nodes in a single partition whose
   * distance surpasses the minimum loop closure nodeID distance. The latter
   * is read from a .ini external file, thus specified by the user (see \b
   * TLoopClosureParams.LC_min_nodeid_diff.
   *
   * \sa evaluatePartitionsForLC
   */
  void checkPartitionsForLC(partitions_t* partitions_for_LC);
  /**\brief Evaluate the given partitions for loop closures.
   *
   * Call this method when you have identified potential loop closures - e.g.
   * far away nodes in the same partitions - and you want to evaluate the
   * potential hypotheses in the group. Comprises the main function that
   * tests potential loop closures in <b>partitions of nodes</b>
   *
   * \sa checkPartitionsForLC
   */
  void evaluatePartitionsForLC(const partitions_t& partitions);

  bool computeDominantEigenVector(
      const mrpt::math::CMatrixDouble& consist_matrix,
      mrpt::math::CVectorDouble* eigvec,
      bool use_power_method = false);
  /**\brief Return the pair-wise consistency between the observations of the
   * given nodes.
   *
   * For the tranformation matrix of the loop use the following edges
   * - a1=>a2 (Dijkstra Link)
   * - a2=>b1 (hypothesis - ICP edge)
   * - b1=>b2 (Dijkstra Link)
   * - b2=>a1 (hypothesis - ICP edge)
   *
   * Given the transformation vector \f$ (x,y,\phi)\f$ of the above
   * composition (e.g. T) the
   * pairwise consistency element would then be:
   * <br><center> \f$ A_{i,j} = e^{-T \Sigma_T T^T} \f$ </center>
   *
   * \param[in] hypots Hypothesis corresponding to the potential inter-group
   * constraints
   * \param[in] opt_paths Vector of optimal paths that can be used instead of
   * making queries to the m_node_optimal_paths class vector. See
   * corresponding argument in generatePWConsistenciesMatrix method
   * - 1st element \rightarrow a1->a2 path
   * - 2nd element \rightarrow b1->b2 path
   *
   * \return Pairwise consistency eleement of the composition of
   * transformations
   *
   * \sa generatePWConsistenciesMatrix
   */
  double generatePWConsistencyElement(
      const mrpt::graphs::TNodeID& a1,
      const mrpt::graphs::TNodeID& a2,
      const mrpt::graphs::TNodeID& b1,
      const mrpt::graphs::TNodeID& b2,
      const hypotsp_t& hypots,
      const paths_t* opt_paths = nullptr);
  /**\brief Given a vector of THypothesis objects, find the one that
   * has the given start and end nodes.
   *
   * \note If multiple hypothesis between the same start and end node exist,
   * only the first one is returned.
   *
   * \param[in] vec_hypots Vector of hypothesis to check
   * \param[in] from Starting Node for hypothesis
   * \param[in] to Ending Node for hypothesis
   * \param[in] throw_exc If true and hypothesis is not found, <b>throw a
   * HypothesisNotFoundException</b>
   *
   * \return Pointer to the found hypothesis if that is found, otherwise
   * nullptr.
   *
   */
  static hypot_t* findHypotByEnds(
      const hypotsp_t& vec_hypots,
      const mrpt::graphs::TNodeID& from,
      const mrpt::graphs::TNodeID& to,
      bool throw_exc = true);
  /**\brief Given a vector of TUncertaintyPath objects, find the one that has
   * the given source and destination nodeIDs.
   *
   * \note If multiple paths between the same start and end node exist,
   * only the first one is returned.
   *
   * \return nullptr if a path with the given source and destination NodeIDs
   * is not found, otherwise a pointer to the matching TUncertaintyPath.
   *
   * \exception std::runtime_error if path was not found and throw_exc is set
   * to true
   */
  static const path_t* findPathByEnds(
      const paths_t& vec_paths,
      const mrpt::graphs::TNodeID& src,
      const mrpt::graphs::TNodeID& dst,
      bool throw_exc = true);
  /**\brief Given a vector of THypothesis objects, find the one that
   * has the given ID.
   *
   * \note If multiple hypothesis with the same ID exist, only the first one
   * is returned.
   *
   * \param[in] vec_hypots Vector of hypothesis to check
   * \param[in] id, ID of the hypothesis to be returned
   * \param[in] throw_exc If true and hypothesis is not found, <b>throw a
   * HypothesisNotFoundException</b>
   *
   * \return Pointer to the hypothesis with the given ID if that is found,
   * otherwies nullptr.
   */
  static hypot_t* findHypotByID(const hypotsp_t& vec_hypots, size_t id, bool throw_exc = true);
  /**\brief Get the ICP Edge between the provided nodes.
   *
   * Handy for not having to manually fetch the laser scans, as the method
   * takes care of this.
   *
   * \param[out] icp_info Struct that will be filled with the results of the
   * ICP operation
   *
   * \param[in] ad_params Pointer to additional parameters in the getICPEdge
   * call
   *
   * \return True if operation was successful, false otherwise (e.g. if the
   * either of the nodes' CObservation2DRangeScan object does not contain
   * valid data.
   */
  virtual bool getICPEdge(
      const mrpt::graphs::TNodeID& from,
      const mrpt::graphs::TNodeID& to,
      constraint_t* rel_edge,
      mrpt::slam::CICP::TReturnInfo* icp_info = nullptr,
      const TGetICPEdgeAdParams* ad_params = nullptr);
  /**\brief compute the minimum uncertainty of each node position with
   * regards to the graph root.
   *
   * \param[in] starting_node Node from which I start the Dijkstra projection
   * algorithm
   * \param[in] ending_node Specify the nodeID whose uncertainty wrt the
   * starting_node, we are interested in computing. If given, method
   * execution ends when this path is computed.
   */
  void execDijkstraProjection(
      mrpt::graphs::TNodeID starting_node = 0,
      mrpt::graphs::TNodeID ending_node = mrpt::graphs::INVALID_NODEID);
  /**\brief Given two nodeIDs compute and return the path connecting them.
   *
   * Method takes care of multiple edges, as well as edges with 0 covariance
   * matrices
   */
  void getMinUncertaintyPath(
      const mrpt::graphs::TNodeID from, const mrpt::graphs::TNodeID to, path_t* path) const;
  /**\brief Find the minimum uncertainty path from te given pool of
   * TUncertaintyPath instances.
   *
   * Removes (and returns) the found path from the pool.
   *
   * \return Minimum uncertainty path from the pool provided
   */
  typename mrpt::graphslam::TUncertaintyPath<GRAPH_T>* popMinUncertaintyPath(
      std::set<path_t*>* pool_of_paths) const;
  /**\brief  Append the paths starting from the current node.
   *
   * \param[in] pool_of_paths Paths that are currently registered
   * \param[in] curr_path Path that I am currently traversing. This path is
   * already removed from \a pool_of_paths
   * \param[in] neighbors std::set of neighboring nodes to the last node of
   * the current path
   */
  void addToPaths(
      std::set<path_t*>* pool_of_paths,
      const path_t& curr_path,
      const std::set<mrpt::graphs::TNodeID>& neibors) const;
  /**\brief Query for the optimal path of a nodeID.
   *
   * Method handles calls to out-of-bounds nodes as well as nodes whose paths
   * have not yet been computed.
   *
   * \param[in] node nodeID for which hte path is going to be returned
   *
   * \return Optimal path corresponding to the given nodeID or nullptr if the
   * former is not found.
   */
  typename mrpt::graphslam::TUncertaintyPath<GRAPH_T>* queryOptimalPath(
      const mrpt::graphs::TNodeID node) const;
  /**\brief Split an existing partition to Groups
   *
   *	 Have two groups A, B.
   *	 - Group A consists of the lower nodeIDs. They correspond to the start
   *	 of the course
   *	 - Group B consists of the higher (more recent) nodeIDs. They
   *	 correspond to the end of the course find where to split the current
   *	 partition
   *
   *	 \note Method is used in single-robot graphSLAM for splitting a
   *	 partition of nodes to lower and higher node IDs
   *
   *	 \param[in] partition Partition to be split.
   *	 \param[out] groupA First group of nodes.
   *	 \param[out] groupB Second group of nodes.
   *	 \param[in] max_nodes_in_group Max number of nodes that are to exist in
   *	 each group (Use -1 to disable this threshold).
   */
  void splitPartitionToGroups(
      std::vector<uint32_t>& partition,
      std::vector<uint32_t>* groupA,
      std::vector<uint32_t>* groupB,
      int max_nodes_in_group = 5);
  /**\brief Assign the last recorded 2D Laser scan
   *
   * \note Compact way of assigning the last recorded laser scan for both
   * MRPT rawlog formats.
   *
   * Method takes into account the start of graphSLAM proc. when two nodes
   * are added at the graph at the same time (root + node for 1st constraint)
   */
  void setLastLaserScan2D(mrpt::obs::CObservation2DRangeScan::Ptr scan);

  /**\brief Instance responsible for partitioning the map */
  mrpt::slam::CIncrementalMapPartitioner m_partitioner;

  bool m_visualize_curr_node_covariance = false;
  const mrpt::img::TColor m_curr_node_covariance_color = mrpt::img::TColor(160, 160, 160, 255);
  double m_offset_y_curr_node_covariance;
  int m_text_index_curr_node_covariance;

  /**\brief Keep track of the registered edge types.
   *
   * Handy for displaying them in the Visualization window.
   */
  std::map<std::string, int> m_edge_types_to_nums;
  /**\brief Keep the last laser scan for visualization purposes */
  mrpt::obs::CObservation2DRangeScan::Ptr m_last_laser_scan2D;
  /**\name Partition vectors */
  /**\{ */
  /**\brief Previous partitions vector */
  partitions_t m_last_partitions;
  /**\brief Current partitions vector */
  partitions_t m_curr_partitions;
  /**\} */
  /**\brief Indicate whether the partitions have been updated recently */
  bool m_partitions_full_update = false;
  /**\brief Keep track of the evaluated partitions so they are not checked
   * again if nothing changed in them.
   */
  std::map<int, std::vector<uint32_t>> m_partitionID_to_prev_nodes_list;
  /**\brief Map that stores the lowest uncertainty path towards a node.
   * Starting node depends on the starting node as used in the
   * execDijkstraProjection method
   */
  typename std::map<mrpt::graphs::TNodeID, path_t*> m_node_optimal_paths;
  /**\brief Keep track of the first recorded laser scan so that it can be
   * assigned to the root node when the NRD adds the first *two* nodes to the
   * graph.
   */
  mrpt::obs::CObservation2DRangeScan::Ptr m_first_laser_scan;
  /**\brief Track the first node registration occurance
   *
   * Handy so that we can assign a measurement to the root node as well.
   */
  bool m_is_first_time_node_reg = true;
  /**\brief Node Count lower bound before executing dijkstra
   */
  size_t m_dijkstra_node_count_thresh = 3;
  /**\brief Factor used for accepting an ICP Constraint as valid.
   */
  double m_consec_icp_constraint_factor;
  /**\brief Factor used for accepting an ICP Constraint in the loop closure
   * proc.
   */
  double m_lc_icp_constraint_factor;
};
}  // namespace mrpt::graphslam::deciders
#include "CLoopCloserERD_impl.h"
