//==============================================================================
// Copyright 2018-2020 Kitware, Inc., Kitware SAS
// Author: Guilbert Pierre (Kitware SAS)
//         Cadart Nicolas (Kitware SAS)
// Creation date: 2018-03-27
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//==============================================================================

// This slam algorithm is inspired by the LOAM algorithm:
// J. Zhang and S. Singh. LOAM: Lidar Odometry and Mapping in Real-time.
// Robotics: Science and Systems Conference (RSS). Berkeley, CA, July 2014.

// The algorithm is composed of three sequential steps:
//
// - Keypoints extraction: this step consists of extracting keypoints over
// the points clouds. To do that, the laser lines / scans are treated independently.
// The laser lines are projected onto the XY plane and are rescaled depending on
// their vertical angle. Then we compute their curvature and create two classes of
// keypoints. The edges keypoints which correspond to points with a high curvature
// and planar points which correspond to points with a low curvature.
//
// - Ego-Motion: this step consists of recovering the motion of the lidar
// sensor between two frames (two sweeps). The motion is modelized by a constant
// velocity and angular velocity between two frames (i.e null acceleration).
// Hence, we can parameterize the motion by a rotation and translation per sweep / frame
// and interpolate the transformation inside a frame using the timestamp of the points.
// Since the points clouds generated by a lidar are sparse we can't design a
// pairwise match between keypoints of two successive frames. Hence, we decided to use
// a closest-point matching between the keypoints of the current frame
// and the geometric features derived from the keypoints of the previous frame.
// The geometric features are lines or planes and are computed using the edges
// and planar keypoints of the previous frame. Once the matching is done, a keypoint
// of the current frame is matched with a plane / line (depending of the
// nature of the keypoint) from the previous frame. Then, we recover R and T by
// minimizing the function f(R, T) = sum(d(point, line)^2) + sum(d(point, plane)^2).
// Which can be writen f(R, T) = sum((R*X+T-P).t*A*(R*X+T-P)) where:
// - X is a keypoint of the current frame
// - P is a point of the corresponding line / plane
// - A = (n*n.t) with n being the normal of the plane
// - A = (I - n*n.t).t * (I - n*n.t) with n being a director vector of the line
// Since the function f(R, T) is a non-linear mean square error function
// we decided to use the Levenberg-Marquardt algorithm to recover its argmin.
//
// - Localization: This step consists of refining the motion recovered in the Ego-Motion
// step and to add the new frame in the environment map. Thanks to the ego-motion
// recovered at the previous step it is now possible to estimate the new position of
// the sensor in the map. We use this estimation as an initial point (R0, T0) and we
// perform an optimization again using the keypoints of the current frame and the matched
// keypoints of the map (and not only the previous frame this time!). Once the position in the
// map has been refined from the first estimation it is then possible to update the map by
// adding the keypoints of the current frame into the map.
//
// In the following programs, three 3D coordinates system are used :
// - LIDAR {L} : attached to the geometric center of the LiDAR sensor. The
//   coordinates of the received pointclouds are expressed in this system.
//   LIDAR is rigidly linked (static transform) to BASE.
// - BASE  {B} : attached to the origin of the moving body (e.g. vehicle). We
//   are generally interested in tracking an other point of the moving body than
//   the LiDAR's (for example, we prefer to track the GPS antenna pose).
// - WORLD {W} : The world coordinate system {W} coincides with BASE at the
//   initial position. The output trajectory describes BASE origin in WORLD.

// GENERIC
#include <ctime>

// LOCAL
#include "LidarSlam/Slam.h"
#include "LidarSlam/Utilities.h"
#include "LidarSlam/KDTreePCLAdaptor.h"
#include "LidarSlam/ConfidenceEstimators.h"

// CERES
#include <ceres/solver.h>
// PCL
#include <pcl/common/common.h>
#include <pcl/common/transforms.h>
#include <pcl/io/pcd_io.h>

// EIGEN
#include <Eigen/Dense>

#define PRINT_VERBOSE(minVerbosityLevel, stream) if (this->Verbosity >= (minVerbosityLevel)) {std::cout << stream << std::endl;}
#define IF_VERBOSE(minVerbosityLevel, command) if (this->Verbosity >= (minVerbosityLevel)) { command; }

