Autonomy Software C++ 24.5.1
Welcome to the Autonomy Software repository of the Mars Rover Design Team (MRDT) at Missouri University of Science and Technology (Missouri S&T)! API reference contains the source code and other resources for the development of the autonomy software for our Mars rover. The Autonomy Software project aims to compete in the University Rover Challenge (URC) by demonstrating advanced autonomous capabilities and robust navigation algorithms.
Loading...
Searching...
No Matches
LiDARHandler Class Reference

The LiDARHandler class manages runtime queries against a LiDAR point cloud database for autonomy navigation. More...

#include <LiDARHandler.h>

Classes

struct  PointFilter
 Struct for filtering LiDAR points during queries. More...
 
struct  PointRow
 Struct representing a single LiDAR point row from the database. More...
 

Public Member Functions

 LiDARHandler ()
 Construct a new LiDARHandler::LiDARHandler object.
 
 LiDARHandler (const LiDARHandler &pOther)=delete
 
LiDARHandleroperator= (const LiDARHandler &pOther)=delete
 
 ~LiDARHandler ()
 Destroy the LiDARHandler::LiDARHandler object.
 
bool OpenDB (const std::string &szDBPath)
 Initializes the LiDARHandler by opening the DuckDB database.
 
bool CloseDB ()
 Closes the currently open LiDAR DuckDB connection.
 
std::vector< PointRowGetLiDARData (const PointFilter &stPointFilter)
 Retrieves LiDAR data points from DuckDB based on the specified filter.
 
bool DeclareLiDARObstacle (const geoops::UTMCoordinate &stPoint, double dRadius)
 Modifies all LiDAR points in radius to reflect bad terrain.
 
bool IsDBOpen ()
 Checks if the database is currently open.
 

Private Member Functions

template<typename T >
void AddRangeFilter (std::vector< std::string > &vClauses, duckdb::vector< duckdb::Value > &vBindValues, const char *pColumn, const std::optional< PointFilter::Range< T > > &stdOptRange)
 Adds a range filter to the SQL query clauses and dynamically bound values.
 

Private Attributes

std::unique_ptr< duckdb::DuckDBm_pDB
 
bool m_bIsDBOpen
 
std::shared_mutex m_muQueryMutex
 

Detailed Description

The LiDARHandler class manages runtime queries against a LiDAR point cloud database for autonomy navigation.

Author
clayjay3 (clayt.nosp@m.onra.nosp@m.ycowe.nosp@m.n@gm.nosp@m.ail.c.nosp@m.om)
Date
2026-01-22

Constructor & Destructor Documentation

◆ LiDARHandler()

LiDARHandler::LiDARHandler ( )

Construct a new LiDARHandler::LiDARHandler object.

Author
ClayJay3 (clayt.nosp@m.onra.nosp@m.ycowe.nosp@m.n@gm.nosp@m.ail.c.nosp@m.om)
Date
2025-07-13
28{
29 // Ensure smart pointers are null natively.
30 m_pDB = nullptr;
31 m_bIsDBOpen = false;
32}

◆ ~LiDARHandler()

LiDARHandler::~LiDARHandler ( )

Destroy the LiDARHandler::LiDARHandler object.

Author
ClayJay3 (clayt.nosp@m.onra.nosp@m.ycowe.nosp@m.n@gm.nosp@m.ail.c.nosp@m.om), Eli Byrd (edbgk.nosp@m.k@ms.nosp@m.t.edu)
Date
2025-05-20
41{
42 this->CloseDB(); // Ensure the database is closed on destruction.
43}
bool CloseDB()
Closes the currently open LiDAR DuckDB connection.
Definition LiDARHandler.cpp:112
Here is the call graph for this function:

Member Function Documentation

◆ OpenDB()

bool LiDARHandler::OpenDB ( const std::string &  szDBPath)

Initializes the LiDARHandler by opening the DuckDB database.

This method securely opens the DuckDB file and instantiates a persistent connection object. The connection is opened in default Read/Write mode to allow the autonomy system to dynamically modify the terrain and declare obstacles at runtime.