namespace LidarSlam
{

using KDTree = KDTreePCLAdaptor<Slam::Point>;

namespace Utils
{
namespace
{

//-----------------------------------------------------------------------------
//! Approximate pointcloud memory size
inline size_t PointCloudMemorySize(const Slam::PointCloud& cloud)
{
  return (sizeof(cloud) + (sizeof(Slam::PointCloud::PointType) * cloud.size()));
}

} // end of anonymous namespace
} // end of Utils namespace

//==============================================================================
//   Main SLAM use
//==============================================================================

//-----------------------------------------------------------------------------
Slam::Slam()
{
  // Allocate a default Keypoint Extractor for device 0
  this->KeyPointsExtractors[0] = std::make_shared<SpinningSensorKeypointExtractor>();

  // Allocate maps
  for (auto k : this->UsableKeypoints)
    this->LocalMaps[k] = std::make_shared<RollingGrid>();

  // Set default maps parameters
  if (this->UseKeypoints[EDGE])
    this->InitMap(EDGE);
  if (this->UseKeypoints[INTENSITY_EDGE])
    this->InitMap(INTENSITY_EDGE);
  if (this->UseKeypoints[PLANE])
    this->InitMap(PLANE);
  if (this->UseKeypoints[BLOB])
    this->InitMap(BLOB);

  // Reset SLAM internal state
  this->Reset();
}

//-----------------------------------------------------------------------------
void Slam::InitMap(Keypoint k)
{
  // Allocate map
  this->LocalMaps[k] = std::make_shared<RollingGrid>();

  // Set default maps parameters
  this->LocalMaps[k]->SetVoxelResolution(10.);
  this->LocalMaps[k]->SetGridSize(50);

  switch(k)
  {
    case EDGE:
      this->LocalMaps[k]->SetLeafSize(0.3);
      break;
    case INTENSITY_EDGE:
      this->LocalMaps[k]->SetLeafSize(0.3);
      break;
    case PLANE:
      this->LocalMaps[k]->SetLeafSize(0.6);
      break;
    case BLOB:
      this->LocalMaps[k]->SetLeafSize(0.3);
      break;
    default:
      PRINT_ERROR("Unknown keypoint type");
      break;
  }
}

//-----------------------------------------------------------------------------
void Slam::Reset(bool resetLog)
{
  // Reset keypoints maps
  this->ClearMaps();

  // Reset keyframes
  this->KfLastPose = Eigen::Isometry3d::Identity();
  this->KfCounter = 0;

  // n-DoF parameters
  this->Tworld = Eigen::Isometry3d::Identity();
  this->TworldInit = Eigen::Isometry3d::Identity();
  this->Trelative = Eigen::Isometry3d::Identity();
  this->WithinFrameMotion.SetTransforms(Eigen::Isometry3d::Identity(), Eigen::Isometry3d::Identity());

  // Reset pose uncertainty
  this->LocalizationUncertainty = LocalOptimizer::RegistrationError();

  // Reset point clouds
  this->CurrentFrames.clear();
  this->RegisteredFrame.reset(new PointCloud);
  this->CurrentFrames.emplace_back(new PointCloud);
  for (auto k : this->UsableKeypoints)
  {
    this->CurrentRawKeypoints[k].reset(new PointCloud);
    this->CurrentUndistortedKeypoints[k].reset(new PointCloud);
    this->CurrentWorldKeypoints[k].reset(new PointCloud);
  }

  // Reset keypoints matching results
  for (auto k : this->UsableKeypoints)
  {
    this->EgoMotionMatchingResults[k] = KeypointsMatcher::MatchingResults();
    this->LocalizationMatchingResults[k] = KeypointsMatcher::MatchingResults();
  }

  // Reset external sensor managers
  // WARNING : if offline process, measurements should be reloaded from
  // outside this lib. Moreover, landmark managers lost their absolute poses,
  // they would need to be reloaded too.
  this->ResetSensors(true);

  // Reset log history
  if (resetLog)
  {
    // Reset logged keypoints
    this->NbrFrameProcessed = 0;
    this->LogStates.clear();

    // Reset processing duration timers
    Utils::Timer::Reset();
  }
}

//-----------------------------------------------------------------------------
void Slam::SetNbThreads(int n)
{
  // Set number of threads for main processes
  this->NbThreads = n;
  // Set number of threads for keypoints extraction
  for (const auto& kv : this->KeyPointsExtractors)
    kv.second->SetNbThreads(n);
}

//-----------------------------------------------------------------------------
void Slam::EnableKeypointType(Keypoint k, bool enabled)
{
  this->UseKeypoints[k] = enabled;
  if (enabled)
  {
    if (!this->LocalMaps.count(k))
      this->InitMap(k);
    if (!this->CurrentRawKeypoints.count(k))
      this->CurrentRawKeypoints[k].reset(new PointCloud);
    if (!this->CurrentUndistortedKeypoints.count(k))
      this->CurrentUndistortedKeypoints[k].reset(new PointCloud);
    if (!this->CurrentWorldKeypoints.count(k))
      this->CurrentWorldKeypoints[k].reset(new PointCloud);
    this->EgoMotionMatchingResults[k] = KeypointsMatcher::MatchingResults();
    this->LocalizationMatchingResults[k] = KeypointsMatcher::MatchingResults();
  }
}

//-----------------------------------------------------------------------------
bool Slam::KeypointTypeEnabled(Keypoint k) const
{
  if (!this->UseKeypoints.count(k))
    return false;
  return this->UseKeypoints.at(k);
}

//-----------------------------------------------------------------------------
void Slam::AddFrames(const std::vector<PointCloud::Ptr>& frames)
{
  Utils::Timer::Init("SLAM frame processing");

  // Check that input frames are correct and can be processed
  if (!this->CheckFrames(frames))
    return;
  this->CurrentFrames = frames;
  this->CurrentTime = Utils::PclStampToSec(this->CurrentFrames[0]->header.stamp);

  // Set init pose (can have been modified by global optimization / reset)
  // 1) To ensure a smooth local SLAM, the global optimization must refine
  // poses relatively to last pose, i.e, last pose must be fixed.
  // 2) To ensure a fix reference point, the global optimization must refine
  // poses relatively to starting point, i.e, last pose can have changed.
  // If LogStates empty, do not modify Tworld (must be identity after Reset)
  if (!this->LogStates.empty())
    this->Tworld = this->LogStates.back().Isometry;
  else
    this->Tworld = this->TworldInit;

  // Create UsableKeypointTypes for new frame
  // The keypoints cannot be chosen while processing a frame
  // because it impacts all the maps structure along the process
  this->UsableKeypoints.clear();
  for (auto k : KeypointTypes)
  {
    if (this->UseKeypoints[k])
      this->UsableKeypoints.push_back(k);
  }

  PRINT_VERBOSE(2, "\n#########################################################");
  PRINT_VERBOSE(1, "Processing frame " << this->NbrFrameProcessed << std::fixed << std::setprecision(9) <<
                   " (at time " << this->CurrentTime << ")" << std::scientific);
  PRINT_VERBOSE(2, "#########################################################\n");

  // Compute the edge and planar keypoints
  IF_VERBOSE(3, Utils::Timer::Init("Keypoints extraction"));
  this->ExtractKeypoints();
  IF_VERBOSE(3, Utils::Timer::StopAndDisplay("Keypoints extraction"));

  // Estimate Trelative by extrapolating new pose with a constant velocity model
  // and/or registering current frame on previous one
  IF_VERBOSE(3, Utils::Timer::Init("Ego-Motion"));
  this->ComputeEgoMotion();
  IF_VERBOSE(3, Utils::Timer::StopAndDisplay("Ego-Motion"));

  if ((this->WheelOdomManager && this->WheelOdomManager->CanBeUsedLocally()) ||
      (this->GravityManager && this->GravityManager->CanBeUsedLocally()) ||
       this->LmCanBeUsedLocally() ||
      (this->PoseManager && this->PoseManager->CanBeUsedLocally()))
  {
    IF_VERBOSE(3, Utils::Timer::Init("External sensor constraints computation"));
    this->ComputeSensorConstraints();
    IF_VERBOSE(3, Utils::Timer::StopAndDisplay("External sensor constraints computation"));
  }

  // Perform Localization : update Tworld from map and current frame keypoints
  // and optionally undistort keypoints clouds based on ego-motion
  IF_VERBOSE(3, Utils::Timer::Init("Localization"));
  this->Localization();
  IF_VERBOSE(3, Utils::Timer::StopAndDisplay("Localization"));

  // Compute and check pose confidence estimators
  // Must be set before maps update because the overlap computation
  // requires the current KdTree. This KdTree is reset in the maps update.
  if (this->OverlapSamplingRatio > 0 || this->TimeWindowDuration > 0)
  {
    IF_VERBOSE(3, Utils::Timer::Init("Confidence estimators computation"));
    if (this->OverlapSamplingRatio > 0)
      this->EstimateOverlap();
    if (this->TimeWindowDuration > 0)
      this->CheckMotionLimits();
    IF_VERBOSE(3, Utils::Timer::StopAndDisplay("Confidence estimators computation"));
  }

  // Check if the frame is a keyframe
  this->IsKeyFrame = this->CheckKeyFrame();
  if (this->IsKeyFrame)
  {
    // Notify current frame to be a new keyframe
    this->KfCounter++;
    this->KfLastPose = this->Tworld;
  }

  if (this->Valid || this->IsKeyFrame)
  {
    // Update the tags if requested
    if (this->LandmarkConstraintLocal)
    {
      for (auto& idLm : this->LandmarksManagers)
      {
        if (idLm.second.UpdateAbsolutePose(this->Tworld, this->CurrentTime))
          PRINT_VERBOSE(3, "Updating reference pose of tag #" << idLm.first << " to "<<idLm.second.GetAbsolutePose().transpose());
      }
    }

    // Update keypoints maps if required: add current keypoints to map using Tworld
    // If mapping mode is ADD_KPTS_TO_FIXED_MAP the initial map points will remain untouched
    // If mapping mode is UPDATE, the initial map points can disappear.
    if ((this->MapUpdate == MappingMode::ADD_KPTS_TO_FIXED_MAP
        || this->MapUpdate == MappingMode::UPDATE)
        && this->IsKeyFrame)
    {
      IF_VERBOSE(3, Utils::Timer::Init("Maps update"));
      this->UpdateMapsUsingTworld();
      IF_VERBOSE(3, Utils::Timer::StopAndDisplay("Maps update"));
    }

    // Log current frame processing results : pose, covariance and keypoints.
    IF_VERBOSE(3, Utils::Timer::Init("Logging"));
    this->LogCurrentFrameState();
    IF_VERBOSE(3, Utils::Timer::StopAndDisplay("Logging"));
  }

  // Motion and localization parameters estimation information display
  if (this->Verbosity >= 2)
  {
    SET_COUT_FIXED_PRECISION(3);
    std::cout << "========== SLAM results ==========\n";
    if (this->Undistortion)
    {
      Eigen::Isometry3d motion = this->WithinFrameMotion.GetTransformRange();
      std::cout << "Within frame motion:\n"
                   " translation = [" << motion.translation().transpose()                                        << "] m\n"
                   " rotation    = [" << Utils::Rad2Deg(Utils::RotationMatrixToRPY(motion.linear())).transpose() << "] °\n";
    }
    std::cout << "Ego-Motion:\n"
                 " translation = [" << this->Trelative.translation().transpose()                                        << "] m\n"
                 " rotation    = [" << Utils::Rad2Deg(Utils::RotationMatrixToRPY(this->Trelative.linear())).transpose() << "] °\n"
                 "Localization:\n"
                 " position    = [" << this->Tworld.translation().transpose()                                        << "] m\n"
                 " orientation = [" << Utils::Rad2Deg(Utils::RotationMatrixToRPY(this->Tworld.linear())).transpose() << "] °" << std::endl;
    RESET_COUT_FIXED_PRECISION;
  }

  if (this->Verbosity >= 5)
  {
    SET_COUT_FIXED_PRECISION(3);
    std::cout << "========== Memory usage ==========\n";
    std::map<Keypoint, unsigned int> points;
    std::map<Keypoint, unsigned int> memory;
    // Initialize number of points and memory per keypoint type
    for (auto k : this->UsableKeypoints)
    {
      points[k] = 0;
      memory[k] = 0;
    }
    // Sum points and memory allocated of each keypoints cloud
    for (auto const& st: this->LogStates)
    {
      for (auto k : this->UsableKeypoints)
      {
        points[k] += st.Keypoints.at(k)->PointsSize();
        memory[k] += st.Keypoints.at(k)->MemorySize();
      }
    }

    // Print keypoints memory usage
    for (auto k : this->UsableKeypoints)
    {
      std::cout << Utils::Capitalize(Utils::Plural(KeypointTypeNames.at(k)))<< " log  : "
                << LogStates.size() << " frames, "
                << points[k] * 1e-6 << " points, "
                << memory[k] << " MB\n";

    }
    RESET_COUT_FIXED_PRECISION;
  }

  // Frame processing duration
  this->Latency = Utils::Timer::Stop("SLAM frame processing");
  this->NbrFrameProcessed++;
  IF_VERBOSE(1, Utils::Timer::StopAndDisplay("SLAM frame processing"));
}

//-----------------------------------------------------------------------------
void Slam::ComputeSensorConstraints()
{
  if (this->WheelOdomManager && this->WheelOdomManager->CanBeUsedLocally() &&
      this->WheelOdomManager->ComputeConstraint(this->CurrentTime))
    PRINT_VERBOSE(3, "Wheel odometry constraint added")
  if (this->GravityManager && this->GravityManager->CanBeUsedLocally() &&
      this->GravityManager->ComputeConstraint(this->CurrentTime))
    PRINT_VERBOSE(3, "IMU gravity constraint added")
  if (this->LmCanBeUsedLocally())
  {
    for (auto& idLm : this->LandmarksManagers)
    {
      PRINT_VERBOSE(3, "Checking state of tag #" << idLm.first)
      if (idLm.second.ComputeConstraint(this->CurrentTime))
        PRINT_VERBOSE(3, "\t Adding constraint for tag #" << idLm.first)
    }
  }
  if (this->PoseManager && this->PoseManager->CanBeUsedLocally())
  {
    if (!this->LogStates.empty())
    {
      // Update last pose information because the external pose constraint is relative
      // The last pose logged corresponds to last lidar frame
      this->PoseManager->SetPrevLidarTime(this->LogStates.back().Time);
      this->PoseManager->SetPrevPoseTransform(this->LogStates.back().Isometry);
      if (this->PoseManager->ComputeConstraint(this->CurrentTime))
        PRINT_VERBOSE(3, "External pose constraint added")
    }
  }
}

//-----------------------------------------------------------------------------
void Slam::UpdateMaps()
{
  // The iteration is not directly on Keypoint types
  // because of openMP behaviour which needs int iteration on MSVC
  int nbKeypointTypes = static_cast<int>(this->UsableKeypoints.size());
  #pragma omp parallel for num_threads(std::min(this->NbThreads, nbKeypointTypes))
  for (int i = 0; i < nbKeypointTypes; ++i)
  {
    Keypoint k = static_cast<Keypoint>(this->UsableKeypoints[i]);
    this->LocalMaps[k]->Clear();
    PointCloud::Ptr keypoints(new PointCloud);
    for (auto& state : this->LogStates)
    {
      if (!state.IsKeyFrame)
        continue;

      // Keypoints are stored undistorted : because of the keyframes mechanism,
      // undistortion cannot be refined during pose graph
      // We rely on a good first estimation of the in-frame motion
      keypoints.reset(new PointCloud);
      PointCloud::Ptr undistortedKeypoints = state.Keypoints[k]->GetCloud();
      pcl::transformPointCloud(*undistortedKeypoints, *keypoints, state.Isometry.matrix().cast<float>());

      this->LocalMaps[k]->Add(keypoints, false);
    }
    // Roll to center onto last pose
    Eigen::Vector4f minPoint, maxPoint;
    pcl::getMinMax3D(*keypoints, minPoint, maxPoint);
    this->LocalMaps[k]->Roll(minPoint.head<3>().array(), maxPoint.head<3>().array());
  }
}

//-----------------------------------------------------------------------------
bool Slam::OptimizeGraph()
{
  #ifdef USE_G2O
  // Check if graph can be optimized
  if (!this->LmHasData() && !this->GpsHasData())
  {
    PRINT_WARNING("No external constraint found, graph cannot be optimized");
    return false;
  }

  PoseGraphOptimizer graphManager;
  graphManager.SetFixFirst(this->FixFirstVertex);
  graphManager.SetFixLast(this->FixLastVertex);
  // Clear the graph
  graphManager.ResetGraph();
  // Init pose graph optimizer
  graphManager.SetNbIteration(this->NbGraphIterations);
  graphManager.SetVerbose(this->Verbosity >= 2);
  graphManager.SetSaveG2OFile(!this->G2oFileName.empty());
  graphManager.SetG2OFileName(this->G2oFileName);
  // Add new SLAM states to graph
  graphManager.AddLidarStates(this->LogStates);

  IF_VERBOSE(1, Utils::Timer::Init("Pose graph optimization"));
  IF_VERBOSE(3, Utils::Timer::Init("PGO : optimization"));

  // Boolean to store the info "there is at least one external constraint in the graph"
  bool externalConstraint = false;

  // Look for landmark constraints
  if (this->LmHasData())
  {
    // Allow the rotation of the covariances when interpolating the measurements
    this->SetLandmarkCovarianceRotation(true);
    // Set the landmark detector calibration
    graphManager.AddExternalSensor(this->LmDetectorCalibration, int(ExternalSensor::LANDMARK_DETECTOR));

    for (auto& idLm : this->LandmarksManagers)
    {
      // Shortcut to current manager
      auto& lm = idLm.second;

      // Add landmark to the graph
      Eigen::Vector6d lmPose = lm.GetAbsolutePose();
      Eigen::Isometry3d lmTransfo = Utils::XYZRPYtoIsometry(lmPose.data());
      graphManager.AddLandmark(lmTransfo, idLm.first, lm.GetPositionOnly());

      // Add landmarks constraint to the graph
      lm.SetVerbose(false);
      for (auto& s : this->LogStates)
      {
        ExternalSensors::LandmarkMeasurement lmSynchMeasure; // Virtual landmark measure with synchronized timestamp and no calibration applied
        if (!lm.ComputeSynchronizedMeasure(s.Time, lmSynchMeasure))
          continue;
        // Add synchronized landmark observations to the graph
        graphManager.AddLandmarkConstraint(s.Index, idLm.first, lmSynchMeasure, lm.GetPositionOnly());
        externalConstraint = true;
      }
      lm.SetVerbose(this->Verbosity >= 3);
    }
    // Reset the rotate covariance member to not rotate covariances
    // in future local constraints building
    this->SetLandmarkCovarianceRotation(false);
  }

  // Look for GPS constraints
  if (this->GpsHasData())
  {
    graphManager.AddExternalSensor(this->GpsManager->GetCalibration(), int(ExternalSensor::GPS));
    for (auto& s : this->LogStates)
    {
      ExternalSensors::GpsMeasurement gpsSynchMeasure; // Virtual GPS measure in SLAM reference frame with synchronized timestamp
      if (!this->GpsManager->ComputeSynchronizedMeasureOffset(s.Time, gpsSynchMeasure))
        continue;

      // Add synchronized gps measurement to the graph
      graphManager.AddGpsConstraint(s.Index, gpsSynchMeasure);
      externalConstraint = true;
    }
  }

  if (!externalConstraint)
  {
    PRINT_ERROR("No external constraints exist. Pose graph can not be optimized");
    return false;
  }

  auto statesInit = this->LogStates;

  // Run pose graph optimization
  if (!graphManager.Process(this->LogStates))
  {
    PRINT_ERROR("Pose graph optimization failed.");
    return false;
  }

  // WARNING : covariances are not updated at each graph optimization
  // because g2o does not allow to reach them.
  // Covariances rotation is mandatory if covariances are to be used again afterwards
  auto itStates = this->LogStates.begin();
  auto itInit = statesInit.begin();
  while (itInit != statesInit.end())
  {
    // Compute relative transform
    Eigen::Isometry3d Trel = itInit->Isometry.inverse() * itStates->Isometry;
    Eigen::Vector6d pose = Utils::IsometryToXYZRPY(itInit->Isometry);
    CeresTools::RotateCovariance(pose, itStates->Covariance, Trel); // new = init * Trel
    ++itStates;
    ++itInit;
  }

  IF_VERBOSE(3, Utils::Timer::StopAndDisplay("PGO : optimization"));

  // Update the maps
  IF_VERBOSE(3, Utils::Timer::Init("PGO : maps update"));
  this->UpdateMaps();
  IF_VERBOSE(3, Utils::Timer::StopAndDisplay("PGO : maps update"));

  // The last pose has to be updated with new optimized pose
  this->SetWorldTransformFromGuess(this->LogStates.back().Isometry);

  IF_VERBOSE(1, Utils::Timer::StopAndDisplay("Pose graph optimization"));

  return true;

  #else
  PRINT_ERROR("SLAM graph optimization requires G2O, but it was not found.");
  return false;
  #endif  // USE_G2O
}

//-----------------------------------------------------------------------------
void Slam::SetWorldTransformFromGuess(const Eigen::Isometry3d& poseGuess)
{
  // Store pose in case of reinitialization need
  this->TworldInit = poseGuess;
  // Set current pose
  this->Tworld = poseGuess;

  // Ego-Motion estimation is not valid anymore since we imposed a discontinuity.
  // We reset previous pose so that previous ego-motion extrapolation results in Identity matrix.
  // We reset current frame keypoints so that ego-motion registration will be skipped for next frame.
  if (!this->LogStates.empty())
    this->LogStates.back().Isometry = this->Tworld;
  for (auto k : this->UsableKeypoints)
    this->CurrentRawKeypoints[k].reset(new PointCloud);
}

//-----------------------------------------------------------------------------
void Slam::SaveMapsToPCD(const std::string& filePrefix, PCDFormat pcdFormat, bool filtered) const
{
  IF_VERBOSE(3, Utils::Timer::Init("Keypoints maps saving to PCD"));

  // Save keypoint maps
  for (auto k : this->UsableKeypoints)
    savePointCloudToPCD(filePrefix + Utils::Plural(KeypointTypeNames.at(k)) + ".pcd",  *this->GetMap(k, filtered),  pcdFormat, true);

  IF_VERBOSE(3, Utils::Timer::StopAndDisplay("Keypoints maps saving to PCD"));
}

//-----------------------------------------------------------------------------
void Slam::LoadMapsFromPCD(const std::string& filePrefix, bool resetMaps)
{
  IF_VERBOSE(3, Utils::Timer::Init("Keypoints maps loading from PCD"));

  // In most of the cases, we would like to reset SLAM internal maps before
  // loading new maps to avoid conflicts.
  if (resetMaps)
    this->ClearMaps();

  for (auto k : this->UsableKeypoints)
  {
    std::string path = filePrefix + Utils::Plural(KeypointTypeNames.at(k)) + ".pcd";
    PointCloud::Ptr keypoints(new PointCloud);
    if (pcl::io::loadPCDFile(path, *keypoints) == 0)
    {
      std::cout << "SLAM keypoints map successfully loaded from " << path << std::endl;
      // If mapping mode is NONE or ADD_DECAYING_KPTS, the first map points are fixed,
      // else, the initial map points can be updated
      bool fixedMap = this->MapUpdate == MappingMode::NONE || this->MapUpdate == MappingMode::ADD_KPTS_TO_FIXED_MAP;
      this->LocalMaps[k]->Add(keypoints, fixedMap, std::time(nullptr));
    }
  }
  // TODO : load/use map origin (in which coordinates?) in title or VIEWPOINT field
  IF_VERBOSE(3, Utils::Timer::StopAndDisplay("Keypoints maps loading from PCD"));
}

//==============================================================================
//   SLAM results getters
//==============================================================================

//-----------------------------------------------------------------------------
Eigen::Isometry3d Slam::GetLatencyCompensatedWorldTransform() const
{
  // Get 2 last transforms
  unsigned int trajectorySize = this->LogStates.size();
  if (trajectorySize == 0)
    return Eigen::Isometry3d::Identity();
  else if (trajectorySize == 1)
    return this->LogStates.back().Isometry;
  auto itSt = this->LogStates.end();
  const LidarState& current = *(--itSt);
  const LidarState& previous = *(--itSt);

  // Linearly compute normalized timestamp of Hpred.
  // We expect H0 and H1 to match with time 0 and 1.
  // If timestamps are not defined or too close, extrapolation is impossible.
  if (std::abs(current.Time - previous.Time) < 1e-6)
  {
    PRINT_WARNING("Unable to compute latency-compensated transform : timestamps undefined or too close.");
    return current.Isometry;
  }
  // If requested extrapolation timestamp is too far from previous frames timestamps, extrapolation is impossible.
  if (std::abs(this->Latency / (current.Time - previous.Time)) > this->MaxExtrapolationRatio)
  {
    PRINT_WARNING("Unable to compute latency-compensated transform : extrapolation time is too far.");
    return current.Isometry;
  }

  // Extrapolate H0 and H1 to get expected Hpred at current time
  Eigen::Isometry3d Hpred = LinearInterpolation(previous.Isometry, current.Isometry, current.Time + this->Latency, previous.Time, current.Time);
  return Hpred;
}

//-----------------------------------------------------------------------------
std::unordered_map<std::string, double> Slam::GetDebugInformation() const
{
  std::unordered_map<std::string, double> map;
  for (Keypoint k : this->UsableKeypoints)
  {
    std::string name = "EgoMotion: " + Utils::Plural(KeypointTypeNames.at(k)) + " used";
    map[name] = this->EgoMotionMatchingResults.at(k).NbMatches();
  }

  for (auto k : this->UsableKeypoints)
  {
    std::string name = "Localization: " + Utils::Plural(KeypointTypeNames.at(k)) + " used";
    map[name] = this->LocalizationMatchingResults.at(k).NbMatches();
  }

  map["Localization: position error"]      = this->LocalizationUncertainty.PositionError;
  map["Localization: orientation error"]   = this->LocalizationUncertainty.OrientationError;
  map["Confidence: overlap"]               = this->OverlapEstimation;
  map["Confidence: comply motion limits"]  = this->ComplyMotionLimits;

  return map;
}

//-----------------------------------------------------------------------------
std::unordered_map<std::string, std::vector<double>> Slam::GetDebugArray() const
{
  auto toDoubleVector = [](auto const& scalars) { return std::vector<double>(scalars.begin(), scalars.end()); };

  std::unordered_map<std::string, std::vector<double>> map;
  for (auto k : this->UsableKeypoints)
  {
    std::string name = "EgoMotion: " + KeypointTypeNames.at(k) + " matches";
    map[name]  = toDoubleVector(this->EgoMotionMatchingResults.at(k).Rejections);
    name = "EgoMotion: " + KeypointTypeNames.at(k) + " weights";
    map[name]  = this->EgoMotionMatchingResults.at(k).Weights;
  }

  for (auto k : this->UsableKeypoints)
  {
    std::string name = "Localization: " + KeypointTypeNames.at(k) + " matches";
    map[name]  = toDoubleVector(this->LocalizationMatchingResults.at(k).Rejections);
    name = "Localization: " + KeypointTypeNames.at(k) + " weights";
    map[name]  = this->LocalizationMatchingResults.at(k).Weights;
  }

  return map;
}

//-----------------------------------------------------------------------------
Slam::PointCloud::Ptr Slam::GetRegisteredFrame()
{
  // If the input points have not been aggregated to WORLD coordinates yet,
  // transform and aggregate them
  if (this->RegisteredFrame->header.stamp != this->CurrentFrames[0]->header.stamp)
    this->RegisteredFrame = this->AggregateFrames(this->CurrentFrames, true);
  return this->RegisteredFrame;
}

//-----------------------------------------------------------------------------
Slam::PointCloud::Ptr Slam::GetMap(Keypoint k, bool clean) const
{
  PointCloud::Ptr map = this->LocalMaps.at(k)->Get(clean);
  map->header = Utils::BuildPclHeader(this->CurrentFrames[0]->header.stamp,
                                      this->WorldFrameId,
                                      this->NbrFrameProcessed);
  return map;
}

//-----------------------------------------------------------------------------
Slam::PointCloud::Ptr Slam::GetTargetSubMap(Keypoint k) const
{
  PointCloud::Ptr subMap = this->LocalMaps.at(k)->GetSubMap();
  subMap->header = Utils::BuildPclHeader(this->CurrentFrames[0]->header.stamp,
                                         this->WorldFrameId,
                                         this->NbrFrameProcessed);
  return subMap;
}

//-----------------------------------------------------------------------------
Slam::PointCloud::Ptr Slam::GetKeypoints(Keypoint k, bool worldCoordinates)
{
  // Return keypoints in BASE coordinates
  if (!worldCoordinates)
    return this->CurrentUndistortedKeypoints.at(k);

  // Return keypoints in WORLD coordinates
  // If the keypoints have not been transformed yet to WORLD coordinates, perform transformation
  if (this->CurrentWorldKeypoints.at(k)->header.stamp != this->CurrentUndistortedKeypoints.at(k)->header.stamp)
    this->CurrentWorldKeypoints[k] = this->TransformPointCloud(this->CurrentUndistortedKeypoints[k],
                                                               this->Tworld, this->WorldFrameId);
  return this->CurrentWorldKeypoints.at(k);
}

//==============================================================================
//   Main SLAM steps
//==============================================================================

//-----------------------------------------------------------------------------
bool Slam::CheckFrames(const std::vector<PointCloud::Ptr>& frames)
{
  // Check input frames and return if they are all empty
  bool allFramesEmpty = true;
  for (unsigned int i = 0; i < frames.size(); ++i)
  {
    if (!frames[i] || !frames[i]->empty())
      allFramesEmpty = false;
    else
      PRINT_WARNING("SLAM input frame " << i << " is an empty pointcloud : frame ignored.");
  }
  if (allFramesEmpty)
  {
    PRINT_ERROR("SLAM input only contains empty pointclouds : exiting.");
    return false;
  }

  // Skip frames if it has the same timestamp as previous ones (will induce problems in extrapolation)
  if (frames[0]->header.stamp == this->CurrentFrames[0]->header.stamp)
  {
    PRINT_ERROR("SLAM frames have the same timestamp (" << frames[0]->header.stamp << ") as previous ones : frames ignored.");
    return false;
  }

  // Check frame dropping
  for (unsigned int i = 0; i < frames.size(); ++i)
  {
    unsigned int droppedFrames = frames[i]->header.seq - this->PreviousFramesSeq[i] - 1;
    if ((this->PreviousFramesSeq[i] > 0) && (droppedFrames > 0))
      PRINT_WARNING(droppedFrames << " frame(s)" << (frames.size() > 1 ? " from LiDAR device " + std::to_string(i) : "") << " were dropped by SLAM\n");
    this->PreviousFramesSeq[i] = frames[i]->header.seq;
  }

  return true;
}

//-----------------------------------------------------------------------------
void Slam::ExtractKeypoints()
{
  PRINT_VERBOSE(2, "========== Keypoints extraction ==========");

  // Current keypoints become previous ones
  this->PreviousRawKeypoints = this->CurrentRawKeypoints;

  // Extract keypoints from each input cloud
  std::map<Keypoint, std::vector<PointCloud::Ptr>> keypoints;
  for (const auto& frame: this->CurrentFrames)
  {
    // If the frame is empty, ignore it
    if (frame->empty())
      continue;

    // Get keypoints extractor to use for this LiDAR device
    int lidarDevice = frame->front().device_id;
    // Check if KE exists
    if (!this->KeyPointsExtractors.count(lidarDevice))
    {
      // If KE does not exist but we are only using a single KE, use default one
      if (this->KeyPointsExtractors.size() == 1)
      {
        PRINT_WARNING("Input frame comes from LiDAR device " << lidarDevice
                    << " but no keypoints extractor has been set for this device : using default extractor for device 0.");
        lidarDevice = 0;
      }
      // Otherwise ignore frame
      else
      {
        PRINT_ERROR("Input frame comes from LiDAR device " << lidarDevice
                    << " but no keypoints extractor has been set for this device : ignoring frame.");
        continue;
      }
    }
    KeypointExtractorPtr& ke = this->KeyPointsExtractors[lidarDevice];
    ke->Enable(this->UsableKeypoints);
    // Extract keypoints from this frame
    ke->ComputeKeyPoints(frame);
    for (auto k : this->UsableKeypoints)
      keypoints[k].push_back(ke->GetKeypoints(k));
  }

  // Merge all keypoints extracted from different frames together
  for (auto k : this->UsableKeypoints)
    this->CurrentRawKeypoints[k] = this->AggregateFrames(keypoints[k], false);

  if (this->Verbosity >= 2)
  {
    std::cout << "Extracted features : ";
    for (auto k : this->UsableKeypoints)
      std::cout << this->CurrentRawKeypoints[k]->size() << " " << Utils::Plural(KeypointTypeNames.at(k)) << " ";
    std::cout << std::endl;
  }
}

//-----------------------------------------------------------------------------
bool Slam::InitTworldWithPoseMeasurement(double time)
{
  // Compute synchronized pose
  if (!this->PoseManager)
  {
    PRINT_WARNING("No external pose manager : Lidar pose not initialized")
    return false;
  }
  ExternalSensors::PoseMeasurement synchMeas; // Virtual measure with synchronized timestamp and calibration applied
  if (!this->PoseManager->ComputeSynchronizedMeasureBase(time, synchMeas))
  {
    PRINT_WARNING("Cannot find synchronized pose measurement : Lidar pose not initialized")
    return false;
  }

  Eigen::Isometry3d odom2Ext = this->Tworld.inverse() * synchMeas.Pose;
  // Update trajectory
  if (!this->LogStates.empty())
  {
    for (auto& s : this->LogStates)
    {
      // Rotate covariance
      Eigen::Vector6d initPose = Utils::IsometryToXYZRPY(s.Isometry);
      CeresTools::RotateCovariance(initPose, s.Covariance, odom2Ext); // new = init * odom2Ext
      // Transform pose
      s.Isometry = s.Isometry * odom2Ext;
    }
    // Update maps
    this->UpdateMaps();
  }
  else
    this->TworldInit = this->Tworld * odom2Ext;

  this->Tworld = this->Tworld * odom2Ext;

  PRINT_VERBOSE(1, "Pose initialized with external pose measurement");
  return true;
}

//-----------------------------------------------------------------------------
void Slam::ComputeEgoMotion()
{
  PRINT_VERBOSE(2, "========== Ego-Motion ==========");

  // Reset ego-motion
  this->Trelative = Eigen::Isometry3d::Identity();

  bool externalAvailable = false;
  // Linearly extrapolate previous motion to estimate new pose
  if (!this->LogStates.empty() &&
      (this->EgoMotion == EgoMotionMode::EXTERNAL ||
       this->EgoMotion == EgoMotionMode::EXTERNAL_OR_MOTION_EXTRAPOLATION))
  {
    if (this->PoseHasData())
    {
      ExternalSensors::PoseMeasurement synchPreviousPoseMeas; // Virtual measure with synchronized timestamp and calibration applied
      if (this->PoseManager->ComputeSynchronizedMeasureBase(this->LogStates.back().Time, synchPreviousPoseMeas))
      {
        ExternalSensors::PoseMeasurement synchPoseMeas; // Virtual measure with synchronized timestamp and calibration applied
        if (this->PoseManager->ComputeSynchronizedMeasureBase(this->CurrentTime, synchPoseMeas))
        {
          this->Trelative = synchPreviousPoseMeas.Pose.inverse() * synchPoseMeas.Pose;
          externalAvailable = true;
          PRINT_VERBOSE(3, "Prior pose computed using external poses supplied");
        }
      }
    }
    else
      PRINT_WARNING("External poses are empty : cannot use them to compute ego motion")
  }

  // Linearly extrapolate previous motion to estimate new pose
  if (this->LogStates.size() >= 2 && (
      (this->EgoMotion == EgoMotionMode::MOTION_EXTRAPOLATION ||
       this->EgoMotion == EgoMotionMode::MOTION_EXTRAPOLATION_AND_REGISTRATION) ||
      (this->EgoMotion == EgoMotionMode::EXTERNAL_OR_MOTION_EXTRAPOLATION &&
       !externalAvailable)))
  {
    // Estimate new Tworld with a constant velocity model
    auto itSt = this->LogStates.end();

    const double t1 = (--itSt)->Time;
    const Eigen::Isometry3d& T1 = itSt->Isometry;
    const double t0 = (--itSt)->Time;
    const Eigen::Isometry3d& T0 = itSt->Isometry;
    if (std::abs((this->CurrentTime - t1) / (t1 - t0)) > this->MaxExtrapolationRatio)
      PRINT_WARNING("Unable to extrapolate scan pose from previous motion : extrapolation time is too far.")
    else
    {
      Eigen::Isometry3d nextTworldEstimation = LinearInterpolation(T0, T1, this->CurrentTime, t0, t1);
      this->Trelative = this->Tworld.inverse() * nextTworldEstimation;
    }
  }

  // Refine Trelative estimation by registering current frame on previous one
  if (this->EgoMotion == EgoMotionMode::REGISTRATION ||
      this->EgoMotion == EgoMotionMode::MOTION_EXTRAPOLATION_AND_REGISTRATION)
  {
    // kd-tree to process fast nearest neighbor
    // among the keypoints of the previous pointcloud
    IF_VERBOSE(3, Utils::Timer::Init("EgoMotion : build KD tree"));
    std::map<Keypoint, KDTree> kdtreePrevious;
    // Kdtrees map initialization to parallelize their
    // construction using OMP and avoid concurrency issues
    for (auto k : this->UsableKeypoints)
      kdtreePrevious[k] = KDTree();

    // The iteration is not directly on Keypoint types
    // because of openMP behaviour which needs int iteration on MSVC
    int nbKeypointTypes = static_cast<int>(this->UsableKeypoints.size());
    #pragma omp parallel for num_threads(std::min(this->NbThreads, nbKeypointTypes))
    for (int i = 0; i < nbKeypointTypes; ++i)
    {
      Keypoint k = static_cast<Keypoint>(this->UsableKeypoints[i]);
      if (kdtreePrevious.count(k))
        kdtreePrevious[k].Reset(this->PreviousRawKeypoints[k]);
    }

    if (this->Verbosity >= 2)
    {
      std::cout << "Keypoints extracted from previous frame : ";
      for (auto k : this->UsableKeypoints)
        std::cout << this->PreviousRawKeypoints[k]->size() << " " << Utils::Plural(KeypointTypeNames.at(k)) << " ";
      std::cout << std::endl;
    }

    IF_VERBOSE(3, Utils::Timer::StopAndDisplay("EgoMotion : build KD tree"));
    IF_VERBOSE(3, Utils::Timer::Init("Ego-Motion : whole ICP-LM loop"));

    // Reset ICP results
    this->TotalMatchedKeypoints = 0;

    // Init matching parameters
    KeypointsMatcher::Parameters matchingParams;
    matchingParams.NbThreads = this->NbThreads;
    matchingParams.SingleEdgePerRing = true;
    matchingParams.MaxNeighborsDistance = this->EgoMotionMaxNeighborsDistance;
    matchingParams.EdgeNbNeighbors = this->EgoMotionEdgeNbNeighbors;
    matchingParams.EdgeMinNbNeighbors = this->EgoMotionEdgeMinNbNeighbors;
    matchingParams.EdgeMaxModelError = this->EgoMotionEdgeMaxModelError;
    matchingParams.PlaneNbNeighbors = this->EgoMotionPlaneNbNeighbors;
    matchingParams.PlanarityThreshold = this->EgoMotionPlanarityThreshold;
    matchingParams.PlaneMaxModelError = this->EgoMotionPlaneMaxModelError;

    // ICP - Levenberg-Marquardt loop
    // At each step of this loop an ICP matching is performed. Once the keypoints
    // are matched, we estimate the the 6-DOF parameters by minimizing the
    // non-linear least square cost function using Levenberg-Marquardt algorithm.
    for (unsigned int icpIter = 0; icpIter < this->EgoMotionICPMaxIter; ++icpIter)
    {
      IF_VERBOSE(3, Utils::Timer::Init("  Ego-Motion : ICP"));

      // We want to estimate our 6-DOF parameters using a non linear least square
      // minimization. The non linear part comes from the parametrization of the
      // rotation endomorphism SO(3).
      // First, we need to build the point-to-line, point-to-plane and
      // point-to-blob ICP matches that will be optimized.
      // Then, we use CERES Levenberg-Marquardt optimization to minimize the problem.

      // Create a keypoints matcher
      // At each ICP iteration, the outliers removal is refined to be stricter
      double iterRatio = icpIter / static_cast<double>(this->EgoMotionICPMaxIter - 1);
      matchingParams.SaturationDistance = (1 - iterRatio) * this->EgoMotionInitSaturationDistance + iterRatio * this->EgoMotionFinalSaturationDistance;
      KeypointsMatcher matcher(matchingParams, this->Trelative);

      // Loop over keypoints to build the residuals
      for (auto k : this->UsableKeypoints)
        this->EgoMotionMatchingResults[k] = matcher.BuildMatchResiduals(this->CurrentRawKeypoints[k], kdtreePrevious[k], k);

      // Count matches and skip this frame
      // if there are too few geometric keypoints matched
      this->TotalMatchedKeypoints = 0;
      for (auto k : this->UsableKeypoints)
        this->TotalMatchedKeypoints += this->EgoMotionMatchingResults[k].NbMatches();

      if (this->TotalMatchedKeypoints < this->MinNbMatchedKeypoints)
      {
        PRINT_WARNING("Not enough keypoints, EgoMotion skipped for this frame.");
        break;
      }

      IF_VERBOSE(3, Utils::Timer::StopAndDisplay("  Ego-Motion : ICP"));
      IF_VERBOSE(3, Utils::Timer::Init("  Ego-Motion : LM optim"));

      // Init the optimizer with initial pose and parameters
      LocalOptimizer optimizer;
      optimizer.SetTwoDMode(this->TwoDMode);
      optimizer.SetPosePrior(this->Trelative);
      optimizer.SetLMMaxIter(this->EgoMotionLMMaxIter);
      optimizer.SetNbThreads(this->NbThreads);

      // Add LiDAR ICP matches
      for (auto k : this->UsableKeypoints)
        optimizer.AddResiduals(this->EgoMotionMatchingResults[k].Residuals);

      // Run LM optimization
      ceres::Solver::Summary summary = optimizer.Solve();
      PRINT_VERBOSE(4, summary.BriefReport());

      // Get back optimized Trelative
      this->Trelative = optimizer.GetOptimizedPose();

      IF_VERBOSE(3, Utils::Timer::StopAndDisplay("  Ego-Motion : LM optim"));

      // If no L-M iteration has been made since the last ICP matching, it means
      // that we reached a local minimum for the ICP-LM algorithm.
      if (summary.num_successful_steps == 1)
      {
        break;
      }
    }

    IF_VERBOSE(3, Utils::Timer::StopAndDisplay("Ego-Motion : whole ICP-LM loop"));
    if (this->Verbosity >= 2)
    {
      std::cout << "Matched keypoints: " << this->TotalMatchedKeypoints << " (";
      for (auto k : this->UsableKeypoints)
        std::cout << this->EgoMotionMatchingResults[k].NbMatches() << " " << Utils::Plural(KeypointTypeNames.at(k)) << " ";
      std::cout << ")" << std::endl;
    }
  }

  // Print EgoMotion results
  SET_COUT_FIXED_PRECISION(3);
  PRINT_VERBOSE(2, "Estimated Ego-Motion (motion since last frame):\n"
                   " translation = [" << this->Trelative.translation().transpose() << "] m\n"
                   " rotation    = [" << Utils::Rad2Deg(Utils::RotationMatrixToRPY(this->Trelative.linear())).transpose() << "] °");
  RESET_COUT_FIXED_PRECISION;
}

//-----------------------------------------------------------------------------
void Slam::Localization()
{
  this->LocalizationUncertainty = LocalOptimizer::RegistrationError();
  this->Valid = true;
  PRINT_VERBOSE(2, "========== Localization ==========");

  // Integrate the relative motion to the world transformation
  // Store previous tworld for next iteration
  this->Tworld = this->Tworld * this->Trelative;

  // Init undistorted keypoints clouds from raw points
  // Warning : pointer copy = points modification :
  // do not use CurrentRawKeypoints after this step
  this->CurrentUndistortedKeypoints = this->CurrentRawKeypoints;
  // Init and run undistortion if required
  if (this->Undistortion)
  {
    IF_VERBOSE(3, Utils::Timer::Init("Localization : initial undistortion"));
    if (this->Undistortion != UndistortionMode::EXTERNAL)
    {
      // Init the within frame motion interpolator time bounds
      this->InitUndistortion();
      // Undistort keypoints clouds
      this->RefineUndistortion();
    }
    else
      this->UndistortWithPoseMeasurement();

    IF_VERBOSE(3, Utils::Timer::StopAndDisplay("Localization : initial undistortion"));
  }

  // Get keypoints from maps and build kd-trees for fast nearest neighbors search
  IF_VERBOSE(3, Utils::Timer::Init("Localization : map keypoints extraction"));

  // The iteration is not directly on Keypoint types
  // because of openMP behaviour which needs int iteration on MSVC
  int nbKeypointTypes = static_cast<int>(this->UsableKeypoints.size());
  #pragma omp parallel for num_threads(std::min(this->NbThreads, nbKeypointTypes))
  for (int i = 0; i < nbKeypointTypes; ++i)
  {
    // If the map has been updated, the KD-tree needs to be updated
    Keypoint k = static_cast<Keypoint>(this->UsableKeypoints[i]);
    if (!this->LocalMaps[k]->IsSubMapKdTreeValid())
    {
      // If maps are fixed, we can build a single KD-tree
      // of the entire map to avoid rebuilding it again
      if (this->MapUpdate == MappingMode::NONE)
        this->LocalMaps[k]->BuildSubMapKdTree();

      // Otherwise, we only extract the local sub maps
      // to build a local and smaller KD-tree
      else
      {
        if (this->LocalMaps[k]->IsTimeThreshold())
        {
          IF_VERBOSE(3, Utils::Timer::Init("Localization : clearing old points"));
          this->LocalMaps[k]->ClearOldPoints(this->CurrentTime);
          IF_VERBOSE(3, Utils::Timer::StopAndDisplay("Localization : clearing old points"));
        }
        // Estimate current keypoints bounding box
        PointCloud currWorldKeypoints;
        pcl::transformPointCloud(*this->CurrentUndistortedKeypoints[k], currWorldKeypoints, this->Tworld.matrix());
        Eigen::Vector4f minPoint, maxPoint;
        pcl::getMinMax3D(currWorldKeypoints, minPoint, maxPoint);

        // Build submap of all points lying in this bounding box
        // Moving objects are rejected but the constraint is removed
        // if less than half the number of current keypoints are extracted from the map
        this->LocalMaps[k]->BuildSubMapKdTree(minPoint.head<3>().array(), maxPoint.head<3>().array(), currWorldKeypoints.size() / 2);
      }
    }
  }

  if (this->Verbosity >= 2)
  {
    std::cout << "Keypoints extracted from map : ";
    for (auto k : this->UsableKeypoints)
    {
      std::cout << this->LocalMaps[k]->GetSubMapKdTree().GetInputCloud()->size()
                << " " << Utils::Plural(KeypointTypeNames.at(k)) << " ";
    }
    std::cout << std::endl;
  }

  IF_VERBOSE(3, Utils::Timer::StopAndDisplay("Localization : map keypoints extraction"));
  IF_VERBOSE(3, Utils::Timer::Init("Localization : whole ICP-LM loop"));

  // Reset ICP results
  this->TotalMatchedKeypoints = 0;

  // Init matching parameters
  KeypointsMatcher::Parameters matchingParams;
  matchingParams.NbThreads = this->NbThreads;
  matchingParams.SingleEdgePerRing = false;
  matchingParams.MaxNeighborsDistance = this->LocalizationMaxNeighborsDistance;
  matchingParams.EdgeNbNeighbors = this->LocalizationEdgeNbNeighbors;
  matchingParams.EdgeMinNbNeighbors = this->LocalizationEdgeMinNbNeighbors;
  matchingParams.EdgeMaxModelError = this->LocalizationEdgeMaxModelError;
  matchingParams.PlaneNbNeighbors = this->LocalizationPlaneNbNeighbors;
  matchingParams.PlanarityThreshold = this->LocalizationPlanarityThreshold;
  matchingParams.PlaneMaxModelError = this->LocalizationPlaneMaxModelError;
  matchingParams.BlobNbNeighbors = this->LocalizationBlobNbNeighbors;

  // ICP - Levenberg-Marquardt loop
  // At each step of this loop an ICP matching is performed. Once the keypoints
  // are matched, we estimate the the 6-DOF parameters by minimizing the
  // non-linear least square cost function using Levenberg-Marquardt algorithm.
  for (unsigned int icpIter = 0; icpIter < this->LocalizationICPMaxIter; ++icpIter)
  {
    IF_VERBOSE(3, Utils::Timer::Init("  Localization : ICP"));

    // We want to estimate our 6-DOF parameters using a non linear least square
    // minimization. The non linear part comes from the parametrization of the
    // rotation endomorphism SO(3).
    // First, we need to build the point-to-line, point-to-plane and
    // point-to-blob ICP matches that will be optimized.
    // Then, we use CERES Levenberg-Marquardt optimization to minimize problem.

    // Create a keypoints matcher
    // At each ICP iteration, the outliers removal is refined to be stricter
    double iterRatio = icpIter / static_cast<double>(this->LocalizationICPMaxIter - 1);
    matchingParams.SaturationDistance = (1 - iterRatio) * this->LocalizationInitSaturationDistance + iterRatio * this->LocalizationFinalSaturationDistance;
    KeypointsMatcher matcher(matchingParams, this->Tworld);

    // Loop over keypoints to build the point to line residuals
    this->TotalMatchedKeypoints = 0;
    for (auto k : this->UsableKeypoints)
    {
      this->LocalizationMatchingResults[k] = matcher.BuildMatchResiduals(this->CurrentUndistortedKeypoints[k], this->LocalMaps[k]->GetSubMapKdTree(), k);
      this->TotalMatchedKeypoints += this->LocalizationMatchingResults[k].NbMatches();
    }

    // Skip frame if not enough keypoints are extracted
    if (this->TotalMatchedKeypoints < this->MinNbMatchedKeypoints)
    {
      // Reset state to previous one to avoid instability
      if (this->LogStates.empty())
        this->Tworld = this->TworldInit;
      else
        this->Tworld = this->LogStates.back().Isometry;
      if (this->Undistortion)
        this->WithinFrameMotion.SetTransforms(Eigen::Isometry3d::Identity(), Eigen::Isometry3d::Identity());
      PRINT_ERROR("Not enough keypoints matched, Localization skipped for this frame.");
      this->Valid = false;
      break;
    }

    IF_VERBOSE(3, Utils::Timer::StopAndDisplay("  Localization : ICP"));
    IF_VERBOSE(3, Utils::Timer::Init("  Localization : LM optim"));

    // Init the optimizer with initial pose and parameters
    LocalOptimizer optimizer;
    optimizer.SetTwoDMode(this->TwoDMode);
    optimizer.SetPosePrior(this->Tworld);
    optimizer.SetLMMaxIter(this->LocalizationLMMaxIter);
    optimizer.SetNbThreads(this->NbThreads);

    // Add LiDAR ICP matches
    for (auto k : this->UsableKeypoints)
      optimizer.AddResiduals(this->LocalizationMatchingResults[k].Residuals);

    // Add odometry constraint
    // if constraint has been successfully created
    if (this->WheelOdomManager && this->WheelOdomManager->GetResidual().Cost)
      optimizer.AddResidual(this->WheelOdomManager->GetResidual());

    // Add gravity alignment constraint
    // if constraint has been successfully created
    if (this->GravityManager && this->GravityManager->GetResidual().Cost)
      optimizer.AddResidual(this->GravityManager->GetResidual());

    // Add Pose constraint
    // if constraint has been successfully created
    if (this->PoseManager && this->PoseManager->GetResidual().Cost)
      optimizer.AddResidual(this->PoseManager->GetResidual());

    // Add available landmark constraints
    for (auto& idLm : this->LandmarksManagers)
    {
      // Add landmark constraint
      // if constraint has been successfully created
      if (idLm.second.GetResidual().Cost)
        optimizer.AddResidual(idLm.second.GetResidual());
    }

    // Run LM optimization
    ceres::Solver::Summary summary = optimizer.Solve();
    PRINT_VERBOSE(4, summary.BriefReport());

    // Update Tworld from optimization results
    this->Tworld = optimizer.GetOptimizedPose();

    // Optionally refine undistortion
    if (this->Undistortion == UndistortionMode::REFINED)
      this->RefineUndistortion();

    IF_VERBOSE(3, Utils::Timer::StopAndDisplay("  Localization : LM optim"));

    // If no L-M iteration has been made since the last ICP matching, it means
    // that we reached a local minimum for the ICP-LM algorithm.
    // We evaluate the quality of the Tworld optimization using an approximate
    // computation of the variance covariance matrix.
    if ((summary.num_successful_steps == 1) || (icpIter == this->LocalizationICPMaxIter - 1))
    {
      this->LocalizationUncertainty = optimizer.EstimateRegistrationError();
      break;
    }
  }

  IF_VERBOSE(3, Utils::Timer::StopAndDisplay("Localization : whole ICP-LM loop"));

  // Optionally print localization optimization summary
  if (this->Verbosity >= 2)
  {
    SET_COUT_FIXED_PRECISION(3);
    std::cout << "Matched keypoints: " << this->TotalMatchedKeypoints << " (";
    for (auto k : this->UsableKeypoints)
      std::cout << this->LocalizationMatchingResults[k].NbMatches() << " " << Utils::Plural(KeypointTypeNames.at(k)) << " ";

    std::cout << ")"
              << "\nPosition uncertainty    = " << this->LocalizationUncertainty.PositionError    << " m"
              << " (along [" << this->LocalizationUncertainty.PositionErrorDirection.transpose()    << "])"
              << "\nOrientation uncertainty = " << this->LocalizationUncertainty.OrientationError << " °"
              << " (along [" << this->LocalizationUncertainty.OrientationErrorDirection.transpose() << "])"
              << std::endl;
    RESET_COUT_FIXED_PRECISION;
  }
}

//-----------------------------------------------------------------------------
bool Slam::CheckKeyFrame()
{
  // Compute motion since last keyframe
  Eigen::Isometry3d motionSinceLastKf = this->KfLastPose.inverse() * this->Tworld;
  double transSinceLastKf = motionSinceLastKf.translation().norm();
  double rotSinceLastKf = Eigen::AngleAxisd(motionSinceLastKf.linear()).angle();
  PRINT_VERBOSE(3, "Motion since last keyframe #" << this->KfCounter << ": "
                                                  << transSinceLastKf << " m, "
                                                  << Utils::Rad2Deg(rotSinceLastKf) << " °");
  // Check if current frame is a new keyframe
  // If we don't have enough keyframes yet, the threshold is linearly lowered
  constexpr double MIN_KF_NB = 10.;
  double thresholdCoef = std::min(this->KfCounter / MIN_KF_NB, 1.);
  unsigned int nbMapKpts = 0;
  for (const auto& mapKptsCloud : this->LocalMaps)
    nbMapKpts += mapKptsCloud.second->Size();

  // Mark as keyframe if a new tag was seen after some time
  // This allows to force some keyframes and therefore a constraint in the pose graph optimization
  // if the tag detections are quite sparse
  bool tagRequirement = false;
  for (auto& idLm : this->LandmarksManagers)
  {
    if (idLm.second.NeedsReferencePoseRefresh(this->CurrentTime))
    {
      tagRequirement = true;
      break;
    }
  }

  return nbMapKpts < this->MinNbMatchedKeypoints * 10 ||
         transSinceLastKf >= thresholdCoef * this->KfDistanceThreshold ||
         rotSinceLastKf >= Utils::Deg2Rad(thresholdCoef * this->KfAngleThreshold) ||
         tagRequirement;
}

//-----------------------------------------------------------------------------
void Slam::UpdateMapsUsingTworld()
{
  PRINT_VERBOSE(3, "Adding new keyframe #" << this->KfCounter);

  // Transform keypoints to WORLD coordinates
  for (auto k : this->UsableKeypoints)
    this->CurrentWorldKeypoints[k] = this->GetKeypoints(k, true);

  // Add registered points to map
  // The iteration is not directly on Keypoint types
  // because of openMP behaviour which needs int iteration on MSVC
  int nbKeypointTypes = static_cast<int>(this->UsableKeypoints.size());
  #pragma omp parallel for num_threads(std::min(this->NbThreads, nbKeypointTypes))
  for (int i = 0; i < nbKeypointTypes; ++i)
  {
    Keypoint k = static_cast<Keypoint>(this->UsableKeypoints[i]);
    // Add not fixed points
    this->LocalMaps[k]->Add(this->CurrentWorldKeypoints[k], false, this->CurrentTime);
  }
}

//-----------------------------------------------------------------------------
void Slam::LogCurrentFrameState()
{
  // The two last poses are logged in any case for motion extrapolation and undistortion
  // So, if LogOnlyKeyframes is required, remove the second last pose at each iteration if it was not a keyframe
  if (this->LogStates.size() > 1)
  {
    auto itSt = this->LogStates.end();
    --itSt; --itSt;
    if (this->LogOnlyKeyframes && !itSt->IsKeyFrame)
      this->LogStates.erase(itSt);
  }
  // Save current state to log buffer.
  // This buffer will be processed in the pose graph optimization
  // and is used locally to compute prior ego-motion estimation
  // and to undistort the input points
  LidarState state;
  state.Isometry = this->Tworld;
  // Increase covariance area by scale
  state.Covariance = std::pow(this->CovarianceScale, 2) * this->LocalizationUncertainty.Covariance;
  float defaultPositionError = 1e-2; // 1cm
  float defaultAngleError = Utils::Deg2Rad(1.f); // 1°
  if (!Utils::isCovarianceValid(state.Covariance))
    state.Covariance = Utils::CreateDefaultCovariance(defaultPositionError, defaultAngleError); //1mm, 0.5°

  // If 2D mode enabled, supply constant covariance for unevaluated variables
  if (this->TwoDMode)
  {
    state.Covariance(2, 2) = std::pow(defaultPositionError, 2);
    state.Covariance(3, 3) = std::pow(defaultAngleError,    2);
    state.Covariance(4, 4) = std::pow(defaultAngleError,    2);
  }
  state.Time = this->CurrentTime;
  state.Index = this->NbrFrameProcessed;
  state.IsKeyFrame = this->IsKeyFrame;
  for (auto k : this->UsableKeypoints)
    state.Keypoints[k] = std::make_shared<PCStorage>(this->CurrentUndistortedKeypoints[k], this->LoggingStorage);

  this->LogStates.emplace_back(state);
  // Remove the oldest logged states
  auto itSt = this->LogStates.begin();
  while (this->CurrentTime - itSt->Time > this->LoggingTimeout && this->LogStates.size() > 2)
  {
    ++itSt;
    this->LogStates.pop_front();
  }
}

//-----------------------------------------------------------------------------
LidarState& Slam::GetLastState()
{
  return this->LogStates.back();
}

//==============================================================================
//   Undistortion helpers
//==============================================================================

//-----------------------------------------------------------------------------
Eigen::Isometry3d Slam::InterpolateScanPose(double time)
{
  if (this->LogStates.empty())
    return this->Tworld;

  const double prevPoseTime = this->LogStates.back().Time;
  if (std::abs(time / (this->CurrentTime - prevPoseTime)) > this->MaxExtrapolationRatio)
  {
    PRINT_WARNING("Unable to interpolate scan pose from motion : extrapolation time is too far.");
    return this->Tworld;
  }

  return LinearInterpolation(this->LogStates.back().Isometry, this->Tworld, this->CurrentTime + time, prevPoseTime, this->CurrentTime);
}

//-----------------------------------------------------------------------------
void Slam::InitUndistortion()
{
  // Get 'time' field range
  double frameFirstTime = std::numeric_limits<double>::max();
  double frameLastTime  = std::numeric_limits<double>::lowest();
  for (auto k : this->UsableKeypoints)
  {
    for (const auto& point : *this->CurrentUndistortedKeypoints[k])
    {
      frameFirstTime = std::min(frameFirstTime, point.time);
      frameLastTime  = std::max(frameLastTime, point.time);
    }
  }

  // Update interpolator timestamps and reset transforms
  this->WithinFrameMotion.SetTimes(frameFirstTime, frameLastTime);
  this->WithinFrameMotion.SetTransforms(Eigen::Isometry3d::Identity(), Eigen::Isometry3d::Identity());

  // Check time values
  if (this->WithinFrameMotion.GetTimeRange() < 1e-6)
  {
    // If frame duration is 0, it means that the time field is constant and cannot be used.
    // We reset timestamps to 0, to ensure no time offset will be used.
    PRINT_WARNING("'time' field is not properly set (constant value) and cannot be used for undistortion.");
    this->WithinFrameMotion.SetTimes(0., 0.);
  }
  else if (this->WithinFrameMotion.GetTimeRange() > 10.)
  {
    // If frame duration is bigger than 10 seconds, it is probably wrongly set
    PRINT_WARNING("'time' field looks not properly set (frame duration > 10 s) and can lead to faulty undistortion.");
  }
}

//-----------------------------------------------------------------------------
void Slam::UndistortWithPoseMeasurement()
{
  if (this->PoseHasData())
  {
    // Get synchronized point pose relatively to frame
    ExternalSensors::PoseMeasurement synchPoseMeasCurrent; // Virtual measure with synchronized timestamp and calibration applied
    if (this->PoseManager->ComputeSynchronizedMeasureBase(this->CurrentTime, synchPoseMeasCurrent))
    {
      Eigen::Isometry3d invSynchPoseMeasCurrent = synchPoseMeasCurrent.Pose.inverse();
      // Undistort keypoints
      for (auto k : this->UsableKeypoints)
      {
        // Compute synchronized measures (not parallelizable)
        int nbPoints = this->CurrentUndistortedKeypoints[k]->size();
        std::vector<ExternalSensors::PoseMeasurement> synchMeas (nbPoints); // Virtual measures with synchronized timestamp and calibration applied
        // Sort points by time to speed up synchronization search
        std::sort(this->CurrentUndistortedKeypoints[k]->points.begin(), this->CurrentUndistortedKeypoints[k]->points.end(), [](const LidarPoint& pt1, const LidarPoint& pt2){return pt1.time < pt2.time;});
        int idxPt = 0;
        for (auto& point : *this->CurrentUndistortedKeypoints[k])
        {
          this->PoseManager->ComputeSynchronizedMeasureBase(this->CurrentTime + point.time, synchMeas[idxPt]);
          ++idxPt;
        }

        // Transform with computed measures (parallelized)
        #pragma omp parallel for num_threads(this->NbThreads)
        for (int i = 0; i < nbPoints; ++i)
        {
          auto& point = this->CurrentUndistortedKeypoints[k]->at(i);
          Utils::TransformPoint(point, invSynchPoseMeasCurrent * synchMeas[i].Pose);
        }
      }
    }
  }
  else
    PRINT_WARNING("External poses are empty : cannot use them to undistort input pointcloud")
}

//-----------------------------------------------------------------------------
void Slam::RefineUndistortion()
{
  // Get previously applied undistortion
  Eigen::Isometry3d previousBaseBegin = this->WithinFrameMotion.GetH0();
  Eigen::Isometry3d previousBaseEnd = this->WithinFrameMotion.GetH1();

  // Extrapolate first and last poses to update within frame motion interpolator
  Eigen::Isometry3d worldToBaseBegin = this->InterpolateScanPose(this->WithinFrameMotion.GetTime0());
  Eigen::Isometry3d worldToBaseEnd = this->InterpolateScanPose(this->WithinFrameMotion.GetTime1());
  Eigen::Isometry3d baseToWorld = this->Tworld.inverse();
  Eigen::Isometry3d newBaseBegin = baseToWorld * worldToBaseBegin;
  Eigen::Isometry3d newBaseEnd = baseToWorld * worldToBaseEnd;
  this->WithinFrameMotion.SetTransforms(newBaseBegin, newBaseEnd);

  // Init the interpolator to use to remove previous undistortion and apply updated one
  auto transformInterpolator = this->WithinFrameMotion;
  transformInterpolator.SetTransforms(newBaseBegin * previousBaseBegin.inverse(),
                                      newBaseEnd   * previousBaseEnd.inverse());

  // Refine undistortion of keypoints clouds
  for (auto k : this->UsableKeypoints)
  {
    int nbPoints = this->CurrentUndistortedKeypoints[k]->size();
    #pragma omp parallel for num_threads(this->NbThreads)
    for (int i = 0; i < nbPoints; ++i)
    {
      auto& point = this->CurrentUndistortedKeypoints[k]->at(i);
      Utils::TransformPoint(point, transformInterpolator(point.time));
    }
  }
}

//==============================================================================
//   Confidence estimators
//==============================================================================

//-----------------------------------------------------------------------------
void Slam::SetOverlapSamplingRatio (float _arg)
{
  // Clamp ratio beteen 0 and 1
  this->OverlapSamplingRatio = Utils::Clamp(_arg, 0.f, 1.f);

  // Reset overlap value if overlap computation is disabled
  if (this->OverlapSamplingRatio == 0.)
    this->OverlapEstimation = -1.f;
}

//-----------------------------------------------------------------------------
void Slam::EstimateOverlap()
{
  // Aggregate all input points into WORLD coordinates
  PointCloud::Ptr aggregatedPoints = this->GetRegisteredFrame();

  // Keep only the maps to use
  std::map<Keypoint, std::shared_ptr<RollingGrid>> mapsToUse;
  for (auto k : this->UsableKeypoints)
  {
    if (this->LocalMaps[k]->IsSubMapKdTreeValid())
      mapsToUse[k] = this->LocalMaps[k];
  }

  // Compute LCP like estimator
  // (see http://geometry.cs.ucl.ac.uk/projects/2014/super4PCS/ for more info)
  this->OverlapEstimation = Confidence::LCPEstimator(aggregatedPoints, mapsToUse, this->OverlapSamplingRatio, this->NbThreads);
  PRINT_VERBOSE(3, "Overlap : " << this->OverlapEstimation << ", estimated on : "
                                << static_cast<int>(aggregatedPoints->size() * this->OverlapSamplingRatio) << " points.");
}

//-----------------------------------------------------------------------------
void Slam::CheckMotionLimits()
{
  int nPoses = this->LogStates.size();
  if (nPoses == 0)
    return;

  // Extract number of poses to comply with the required window time, and relative time duration.
  double deltaTime = this->CurrentTime - this->LogStates.back().Time;
  double nextDeltaTime = FLT_MAX;
  // Index of the last pose that defines the window starting bound
  // The window ends on current pose
  auto startIt = this->LogStates.end();
  auto beforeBegin = this->LogStates.begin();
  --beforeBegin;
  --startIt;
  // If the time between current pose and the last pose is l.t. TimeWindowDuration, look for the window's starting bound pose
  if (deltaTime < this->TimeWindowDuration)
  {
    // Search an interval containing TimeWindowDuration : [deltaTime, nextDeltaTime]
    while (startIt != beforeBegin)
    {
      deltaTime = nextDeltaTime;
      nextDeltaTime = this->CurrentTime - startIt->Time;
      if (nextDeltaTime >= this->TimeWindowDuration)
        break;
      --startIt;
    }

    // If startIt lays before first element, no interval containing TimeWindowDuration was found, the oldest logged pose is taken
    if (startIt == beforeBegin)
    {
      PRINT_WARNING("Not enough logged trajectory poses to get the required time window to estimate velocity, using a smaller time window of "
                    << nextDeltaTime << "s")
      startIt = this->LogStates.begin();
    }

    // Choose which bound of the interval is the best window's starting bound
    else if (std::abs(deltaTime - this->TimeWindowDuration) < std::abs(nextDeltaTime - this->TimeWindowDuration))
      ++startIt;

    // Actualize deltaTime with the best startIndex
    deltaTime = this->CurrentTime - startIt->Time;
  }
  // If the time between current pose and the last pose is g.t. TimeWindowDuration, take the last pose as window's starting bound
  else
    PRINT_WARNING("The required time window is too short to estimate velocity, using motion since last pose")

  this->ComplyMotionLimits = true;

  // Compute transform between the two pose bounds of the window
  Eigen::Isometry3d TWindow = startIt->Isometry.inverse() * this->Tworld;

  // Compute angular part
  // NOTE : It is not possible to detect an angle greater than PI,
  // the detectable velocity and acceleration are limited on deltaTime
  // Rotation angle in [0, 2pi]
  float angle = Eigen::AngleAxisd(TWindow.linear()).angle();
  // Rotation angle in [0, pi]
  if (angle > M_PI)
    angle = 2 * M_PI - angle;
  angle = Utils::Rad2Deg(angle);
  // Compute linear part
  float distance = TWindow.translation().norm();

  // Compute velocity
  Eigen::Array2f velocity = {distance / deltaTime, angle / deltaTime};
  SET_COUT_FIXED_PRECISION(3);
  // Print local velocity
  PRINT_VERBOSE(3, "Velocity     = " << std::setw(6) << velocity[0] << " m/s,  "
                                     << std::setw(6) << velocity[1] << " °/s")
  RESET_COUT_FIXED_PRECISION;

  if (this->NbrFrameProcessed >= 2)
  {
    // Compute local acceleration in BASE
    Eigen::Array2f acceleration = (velocity - this->PreviousVelocity) / deltaTime;
    // Print local acceleration
    SET_COUT_FIXED_PRECISION(3);
    PRINT_VERBOSE(3, "Acceleration = " << std::setw(6) << acceleration[0] << " m/s2, "
                                       << std::setw(6) << acceleration[1] << " °/s2")
    RESET_COUT_FIXED_PRECISION;

    // Check velocity compliance
    bool complyVelocityLimits = (velocity < this->VelocityLimits).all();
    // Check acceleration compliance
    bool complyAccelerationLimits = (acceleration.abs() < this->AccelerationLimits).all();

    // Set ComplyMotionLimits
    this->ComplyMotionLimits = complyVelocityLimits && complyAccelerationLimits;
  }

  this->PreviousVelocity = velocity;

  if (!this->ComplyMotionLimits)
    PRINT_WARNING("The pose does not comply with the motion limitations. Lidar SLAM may have failed.")
}

//==============================================================================
//   Transformation helpers
//==============================================================================

//-----------------------------------------------------------------------------
Slam::PointCloud::Ptr Slam::TransformPointCloud(PointCloud::ConstPtr cloud,
                                                const Eigen::Isometry3d& tf,
                                                const std::string& frameId) const
{
  // Copy all fields and set frame ID
  PointCloud::Ptr transformedCloud(new PointCloud);
  pcl::copyPointCloud(*cloud, *transformedCloud);
  transformedCloud->header.frame_id = frameId;

  // Transform each point inplace in parallel
  int nbPoints = transformedCloud->size();
  #pragma omp parallel for num_threads(this->NbThreads)
  for (int i = 0; i < nbPoints; ++i)
  {
    auto& point = transformedCloud->at(i);
    Utils::TransformPoint(point, tf);
  }
  return transformedCloud;
}

//-----------------------------------------------------------------------------
Slam::PointCloud::Ptr Slam::AggregateFrames(const std::vector<PointCloud::Ptr>& frames, bool worldCoordinates) const
{
  PointCloud::Ptr aggregatedFrames(new PointCloud);
  aggregatedFrames->header = Utils::BuildPclHeader(this->CurrentFrames[0]->header.stamp,
                                                   worldCoordinates ? this->WorldFrameId : this->BaseFrameId,
                                                   this->NbrFrameProcessed);

  // Loop over frames of input
  for (const auto& frame: frames)
  {
    // If the frame is empty, ignore it
    if (frame->empty())
      continue;

    // Add frame to aggregated output
    int startIdx = aggregatedFrames->size();
    int endIdx = startIdx + frame->size();
    *aggregatedFrames += *frame;

    // Modify point-wise time offsets to match header.stamp
    // And transform points from LIDAR to BASE or WORLD coordinate system
    double timeOffset = Utils::PclStampToSec(frame->header.stamp) - Utils::PclStampToSec(aggregatedFrames->header.stamp);
    Eigen::Isometry3d baseToLidar = this->GetBaseToLidarOffset(frame->front().device_id);

    // Rigid transform from LIDAR to BASE then undistortion from BASE to WORLD
    if (worldCoordinates && this->Undistortion)
    {
      auto transformInterpolator = this->WithinFrameMotion;
      transformInterpolator.SetTransforms(this->Tworld * this->WithinFrameMotion.GetH0() * baseToLidar,
                                          this->Tworld * this->WithinFrameMotion.GetH1() * baseToLidar);
      #pragma omp parallel for num_threads(this->NbThreads)
      for (int i = startIdx; i < endIdx; ++i)
      {
        auto& point = aggregatedFrames->at(i);
        point.time += timeOffset;
        Utils::TransformPoint(point, transformInterpolator(point.time));
      }
    }

    // Rigid transform from LIDAR to BASE or WORLD coordinate system
    else
    {
      // Get rigid transform to apply
      Eigen::Isometry3d tf = worldCoordinates ? this->Tworld * baseToLidar : baseToLidar;
      // If transform to apply is identity, avoid much work
      if (tf.isApprox(Eigen::Isometry3d::Identity()))
      {
        #pragma omp parallel for num_threads(this->NbThreads)
        for (int i = startIdx; i < endIdx; ++i)
          aggregatedFrames->at(i).time += timeOffset;
      }
      // If transform is non trivial, run transformation
      else
      {
        #pragma omp parallel for num_threads(this->NbThreads)
        for (int i = startIdx; i < endIdx; ++i)
        {
          auto& point = aggregatedFrames->at(i);
          point.time += timeOffset;
          Utils::TransformPoint(point, tf);
        }
      }
    }
  }

  return aggregatedFrames;
}

//==============================================================================
//   External sensors
//==============================================================================

//-----------------------------------------------------------------------------
void Slam::InitWheelOdom()
{
  this->WheelOdomManager = std::make_shared<ExternalSensors::WheelOdometryManager>(0.,
                                                                                   this->SensorTimeOffset,
                                                                                   this->SensorTimeThreshold,
                                                                                   this->SensorMaxMeasures,
                                                                                   this->Verbosity >= 3);
}

//-----------------------------------------------------------------------------
void Slam::InitGravity()
{
  this->GravityManager = std::make_shared<ExternalSensors::ImuGravityManager>(0.,
                                                                              this->SensorTimeOffset,
                                                                              this->SensorTimeThreshold,
                                                                              this->SensorMaxMeasures,
                                                                              this->Verbosity >= 3);
}

//-----------------------------------------------------------------------------
void Slam::InitLandmarkManager(int id)
{
  this->LandmarksManagers[id] = ExternalSensors::LandmarkManager(this->SensorTimeOffset,
                                                                 this->SensorTimeThreshold,
                                                                 this->SensorMaxMeasures,
                                                                 this->LandmarkPositionOnly,
                                                                 this->Verbosity >= 3);
  this->LandmarksManagers[id].SetWeight(this->LandmarkWeight);
  this->LandmarksManagers[id].SetSaturationDistance(this->LandmarkSaturationDistance);
  // The calibration can be modified afterwards
  this->LandmarksManagers[id].SetCalibration(this->LmDetectorCalibration);
}

//-----------------------------------------------------------------------------
void Slam::InitGps()
{
  this->GpsManager = std::make_shared<ExternalSensors::GpsManager>(this->SensorTimeOffset,
                                                                   this->SensorTimeThreshold,
                                                                   this->SensorMaxMeasures,
                                                                   this->Verbosity >= 3);
}

//-----------------------------------------------------------------------------
void Slam::InitPoseSensor()
{
  this->PoseManager = std::make_shared<ExternalSensors::PoseManager>(0.,
                                                                     this->SensorTimeOffset,
                                                                     this->SensorTimeThreshold,
                                                                     this->SensorMaxMeasures,
                                                                     this->Verbosity >= 3);
}

// Sensor data
//-----------------------------------------------------------------------------

// Wheel odometer
//-----------------------------------------------------------------------------
void Slam::AddWheelOdomMeasurement(const ExternalSensors::WheelOdomMeasurement& om)
{
  if (!this->WheelOdomManager)
    this->InitWheelOdom();
  this->WheelOdomManager->AddMeasurement(om);
}

// IMU gravity
//-----------------------------------------------------------------------------
void Slam::AddGravityMeasurement(const ExternalSensors::GravityMeasurement& gm)
{
  if (!this->GravityManager)
    this->InitGravity();
  this->GravityManager->AddMeasurement(gm);
}

// Landmark detector
//-----------------------------------------------------------------------------
void Slam::AddLandmarkMeasurement(const ExternalSensors::LandmarkMeasurement& lm, int id)
{
  if (!this->LandmarksManagers.count(id))
    this->InitLandmarkManager(id);
  this->LandmarksManagers[id].AddMeasurement(lm);
}

//-----------------------------------------------------------------------------
void Slam::AddLandmarkManager(int id, const Eigen::Vector6d& absolutePose, const Eigen::Matrix6d& absolutePoseCovariance)
{
  if (!this->LandmarksManagers.count(id))
    this->InitLandmarkManager(id);
  this->LandmarksManagers[id].SetAbsolutePose(absolutePose, absolutePoseCovariance);
}

//-----------------------------------------------------------------------------
void Slam::SetLmDetectorCalibration(const Eigen::Isometry3d& calib)
{
  this->LmDetectorCalibration = calib;
  for (auto& idLm : this->LandmarksManagers)
    idLm.second.SetCalibration(calib);
}

//-----------------------------------------------------------------------------
bool Slam::LmCanBeUsedLocally()
{
  bool lmCanBeUsed = false;
  for (auto& idLm : this->LandmarksManagers)
  {
    if (idLm.second.CanBeUsedLocally())
    {
      lmCanBeUsed = true;
      break;
    }
  }
  return lmCanBeUsed;
}

//-----------------------------------------------------------------------------
bool Slam::LmHasData()
{
  bool lmHasData = false;
  for (auto& idLm : this->LandmarksManagers)
  {
    if (idLm.second.HasData())
    {
      lmHasData = true;
      break;
    }
  }
  return lmHasData;
}

// GPS
//-----------------------------------------------------------------------------
void Slam::AddGpsMeasurement(const ExternalSensors::GpsMeasurement& gpsMeas)
{
  if (!this->GpsManager)
    this->InitGps();
  this->GpsManager->AddMeasurement(gpsMeas);
}

//-----------------------------------------------------------------------------
void Slam::SetGpsCalibration(const Eigen::Isometry3d& calib)
{
  if (!this->GpsManager)
    this->InitGps();
  this->GpsManager->SetCalibration(calib);
}

//-----------------------------------------------------------------------------
Eigen::Isometry3d Slam::GetGpsCalibration()
{
  if (!this->GpsManager)
  {
    PRINT_ERROR("Cannot get GPS calibration : GPS not enabled")
    return Eigen::Isometry3d::Identity();
  }
  return this->GpsManager->GetCalibration();
}

//-----------------------------------------------------------------------------
Eigen::Isometry3d Slam::GetGpsOffset()
{
  if (!this->GpsManager)
  {
    PRINT_ERROR("Cannot get GPS offset : GPS not enabled")
    return Eigen::Isometry3d::Identity();
  }
  return this->GpsManager->GetOffset();
}

//-----------------------------------------------------------------------------
bool Slam::CalibrateWithGps()
{
  if (!this->GpsHasData())
  {
    PRINT_ERROR("Cannot get GPS offset : GPS not enabled or GPS data not available")
    return false;
  }

  // The search for a synchronized data is one-time so we
  // don't want to keep track of time for next searches (see ComputeSynchronizedMeasure)
  bool trackTime = false;

  // Initialize GPS offset with first synchronized measurements
  // The first graph optimizations will mainly correct the orientations
  Eigen::Isometry3d offset = Eigen::Isometry3d::Identity();
  for (auto& s : this->LogStates)
  {
    ExternalSensors::GpsMeasurement gpsSynchMeasure; // Virtual measure with synchronized timestamp and no offset applied
    if (this->GpsManager->ComputeSynchronizedMeasure(s.Time, gpsSynchMeasure, trackTime))
    {
      offset.translation() = (s.Isometry * this->GpsManager->GetCalibration()).translation() - gpsSynchMeasure.Position;
      break;
    }
  }
  this->GpsManager->SetOffset(offset);

  // Optimize the graph : add lidar states and link with fixed gps states
  if (!this->OptimizeGraph())
    return false;

  // Reset poses in odom frame (first Lidar pose)
  Eigen::Isometry3d firstInverse = this->LogStates.front().Isometry.inverse();
  for (auto& s : this->LogStates)
  {
    // Rotate covariance
    Eigen::Vector6d initPose = Utils::IsometryToXYZRPY(s.Isometry);
    CeresTools::RotateCovariance(initPose, s.Covariance, firstInverse, true); // new = first^-1 * init
    // Transform pose
    s.Isometry = firstInverse * s.Isometry;
  }

  // Update the maps and the pose with new trajectory
  this->UpdateMaps();
  this->SetWorldTransformFromGuess(this->LogStates.back().Isometry);

  // Set refined offset : first Lidar pose + initial offset
  firstInverse.translation() += offset.translation();
  this->GpsManager->SetOffset(firstInverse);

  return true;
}

// Pose
//-----------------------------------------------------------------------------
void Slam::AddPoseMeasurement(const ExternalSensors::PoseMeasurement& pm)
{
  if (!this->PoseManager)
    this->InitPoseSensor();
  this->PoseManager->AddMeasurement(pm);
}

//-----------------------------------------------------------------------------
void Slam::SetPoseCalibration(const Eigen::Isometry3d& calib)
{
  if (!this->PoseManager)
    this->InitPoseSensor();
  this->PoseManager->SetCalibration(calib);
}

// Sensors' parameters
//-----------------------------------------------------------------------------

#define ExtSensorMacro(inFuncProto) \
{ \
  if (this->WheelOdomManager) \
    this->WheelOdomManager->inFuncProto; \
  if (this->GravityManager) \
    this->GravityManager->inFuncProto; \
  for (auto& idLm : this->LandmarksManagers) \
    idLm.second.inFuncProto; \
  if (this->GpsManager) \
    this->GpsManager->inFuncProto; \
  if (this->PoseManager) \
    this->PoseManager->inFuncProto; \
}

//-----------------------------------------------------------------------------
void Slam::ResetSensors(bool emptyMeasurements)
{
  ExtSensorMacro(Reset(emptyMeasurements))
}

//-----------------------------------------------------------------------------
void Slam::SetSensorTimeOffset(double timeOffset)
{
  ExtSensorMacro(SetTimeOffset(timeOffset))
  this->SensorTimeOffset = timeOffset;
}

//-----------------------------------------------------------------------------
void Slam::SetSensorTimeThreshold(double thresh)
{
  ExtSensorMacro(SetTimeThreshold(thresh))
  this->SensorTimeThreshold = thresh;
}

//-----------------------------------------------------------------------------
void Slam::SetSensorMaxMeasures(unsigned int max)
{
  ExtSensorMacro(SetMaxMeasures(max))
  this->SensorMaxMeasures = max;
}

// Wheel odometer
//-----------------------------------------------------------------------------
double Slam::GetWheelOdomWeight() const
{
  if(this->WheelOdomManager)
    return this->WheelOdomManager->GetWeight();
  PRINT_ERROR("Wheel odometer has not been set : can't get wheel odom weight")
  return 0.;
}

//-----------------------------------------------------------------------------
void Slam::SetWheelOdomWeight(double weight)
{
  if(!this->WheelOdomManager)
    this->InitWheelOdom();
  this->WheelOdomManager->SetWeight(weight);
}

//-----------------------------------------------------------------------------
bool Slam::GetWheelOdomRelative() const
{
  if(this->WheelOdomManager)
    return this->WheelOdomManager->GetRelative();
  PRINT_ERROR("Wheel odometer has not been set : can't get wheel odom relative boolean")
  return false;
}

//-----------------------------------------------------------------------------
void Slam::SetWheelOdomRelative(bool isRelative)
{
  if(!this->WheelOdomManager)
    this->InitWheelOdom();
  this->WheelOdomManager->SetRelative(isRelative);
}

// IMU gravity
//-----------------------------------------------------------------------------
double Slam::GetGravityWeight() const
{
  if(this->GravityManager)
    return this->GravityManager->GetWeight();
  PRINT_ERROR("IMU has not been set : can't get IMU weight")
  return 0.;
}

//-----------------------------------------------------------------------------
void Slam::SetGravityWeight(double weight)
{
  if(!this->GravityManager)
    this->InitGravity();
  this->GravityManager->SetWeight(weight);
}

// Landmark detector
//-----------------------------------------------------------------------------
void Slam::SetLandmarkWeight(double weight)
{
  for (auto& idLm : this->LandmarksManagers)
    idLm.second.SetWeight(weight);
  this->LandmarkWeight = weight;
}

//-----------------------------------------------------------------------------
void Slam::SetLandmarkSaturationDistance(float dist)
{
  for (auto& idLm : this->LandmarksManagers)
    idLm.second.SetSaturationDistance(dist);
  this->LandmarkSaturationDistance = dist;
}

//-----------------------------------------------------------------------------
void Slam::SetLandmarkPositionOnly(bool positionOnly)
{
  for (auto& idLm : this->LandmarksManagers)
    idLm.second.SetPositionOnly(positionOnly);
  this->LandmarkPositionOnly = positionOnly;
}

//-----------------------------------------------------------------------------
void Slam::SetLandmarkCovarianceRotation(bool rotate)
{
  for (auto& idLm : this->LandmarksManagers)
    idLm.second.SetCovarianceRotation(rotate);
  this->LandmarkCovarianceRotation = rotate;
}

// Pose
//-----------------------------------------------------------------------------
double Slam::GetPoseWeight() const
{
  if (this->PoseManager)
    return this->PoseManager->GetWeight();
  PRINT_ERROR("Pose sensor has not been set : can't get pose weight")
  return 0.;
}

//-----------------------------------------------------------------------------
void Slam::SetPoseWeight(double weight)
{
  if (!this->PoseManager)
    this->InitPoseSensor();
  this->PoseManager->SetWeight(weight);
}

//==============================================================================
//   Keypoints extraction parameters setting
//==============================================================================

//-----------------------------------------------------------------------------
std::map<uint8_t, Slam::KeypointExtractorPtr> Slam::GetKeyPointsExtractors() const
{
  return this->KeyPointsExtractors;
}
void Slam::SetKeyPointsExtractors(const std::map<uint8_t, KeypointExtractorPtr>& extractors)
{
  this->KeyPointsExtractors = extractors;
}

//-----------------------------------------------------------------------------
Slam::KeypointExtractorPtr Slam::GetKeyPointsExtractor(uint8_t deviceId) const
{
  return this->KeyPointsExtractors.count(deviceId) ? this->KeyPointsExtractors.at(deviceId) : KeypointExtractorPtr();
}
void Slam::SetKeyPointsExtractor(KeypointExtractorPtr extractor, uint8_t deviceId)
{
  this->KeyPointsExtractors[deviceId] = extractor;
}

//-----------------------------------------------------------------------------
Eigen::Isometry3d Slam::GetBaseToLidarOffset(uint8_t deviceId) const
{
  return this->BaseToLidarOffsets.count(deviceId) ? this->BaseToLidarOffsets.at(deviceId) : Eigen::UnalignedIsometry3d::Identity();
}
void Slam::SetBaseToLidarOffset(const Eigen::Isometry3d& transform, uint8_t deviceId)
{
  this->BaseToLidarOffsets[deviceId] = transform;
}

//==============================================================================
//   Rolling grids parameters setting
//==============================================================================

//-----------------------------------------------------------------------------
void Slam::ClearMaps()
{
  for (auto kmap: this->LocalMaps)
    kmap.second->Reset();
}

//-----------------------------------------------------------------------------
double Slam::GetVoxelGridDecayingThreshold() const
{
  return this->LocalMaps.begin()->second->GetDecayingThreshold();
}

//-----------------------------------------------------------------------------
void Slam::SetVoxelGridDecayingThreshold(double decay)
{
  for (auto k : this->UsableKeypoints)
    this->LocalMaps[k]->SetDecayingThreshold(decay);
}

//-----------------------------------------------------------------------------
SamplingMode Slam::GetVoxelGridSamplingMode(Keypoint k) const
{
  return this->LocalMaps.at(k)->GetSampling();
}

//-----------------------------------------------------------------------------
void Slam::SetVoxelGridSamplingMode(Keypoint k, SamplingMode sm)
{
  this->LocalMaps[k]->SetSampling(sm);
}

//-----------------------------------------------------------------------------
void Slam::SetVoxelGridLeafSize(Keypoint k, double size)
{
  this->LocalMaps[k]->SetLeafSize(size);
}

//-----------------------------------------------------------------------------
double Slam::GetVoxelGridLeafSize(Keypoint k) const
{
  return this->LocalMaps.at(k)->GetLeafSize();
}

//-----------------------------------------------------------------------------
void Slam::SetVoxelGridSize(int size)
{
  for (auto k : this->UsableKeypoints)
    this->LocalMaps[k]->SetGridSize(size);
}

//-----------------------------------------------------------------------------
void Slam::SetVoxelGridResolution(double resolution)
{
  for (auto k : this->UsableKeypoints)
    this->LocalMaps[k]->SetVoxelResolution(resolution);
}

//-----------------------------------------------------------------------------
void Slam::SetVoxelGridMinFramesPerVoxel(unsigned int minFrames)
{
  for (auto k : this->UsableKeypoints)
    this->LocalMaps[k]->SetMinFramesPerVoxel(minFrames);
}

//==============================================================================
//   Memory parameters setting
//==============================================================================

//-----------------------------------------------------------------------------
void Slam::SetLoggingTimeout(double lMax)
{
  this->LoggingTimeout = lMax;
  double currentTime = this->LogStates.back().Time;
  auto itSt = this->LogStates.begin();
  while (currentTime - itSt->Time > lMax && this->LogStates.size() > 2)
  {
    ++itSt;
    this->LogStates.pop_front();
  }
}

} // end of LidarSlam namespace