Parameters
szDBPathRelative or absolute path to the DuckDB database file.
Returns
true - If the database was successfully opened.
false - If there was an error opening the database.
Author
ClayJay3 (clayt.nosp@m.onra.nosp@m.ycowe.nosp@m.n@gm.nosp@m.ail.c.nosp@m.om)
Date
2026-05-01
62{
63 // Check if the file actually exists on disk before doing anything.
64 if (!std::filesystem::exists(szDBPath))
65 {
66 LOG_ERROR(logging::g_qSharedLogger, "Failed to open DuckDB: File does not exist at '{}'", szDBPath);
67 return false;
68 }
69
70 // Acquire a write lock on the mutex to ensure thread safety.
71 std::unique_lock<std::shared_mutex> lkWriteLock(m_muQueryMutex);
72
73 // Reset existing connection if already open (fixing the previous race condition)
74 if (m_bIsDBOpen)
75 {
76 LOG_WARNING(logging::g_qSharedLogger, "Database is already open. Closing existing connection before opening a new one.");
77 m_pDB.reset();
78 m_bIsDBOpen = false;
79 }
80
81 try
82 {
83 // Instantiate the DuckDB instance.
84 m_pDB = std::make_unique<duckdb::DuckDB>(szDBPath);
85 }
86 catch (const duckdb::Exception& e)
87 {
88 LOG_ERROR(logging::g_qSharedLogger, "Failed to open DuckDB at '{}': {}", szDBPath, e.what());
89 return false;
90 }
91 catch (const std::exception& e)
92 {
93 LOG_ERROR(logging::g_qSharedLogger, "Standard exception while opening DuckDB: {}", e.what());
94 return false;
95 }
96
97 m_bIsDBOpen = true;
98 LOG_INFO(logging::g_qSharedLogger, "Successfully opened DuckDB analytics engine at '{}'.", szDBPath);
99
100 return true;
101}
Definition duckdb.hpp:578
Here is the caller graph for this function:

◆ CloseDB()

bool LiDARHandler::CloseDB ( )

Closes the currently open LiDAR DuckDB connection.

Returns
true - If the database was successfully closed.
false - If there was an error closing the database.
Author
ClayJay3 (clayt.nosp@m.onra.nosp@m.ycowe.nosp@m.n@gm.nosp@m.ail.c.nosp@m.om)
Date
2025-07-13
113{
114 std::unique_lock<std::shared_mutex> lkWriteLock(m_muQueryMutex);
115
116 if (m_bIsDBOpen)
117 {
118 // Smart pointers automatically release resources and close database locks
119 // when reset. This avoids SQLite's manual finalize() memory leak issues.
120 m_pDB.reset();
121 m_bIsDBOpen = false;
122 }
123
124 return true;
125}
Here is the caller graph for this function:

◆ GetLiDARData()

std::vector< LiDARHandler::PointRow > LiDARHandler::GetLiDARData ( const PointFilter stPointFilter)

Retrieves LiDAR data points from DuckDB based on the specified filter.

Parameters
stPointFilter- The filter criteria to apply when querying LiDAR data.
Returns
std::vector<LiDARHandler::PointRow> - Queried rows from the database.
Author
ClayJay3 (clayt.nosp@m.onra.nosp@m.ycowe.nosp@m.n@gm.nosp@m.ail.c.nosp@m.om)
Date
2025-07-13
137{
138 std::shared_lock<std::shared_mutex> lkReadLock(m_muQueryMutex);
139 std::chrono::time_point<std::chrono::high_resolution_clock> tmStartTime = std::chrono::high_resolution_clock::now();
140
141 if (!m_bIsDBOpen)
142 {
143 LOG_ERROR(logging::g_qSharedLogger, "Database is not open.");
144 return {};
145 }
146
147 // Build dynamic WHERE clauses. DuckDB binds values natively into vectors.
148 std::vector<std::string> vClauses;
150
151 // Spatial bounds are applied directly to the columns. DuckDB uses underlying
152 // block Zonemaps to instantly skip unneeded file blocks on disk.
153 vClauses.emplace_back("p.easting BETWEEN ? AND ?");
154 vBindValues.push_back(duckdb::Value(stPointFilter.dEasting - stPointFilter.dRadius));
155 vBindValues.push_back(duckdb::Value(stPointFilter.dEasting + stPointFilter.dRadius));
156
157 vClauses.emplace_back("p.northing BETWEEN ? AND ?");
158 vBindValues.push_back(duckdb::Value(stPointFilter.dNorthing - stPointFilter.dRadius));
159 vBindValues.push_back(duckdb::Value(stPointFilter.dNorthing + stPointFilter.dRadius));
160
161 // Optional classification filter
162 if (stPointFilter.szClassification && !stPointFilter.szClassification->empty())
163 {
164 vClauses.emplace_back("c.label = ?");
165 vBindValues.push_back(duckdb::Value(*stPointFilter.szClassification));
166 }
167
168 // Add optional filters for metrics using COALESCE to safely treat NULL edge-points as 0.0.
169 this->AddRangeFilter(vClauses, vBindValues, "COALESCE(p.normal_x, 0.0)", stPointFilter.dNormalX);
170 this->AddRangeFilter(vClauses, vBindValues, "COALESCE(p.normal_y, 0.0)", stPointFilter.dNormalY);
171 this->AddRangeFilter(vClauses, vBindValues, "COALESCE(p.normal_z, 0.0)", stPointFilter.dNormalZ);
172 this->AddRangeFilter(vClauses, vBindValues, "COALESCE(p.slope, 0.0)", stPointFilter.dSlope);
173 this->AddRangeFilter(vClauses, vBindValues, "COALESCE(p.rough, 0.0)", stPointFilter.dRoughness);
174 this->AddRangeFilter(vClauses, vBindValues, "COALESCE(p.curvature, 0.0)", stPointFilter.dCurvature);
175 this->AddRangeFilter(vClauses, vBindValues, "COALESCE(p.trav_score, 0.0)", stPointFilter.dTraversalScore);
176
177 // Construct final SQL query string.
178 std::ostringstream stdOSS;
179 stdOSS << "SELECT p.id, p.easting, p.northing, p.altitude, z.label, c.label,"
180 << " COALESCE(p.normal_x, 0.0), COALESCE(p.normal_y, 0.0), COALESCE(p.normal_z, 0.0),"
181 << " COALESCE(p.slope, 0.0), COALESCE(p.rough, 0.0), COALESCE(p.curvature, 0.0), COALESCE(p.trav_score, 0.0)"
182 << " FROM ProcessedLiDARPoints AS p"
183 << " LEFT JOIN Zones AS z ON p.zone_id = z.id"
184 << " LEFT JOIN Classifications AS c ON p.class_code = c.code"
185 << " WHERE ";
186
187 for (size_t siIter = 0; siIter < vClauses.size(); ++siIter)
188 {
189 if (siIter > 0)
190 stdOSS << " AND ";
191 stdOSS << vClauses[siIter];
192 }
193
194 std::vector<PointRow> vResults;
195
196 try
197 {
198 // Thread-local connection to avoid stepping on pending chunk states
199 duckdb::Connection stLocalConn(*m_pDB);
200
201 // DuckDB Prepared Statements protect against injections and compile the plan
202 duckdb::unique_ptr<duckdb::PreparedStatement> stPreparedStmt = stLocalConn.Prepare(stdOSS.str());
203 if (stPreparedStmt->HasError())
204 {
205 LOG_ERROR(logging::g_qSharedLogger, "Failed to prepare DuckDB SQL: {}", stPreparedStmt->GetError());
206 return {};
207 }
208
209 // Execute the query passing the bound values
210 duckdb::unique_ptr<duckdb::QueryResult> stResult = stPreparedStmt->Execute(vBindValues);
211 if (stResult->HasError())
212 {
213 LOG_ERROR(logging::g_qSharedLogger, "Execution Error: {}", stResult->GetError());
214 return {};
215 }
216
217 // DuckDB extracts data in vector chunks. Iterating via Fetch() is extremely performant
218 // and naturally manages memory without locking threads.
219 while (duckdb::unique_ptr<duckdb::DataChunk> stChunk = stResult->Fetch())
220 {
221 size_t siRows = stChunk->size();
222 for (size_t siIter = 0; siIter < siRows; siIter++)
223 {
224 PointRow stRow;
225
226 // Extract native datatypes directly from the memory chunk.
227 stRow.nID = stChunk->GetValue(0, siIter).GetValue<int32_t>();
228 stRow.dEasting = stChunk->GetValue(1, siIter).GetValue<double>();
229 stRow.dNorthing = stChunk->GetValue(2, siIter).GetValue<double>();
230 stRow.dAltitude = stChunk->GetValue(3, siIter).IsNull() ? 0.0 : stChunk->GetValue(3, siIter).GetValue<double>();
231
232 duckdb::Value valZone = stChunk->GetValue(4, siIter);
233 stRow.szZone = valZone.IsNull() ? "Unknown" : valZone.GetValue<std::string>();
234
235 duckdb::Value valClass = stChunk->GetValue(5, siIter);
236 stRow.szClassification = valClass.IsNull() ? "Unclassified" : valClass.GetValue<std::string>();
237
238 // Metrics are guaranteed to be non-null due to the COALESCE in the SELECT clause.
239 stRow.dNormalX = stChunk->GetValue(6, siIter).GetValue<double>();
240 stRow.dNormalY = stChunk->GetValue(7, siIter).GetValue<double>();
241 stRow.dNormalZ = stChunk->GetValue(8, siIter).GetValue<double>();
242 stRow.dSlope = stChunk->GetValue(9, siIter).GetValue<double>();
243 stRow.dRoughness = stChunk->GetValue(10, siIter).GetValue<double>();
244 stRow.dCurvature = stChunk->GetValue(11, siIter).GetValue<double>();
245 stRow.dTraversalScore = stChunk->GetValue(12, siIter).GetValue<double>();
246
247 vResults.push_back(stRow);
248 }
249 }
250 }
251 catch (const duckdb::Exception& e)
252 {
253 LOG_ERROR(logging::g_qSharedLogger, "DuckDB threw an exception in GetLiDARData: {}", e.what());
254 return {};
255 }
256 catch (const std::exception& e)
257 {
258 LOG_ERROR(logging::g_qSharedLogger, "A standard exception was thrown in GetLiDARData: {}", e.what());
259 return {};
260 }
261
262 std::chrono::time_point<std::chrono::high_resolution_clock> tmEndTime = std::chrono::high_resolution_clock::now();
263 double dQueryTime = std::chrono::duration<double>(tmEndTime - tmStartTime).count();
264
265 if (dQueryTime > 0.2)
266 {
267 LOG_WARNING(logging::g_qSharedLogger, "Query took {:.2f} seconds to execute.", dQueryTime);
268 }
269 else
270 {
271 LOG_DEBUG(logging::g_qSharedLogger, "Query took {} seconds to execute.", dQueryTime);
272 }
273
274 if (vResults.empty())
275 {
276 LOG_WARNING(logging::g_qSharedLogger, "Query returned no results.");
277 }
278 return vResults;
279}
void AddRangeFilter(std::vector< std::string > &vClauses, duckdb::vector< duckdb::Value > &vBindValues, const char *pColumn, const std::optional< PointFilter::Range< T > > &stdOptRange)
Adds a range filter to the SQL query clauses and dynamically bound values.
Definition LiDARHandler.cpp:308
Definition duckdb.hpp:42284
Definition duckdb.hpp:5647
Definition duckdb.hpp:960
::int32_t int32_t
Here is the call graph for this function:
Here is the caller graph for this function:

◆ DeclareLiDARObstacle()

bool LiDARHandler::DeclareLiDARObstacle ( const geoops::UTMCoordinate stPoint,
double  dRadius 
)

Modifies all LiDAR points in radius to reflect bad terrain.

Parameters
stPoint- Center UTM coordinate of obstacle
dRadius- Radius of obstacle
Returns
true - If the data points were successfully modified.
false - If the modification failed.
Author
clayjay3 (clayt.nosp@m.onra.nosp@m.ycowe.nosp@m.n@gm.nosp@m.ail.c.nosp@m.om), Sam Nolte (samno.nosp@m.lte0.nosp@m.302@g.nosp@m.mail.nosp@m..com)
Date
2026-05-01
333{
334 // Acquire a write lock on the mutex to ensure thread safety.
335 std::unique_lock<std::shared_mutex> lkWriteLock(m_muQueryMutex);
336
337 // Check if the database is open.
338 if (!m_bIsDBOpen)
339 {
340 LOG_ERROR(logging::g_qSharedLogger, "Database is not open.");
341 return false;
342 }
343
344 // Prepare the SQL statement for updating data.
345 // DuckDB zonemaps replace the need for the old RTree index table.
346 const char* pSQL = R"(
347 UPDATE ProcessedLiDARPoints
348 SET trav_score = 0.01
349 WHERE easting BETWEEN ? AND ?
350 AND northing BETWEEN ? AND ?
351 AND (easting - ?) * (easting - ?) + (northing - ?) * (northing - ?) <= ?
352 )";
353
354 // Bind values to the prepared statement
356
357 // Bounding box (zonemap fast-filter)
358 vBindValues.push_back(duckdb::Value(stPoint.dEasting - dRadius));
359 vBindValues.push_back(duckdb::Value(stPoint.dEasting + dRadius));
360 vBindValues.push_back(duckdb::Value(stPoint.dNorthing - dRadius));
361 vBindValues.push_back(duckdb::Value(stPoint.dNorthing + dRadius));
362
363 // Radial distance check (exact filtering)
364 vBindValues.push_back(duckdb::Value(stPoint.dEasting));
365 vBindValues.push_back(duckdb::Value(stPoint.dEasting));
366 vBindValues.push_back(duckdb::Value(stPoint.dNorthing));
367 vBindValues.push_back(duckdb::Value(stPoint.dNorthing));
368
369 // Pass the squared radius directly so we don't need ? * ? in the SQL
370 vBindValues.push_back(duckdb::Value(dRadius * dRadius));
371
372 int64_t nRowsUpdated = 0;
373 try
374 {
375 // Thread-local connection to safely issue commands without bleeding pending states
376 duckdb::Connection stLocalConn(*m_pDB);
377
378 duckdb::unique_ptr<duckdb::PreparedStatement> stPreparedStmt = stLocalConn.Prepare(pSQL);
379 if (stPreparedStmt->HasError())
380 {
381 LOG_ERROR(logging::g_qSharedLogger, "Failed to prepare DuckDB SQL: {}", stPreparedStmt->GetError());
382 return false;
383 }
384
385 // Execute the statement.
386 auto stResult = stPreparedStmt->Execute(vBindValues);
387 if (stResult->HasError())
388 {
389 LOG_ERROR(logging::g_qSharedLogger, "Failed to update obstacle data: {}", stResult->GetError());
390 return false;
391 }
392
393 // DuckDB UPDATE queries return a single column/row chunk containing the number of updated rows.
394 // It's critical to loop over Fetch() until it returns null to exhaust the result set.
395 while (auto stChunk = stResult->Fetch())
396 {
397 if (stChunk->size() > 0 && nRowsUpdated == 0)
398 {
399 nRowsUpdated = stChunk->GetValue(0, 0).GetValue<int64_t>();
400 }
401 }
402 }
403 catch (const duckdb::Exception& e)
404 {
405 LOG_ERROR(logging::g_qSharedLogger, "DuckDB threw an exception in DeclareLiDARObstacle: {}", e.what());
406 return false;
407 }
408 catch (const std::exception& e)
409 {
410 LOG_ERROR(logging::g_qSharedLogger, "A standard exception was thrown in DeclareLiDARObstacle: {}", e.what());
411 return false;
412 }
413
414 // Log LiDAR changes
415 LOG_INFO(logging::g_qSharedLogger,
416 "Created new obstacle at ({}, {}), radius: {}. Updated {} points",
417 (int) stPoint.dEasting,
418 (int) stPoint.dNorthing,
419 (int) dRadius,
420 nRowsUpdated);
421
422 return true;
423}
::int64_t int64_t
Here is the call graph for this function:

◆ IsDBOpen()

bool LiDARHandler::IsDBOpen ( )

Checks if the database is currently open.

Returns
true - If the database is open.
false - If the database is not open.
Author
ClayJay3 (clayt.nosp@m.onra.nosp@m.ycowe.nosp@m.n@gm.nosp@m.ail.c.nosp@m.om)
Date
2025-07-14
290{
291 std::shared_lock<std::shared_mutex> lkReadLock(m_muQueryMutex);
292 return m_bIsDBOpen;
293}
Here is the caller graph for this function:

◆ AddRangeFilter()

template<typename T >
void LiDARHandler::AddRangeFilter ( std::vector< std::string > &  vClauses,
duckdb::vector< duckdb::Value > &  vBindValues,
const char *  pColumn,
const std::optional< PointFilter::Range< T > > &  stdOptRange 
)
private

Adds a range filter to the SQL query clauses and dynamically bound values.

Template Parameters
T- The data type of the range values.
Parameters
vClauses- The vector of SQL clauses to which the range filter will be added.
vBindValues- The duckdb value container storing runtime query parameters.
pColumn- The name of the column to apply the range filter on.
stdOptRange- The optional range to filter by.
Author
ClayJay3 (clayt.nosp@m.onra.nosp@m.ycowe.nosp@m.n@gm.nosp@m.ail.c.nosp@m.om)
Date
2025-07-14
312{
313 if (stdOptRange)
314 {
315 vClauses.emplace_back(std::string(pColumn) + " BETWEEN ? AND ?");
316 vBindValues.push_back(duckdb::Value(stdOptRange->tMin));
317 vBindValues.push_back(duckdb::Value(stdOptRange->tMax));
318 }
319}
Here is the caller graph for this function:

The documentation for this class was generated from the following files: