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
VisualizationHandler Class Reference

The VisualizationHandler class manages persistent world state and hosts a 3D web server for interactive visualization. More...

#include <VisualizationHandler.h>

Inheritance diagram for VisualizationHandler:
Collaboration diagram for VisualizationHandler:

Classes

struct  DisplayDetection
 Struct representing a persistent detection for visualization. More...
 
struct  DisplayPoint
 Struct representing a single point in the path history. More...
 
struct  DisplayWaypoint
 Struct representing a waypoint or goal beacon for visualization. More...
 

Public Member Functions

 VisualizationHandler (int nPort=8080)
 Construct a new Visualization Handler:: Visualization Handler object.
 
 ~VisualizationHandler ()
 Destroy the Visualization Handler:: Visualization Handler object.
 
void SaveVisualization (const std::string &szFilename)
 Save the current visualization to a single HTML file with embedded assets.
 
- Public Member Functions inherited from AutonomyThread< void >
 AutonomyThread ()
 Construct a new Autonomy Thread object.
 
virtual ~AutonomyThread ()
 Destroy the Autonomy Thread object. If the parent object or main thread is destroyed or exited while this thread is still running, a race condition will occur. Stopping and joining the thread here insures that the main program can't exit if the user forgot to stop and join the thread.
 
void Start ()
 When this method is called, it starts a new thread that runs the code within the ThreadedContinuousCode method. This is the users main code that will run the important and continuous code for the class.
 
void RequestStop ()
 Signals threads to stop executing user code, terminate. DOES NOT JOIN. This method will not force the thread to exit, if the user code is not written properly and contains WHILE statement or any other long-executing or blocking code, then the thread will not exit until the next iteration.
 
void Join ()
 Waits for thread to finish executing and then closes thread. This method will block the calling code until thread is finished.
 
bool Joinable () const
 Check if the code within the thread and all pools created by it are finished executing and the thread is ready to be closed.
 
AutonomyThreadState GetThreadState () const
 Accessor for the Threads State private member.
 
std::string GetThreadUUID () const
 Accessor for the Thread U U I D private member.
 
IPSGetIPS ()
 Accessor for the Frame I P S private member.
 
void SetMainThreadPriority (AutonomyThreadPriority ePriority)
 Set the OS priority for the main continuous thread.
 
void SetPoolThreadPriority (AutonomyThreadPriority ePriority)
 Set the OS priority for the highly parallelized pool threads.
 

Private Member Functions

void UpdatePathHistory (const geoops::UTMCoordinate &stRoverUTM)
 Update the path history with the current rover position.
 
void UpdatePlannedPath ()
 Updates the planned path from the waypoint handler.
 
void UpdateWaypoints ()
 Updates the waypoints from the waypoint handler.
 
void UpdateGoalBeacons (const geoops::UTMCoordinate &stRoverUTM)
 Updates the persistent goal beacons for visualization.
 
void UpdateDetections ()
 Updates the persistent detections for visualization.
 
std::vector< char > OnRequestTelemetry (const std::string &szQuery)
 Handles telemetry requests from the web server.
 
std::vector< char > OnRequestMap (const std::string &szQuery)
 Handles map data requests from the web server.
 
std::vector< char > OnRequestPlannedPath (const std::string &szQuery)
 Handles planned path requests from the web server.
 
std::vector< char > OnRequestWaypoints (const std::string &szQuery)
 Handles waypoint requests from the web server.
 
std::vector< char > OnRequestDetections (const std::string &szQuery)
 Handles detection requests from the web server.
 
std::vector< char > OnRequestDetectionList (const std::string &szQuery)
 Handles detection list requests from the web server.
 
std::vector< char > OnRequestLibThree (const std::string &szQuery)
 Serves the local three.min.js file.
 
std::vector< char > OnRequestLibOrbit (const std::string &szQuery)
 Serves the local OrbitControls.js file.
 
std::vector< char > LoadFileToBuffer (const std::string &szPath)
 Helper to load a file into a byte buffer.
 
std::string Base64Encode (const std::vector< char > &vData)
 Encodes binary data to a Base64 string.
 
std::string GetEmbeddedHtml ()
 Gets the embedded HTML for the visualization page.
 
std::string GenerateStaticHtml (const std::vector< LiDARHandler::PointRow > &vLidar)
 Generates a static HTML page with embedded LiDAR data and embedded dependencies.
 
void ThreadedContinuousCode () override
 The main continuous code for the VisualizationHandler.
 
void PooledLinearCode () override
 The pooled linear code for the VisualizationHandler. This is not used.
 

Private Attributes

std::unique_ptr< SimpleWebServerm_pWebServer
 
int m_nPort
 
geoops::UTMCoordinate m_stOriginUTM
 
bool m_bOriginSet
 
std::vector< DisplayPointm_vPathHistory
 
std::mutex m_muPathMutex
 
std::vector< DisplayPointm_vPlannedPath
 
std::mutex m_muPlannedPathMutex
 
std::vector< DisplayWaypointm_vWaypoints
 
std::mutex m_muWaypointMutex
 
std::vector< DisplayWaypointm_vGoalBeacons
 
std::mutex m_muGoalBeaconMutex
 
std::vector< DisplayDetectionm_vDetections
 
std::mutex m_muDetectionMutex
 

Additional Inherited Members

- Public Types inherited from AutonomyThread< void >
enum  AutonomyThreadState
 
enum  AutonomyThreadPriority
 
- Protected Member Functions inherited from AutonomyThread< void >
void RunPool (const unsigned int nNumTasksToQueue, const unsigned int nNumThreads=2, const bool bForceStopCurrentThreads=false)
 When this method is called, it starts/adds tasks to a thread pool that runs nNumTasksToQueue copies of the code within the PooledLinearCode() method using nNumThreads number of threads. This is meant to be used as an internal utility of the child class to further improve parallelization. Default value for nNumThreads is 2.
 
void RunDetachedPool (const unsigned int nNumTasksToQueue, const unsigned int nNumThreads=2, const bool bForceStopCurrentThreads=false)
 When this method is called, it starts a thread pool full of threads that don't return std::futures (like a placeholder for the thread return type). This means the thread will not have a return type and there is no way to determine if the thread has finished other than calling the Join() method. Only use this if you want to 'set and forget'. It will be faster as it doesn't return futures. Runs PooledLinearCode() method code. This is meant to be used as an internal utility of the child class to further improve parallelization.
 
void ParallelizeLoop (const int nNumThreads, const N tTotalIterations, F &&tLoopFunction)
 Given a ref-qualified looping function and an arbitrary number of iterations, this method will divide up the loop and run each section in a thread pool. This function must not return anything. This method will block until the loop has completed.
 
void ClearPoolQueue ()
 Clears any tasks waiting to be ran in the queue, tasks currently running will remain running.
 
void JoinPool ()
 Waits for pool to finish executing tasks. This method will block the calling code until thread is finished.
 
bool PoolJoinable () const
 Check if the internal pool threads are done executing code and the queue is empty.
 
void SetMainThreadIPSLimit (int nMaxIterationsPerSecond=0)
 Mutator for the Main Thread Max I P S private member.
 
int GetPoolNumOfThreads ()
 Accessor for the Pool Num Of Threads private member.
 
int GetPoolQueueLength ()
 Accessor for the Pool Queue Size private member.
 
std::vector< void > GetPoolResults ()
 Accessor for the Pool Results private member. The action of getting results will destroy and remove them from this object. This method blocks if the thread is not finished, so no need to call JoinPool() before getting results.
 
int GetMainThreadMaxIPS () const
 Accessor for the Main Thread Max I P S private member.
 
- Protected Attributes inherited from AutonomyThread< void >
IPS m_IPS
 

Detailed Description

The VisualizationHandler class manages persistent world state and hosts a 3D web server for interactive visualization.

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

Constructor & Destructor Documentation

◆ VisualizationHandler()

VisualizationHandler::VisualizationHandler ( int  nPort = 8080)

Construct a new Visualization Handler:: Visualization Handler object.

Parameters
nPort- The port to host the web server on.
Author
clayjay3 (clayt.nosp@m.onra.nosp@m.ycowe.nosp@m.n@gm.nosp@m.ail.c.nosp@m.om)
Date
2026-01-22
33{
34 // Initialize member variables.
35 m_nPort = nPort;
36 m_bOriginSet = false;
37
38 // Start Web Server.
39 m_pWebServer = std::make_unique<SimpleWebServer>(m_nPort);
40
41 // Serve the detections folder as static files from the current logging session
42 // This allows access via http://ip:port/detections/filename.png
43 std::string szDetectionsPath = logging::g_szLoggingOutputPath + "detections";
44 m_pWebServer->AddStaticDirectory("/detections", szDetectionsPath);
45
46 // Register Asset Endpoints.
47 m_pWebServer->RegisterEndpoint("/lib/three.js", std::bind(&VisualizationHandler::OnRequestLibThree, this, std::placeholders::_1));
48 m_pWebServer->RegisterEndpoint("/lib/orbit.js", std::bind(&VisualizationHandler::OnRequestLibOrbit, this, std::placeholders::_1));
49
50 // Register Data Endpoints.
51 m_pWebServer->SetHtmlContent(this->GetEmbeddedHtml());
52 m_pWebServer->RegisterEndpoint("/api/telemetry", std::bind(&VisualizationHandler::OnRequestTelemetry, this, std::placeholders::_1));
53 m_pWebServer->RegisterEndpoint("/api/map", std::bind(&VisualizationHandler::OnRequestMap, this, std::placeholders::_1));
54 m_pWebServer->RegisterEndpoint("/api/planned_path", std::bind(&VisualizationHandler::OnRequestPlannedPath, this, std::placeholders::_1));
55 m_pWebServer->RegisterEndpoint("/api/waypoints", std::bind(&VisualizationHandler::OnRequestWaypoints, this, std::placeholders::_1));
56 m_pWebServer->RegisterEndpoint("/api/detections", std::bind(&VisualizationHandler::OnRequestDetections, this, std::placeholders::_1));
57 m_pWebServer->RegisterEndpoint("/api/detection_list", std::bind(&VisualizationHandler::OnRequestDetectionList, this, std::placeholders::_1));
58
59 // Set main thread's max iteration rate.
60 this->SetMainThreadIPSLimit(20); // 20 Hz
61}
void SetMainThreadIPSLimit(int nMaxIterationsPerSecond=0)
Mutator for the Main Thread Max I P S private member.
Definition AutonomyThread.hpp:530
std::vector< char > OnRequestMap(const std::string &szQuery)
Handles map data requests from the web server.
Definition VisualizationHandler.cpp:842
std::vector< char > OnRequestDetections(const std::string &szQuery)
Handles detection requests from the web server.
Definition VisualizationHandler.cpp:706
std::vector< char > OnRequestLibThree(const std::string &szQuery)
Serves the local three.min.js file.
Definition VisualizationHandler.cpp:953
std::string GetEmbeddedHtml()
Gets the embedded HTML for the visualization page.
Definition VisualizationHandler.cpp:1076
std::vector< char > OnRequestPlannedPath(const std::string &szQuery)
Handles planned path requests from the web server.
Definition VisualizationHandler.cpp:611
std::vector< char > OnRequestDetectionList(const std::string &szQuery)
Handles detection list requests from the web server.
Definition VisualizationHandler.cpp:746
std::vector< char > OnRequestWaypoints(const std::string &szQuery)
Handles waypoint requests from the web server.
Definition VisualizationHandler.cpp:648
std::vector< char > OnRequestTelemetry(const std::string &szQuery)
Handles telemetry requests from the web server.
Definition VisualizationHandler.cpp:534
std::vector< char > OnRequestLibOrbit(const std::string &szQuery)
Serves the local OrbitControls.js file.
Definition VisualizationHandler.cpp:968
Here is the call graph for this function:

◆ ~VisualizationHandler()

VisualizationHandler::~VisualizationHandler ( )

Destroy the Visualization Handler:: Visualization Handler object.

Author
clayjay3 (clayt.nosp@m.onra.nosp@m.ycowe.nosp@m.n@gm.nosp@m.ail.c.nosp@m.om)
Date
2026-01-22
71{
72 // Stop Web Server.
73 m_pWebServer.reset();
74}

Member Function Documentation

◆ SaveVisualization()

void VisualizationHandler::SaveVisualization ( const std::string &  szFilename)

Save the current visualization to a single HTML file with embedded assets.

Parameters
szFilename- The filename to save the visualization as.
Author
clayjay3 (clayt.nosp@m.onra.nosp@m.ycowe.nosp@m.n@gm.nosp@m.ail.c.nosp@m.om)
Date
2026-01-22
144{
145 // Check that origin is set.
146 if (!m_bOriginSet)
147 {
148 LOG_WARNING(logging::g_qSharedLogger, "VisualizationHandler: Cannot save, origin not set.");
149 return;
150 }
151
152 // Submit logger message at start of saving process.
153 LOG_INFO(logging::g_qSharedLogger, "VisualizationHandler: Saving visualization to {}...", szFilename);
154
155 // Gather LiDAR Bounds based on Path History
156 double dMinE = 1e9, dMaxE = -1e9, dMinN = 1e9, dMaxN = -1e9;
157
158 {
159 // Acquire lock to read path history.
160 std::lock_guard<std::mutex> lkPathLock(m_muPathMutex);
161
162 // If no path history, use current rover position.
163 if (m_vPathHistory.empty())
164 {
165 geoops::RoverPose stPose = globals::g_pStateMachineHandler->SmartRetrieveRoverPose();
166 dMinE = dMaxE = stPose.GetUTMCoordinate().dEasting;
167 dMinN = dMaxN = stPose.GetUTMCoordinate().dNorthing;
168 }
169 else
170 {
171 // Loop through each point in the path history to find extremes.
172 for (const DisplayPoint& stPoint : m_vPathHistory)
173 {
174 double dAbsE = m_stOriginUTM.dEasting + stPoint.fX;
175 double dAbsN = m_stOriginUTM.dNorthing + stPoint.fZ;
176 if (dAbsE < dMinE)
177 dMinE = dAbsE;
178 if (dAbsE > dMaxE)
179 dMaxE = dAbsE;
180 if (dAbsN < dMinN)
181 dMinN = dAbsN;
182 if (dAbsN > dMaxN)
183 dMaxN = dAbsN;
184 }
185 }
186 }
187
188 // Calculate LiDAR filter parameters.
189 double dBuffer = 60.0;
190 double dCenterE = (dMinE + dMaxE) / 2.0;
191 double dCenterN = (dMinN + dMaxN) / 2.0;
192 double dRadius = std::max(dMaxE - dMinE, dMaxN - dMinN) / 2.0 + dBuffer;
193 std::vector<LiDARHandler::PointRow> vLidarPoints;
194 // Check that global LiDAR handler is valid.
195 if (globals::g_pLiDARHandler)
196 {
197 // Construct point filter.
199 stFilter.dEasting = dCenterE;
200 stFilter.dNorthing = dCenterN;
201 stFilter.dRadius = dRadius;
202 // Get LiDAR data from handler.
203 vLidarPoints = globals::g_pLiDARHandler->GetLiDARData(stFilter);
204 }
205
206 // Generate HTML content (including embedded JS).
207 std::string szHtml = GenerateStaticHtml(vLidarPoints);
208
209 std::ofstream stdOutFile(szFilename);
210 if (stdOutFile.is_open())
211 {
212 // Write file.
213 stdOutFile << szHtml;
214 stdOutFile.close();
215
216 // Submit logger message that we're done saving.
217 LOG_INFO(logging::g_qSharedLogger, "VisualizationHandler: Saved successfully.");
218 }
219 else
220 {
221 LOG_ERROR(logging::g_qSharedLogger, "VisualizationHandler: Failed to open file for writing.");
222 }
223}
std::vector< PointRow > GetLiDARData(const PointFilter &stPointFilter)
Retrieves LiDAR data points from DuckDB based on the specified filter.
Definition LiDARHandler.cpp:136
geoops::RoverPose SmartRetrieveRoverPose(bool bIMUHeading=true)
This method is used to retrieve the rover's current position and heading. It uses the GPS data from t...
Definition StateMachineHandler.cpp:375
std::string GenerateStaticHtml(const std::vector< LiDARHandler::PointRow > &vLidar)
Generates a static HTML page with embedded LiDAR data and embedded dependencies.
Definition VisualizationHandler.cpp:2060
Struct for filtering LiDAR points during queries.
Definition LiDARHandler.h:79
This struct is used by the WaypointHandler to provide an easy way to store all pose data about the ro...
Definition GeospatialOperations.hpp:708
const geoops::UTMCoordinate & GetUTMCoordinate() const
Accessor for the geoops::UTMCoordinate member variable.
Definition GeospatialOperations.hpp:767
Here is the call graph for this function:
Here is the caller graph for this function:

◆ UpdatePathHistory()

void VisualizationHandler::UpdatePathHistory ( const geoops::UTMCoordinate stRoverUTM)
private

Update the path history with the current rover position.

Parameters
stRoverUTM- The current UTM coordinate of the rover.
Author
clayjay3 (clayt.nosp@m.onra.nosp@m.ycowe.nosp@m.n@gm.nosp@m.ail.c.nosp@m.om)
Date
2026-01-22
234{
235 // Acquire lock to update path history.
236 std::lock_guard<std::mutex> lkPathLock(m_muPathMutex);
237
238 // Add new point if moved more than 0.5 meters from last point.
239 static geoops::UTMCoordinate stLastPos = {};
240 if (std::hypot(stRoverUTM.dEasting - stLastPos.dEasting, stRoverUTM.dNorthing - stLastPos.dNorthing) > 0.5)
241 {
242 // Create new point, set its values, and append to history.
243 DisplayPoint stPoint;
244 stPoint.fX = static_cast<float>(stRoverUTM.dEasting - m_stOriginUTM.dEasting);
245 stPoint.fY = static_cast<float>(stRoverUTM.dAltitude - m_stOriginUTM.dAltitude);
246 stPoint.fZ = static_cast<float>(stRoverUTM.dNorthing - m_stOriginUTM.dNorthing);
247 stPoint.fScore = 0.0f;
248
249 // Check if our global state machine handler is valid to get current state.
250 if (globals::g_pStateMachineHandler)
251 {
252 // Set state.
253 stPoint.nState = static_cast<int>(globals::g_pStateMachineHandler->GetCurrentState());
254 }
255 else
256 {
257 // Default to Idle state.
258 stPoint.nState = 0;
259 }
260
261 // Append to history and update last position.
262 m_vPathHistory.push_back(stPoint);
263 stLastPos = stRoverUTM;
264 }
265}
statemachine::States GetCurrentState() const
Accessor for the Current State private member.
Definition StateMachineHandler.cpp:345
This struct stores/contains information about a UTM coordinate.
Definition GeospatialOperations.hpp:211
Here is the call graph for this function:
Here is the caller graph for this function:

◆ UpdatePlannedPath()

void VisualizationHandler::UpdatePlannedPath ( )
private

Updates the planned path from the waypoint handler.

Author
clayjay3 (clayt.nosp@m.onra.nosp@m.ycowe.nosp@m.n@gm.nosp@m.ail.c.nosp@m.om)
Date
2026-01-22
275{
276 // Check that global waypoint handler is valid.
277 if (globals::g_pWaypointHandler == nullptr)
278 {
279 return;
280 }
281
282 // Retrieve raw planned path that the GeoPlanner put in the waypoint handler for us.
283 // This may or may not exist or be the active path that the rover is following.
284 std::vector<geoops::Waypoint> vRawPath = globals::g_pWaypointHandler->RetrievePath("GeoPlannerPath");
285
286 // Acquire lock and update planned path.
287 std::lock_guard<std::mutex> lkPathLock(m_muPlannedPathMutex);
288 // Clear the old path and reserve space for the new path.
289 m_vPlannedPath.clear();
290 m_vPlannedPath.reserve(vRawPath.size());
291 // Loop through raw path and convert to DisplayPoint format.
292 for (const geoops::Waypoint& stWaypoint : vRawPath)
293 {
294 const geoops::UTMCoordinate& stUTMCoord = stWaypoint.GetUTMCoordinate();
295 DisplayPoint stPoint;
296 stPoint.fX = static_cast<float>(stUTMCoord.dEasting - m_stOriginUTM.dEasting);
297 stPoint.fY = static_cast<float>(stUTMCoord.dAltitude - m_stOriginUTM.dAltitude);
298 stPoint.fZ = static_cast<float>(stUTMCoord.dNorthing - m_stOriginUTM.dNorthing);
299 stPoint.fScore = 0.0f;
300 m_vPlannedPath.push_back(stPoint);
301 }
302}
const std::vector< geoops::Waypoint > RetrievePath(const std::string &szPathName)
Retrieve an immutable reference to the path at the given path name/key.
Definition WaypointHandler.cpp:600
This struct is used by the WaypointHandler class to store location, size, and type information about ...
Definition GeospatialOperations.hpp:423
Here is the call graph for this function:
Here is the caller graph for this function:

◆ UpdateWaypoints()

void VisualizationHandler::UpdateWaypoints ( )
private

Updates the waypoints from the waypoint handler.

Author
clayjay3 (clayt.nosp@m.onra.nosp@m.ycowe.nosp@m.n@gm.nosp@m.ail.c.nosp@m.om)
Date
2026-01-22
312{
313 // Check that global waypoint handler is valid.
314 if (globals::g_pWaypointHandler == nullptr)
315 {
316 return;
317 }
318
319 // Retrieve all waypoints and obstacles from the WaypointHandler.
320 std::vector<geoops::Waypoint> vTargets = globals::g_pWaypointHandler->GetAllWaypoints();
321 std::vector<geoops::Waypoint> vObstacles = globals::g_pWaypointHandler->GetAllObstacles();
322
323 // Acquire a lock before updating waypoints list and clear existing waypoints.
324 std::lock_guard<std::mutex> lkWaypointLock(m_muWaypointMutex);
325
326 // Clear old waypoints from list.
327 m_vWaypoints.clear();
328 // Helper to process waypoints and obstacles.
329 std::function ProcessList = [&](const std::vector<geoops::Waypoint>& vWaypoints)
330 {
331 // Loop through all the given waypoints and add them to this classes waypoints list.
332 for (const geoops::Waypoint& stWaypoint : vWaypoints)
333 {
334 const geoops::UTMCoordinate& stUTMCoord = stWaypoint.GetUTMCoordinate();
335 DisplayWaypoint stPoint;
336 stPoint.fX = static_cast<float>(stUTMCoord.dEasting - m_stOriginUTM.dEasting);
337 stPoint.fY = static_cast<float>(stUTMCoord.dAltitude - m_stOriginUTM.dAltitude);
338 stPoint.fZ = static_cast<float>(stUTMCoord.dNorthing - m_stOriginUTM.dNorthing);
339 stPoint.nType = static_cast<int>(stWaypoint.eType);
340 m_vWaypoints.push_back(stPoint);
341 }
342 };
343
344 // Use our utility function to process and add the waypoints and objects from the WaypointHandler to the member variables.
345 ProcessList(vTargets);
346 ProcessList(vObstacles);
347}
const std::vector< geoops::Waypoint > GetAllWaypoints()
Accessor for the full list of current waypoints stored in the WaypointHandler.
Definition WaypointHandler.cpp:656
const std::vector< geoops::Waypoint > GetAllObstacles()
Accessor for the full list of current obstacle stored in the WaypointHandler.
Definition WaypointHandler.cpp:673
Here is the call graph for this function:
Here is the caller graph for this function:

◆ UpdateGoalBeacons()

void VisualizationHandler::UpdateGoalBeacons ( const geoops::UTMCoordinate stRoverUTM)
private

Updates the persistent goal beacons for visualization.

Parameters
stRoverUTM- The current UTM coordinate of the rover.
Author
clayjay3 (clayt.nosp@m.onra.nosp@m.ycowe.nosp@m.n@gm.nosp@m.ail.c.nosp@m.om)
Date
2026-01-22
477{
478 // Check that global multimedia board is valid.
479 if (globals::g_pMultimediaBoard == nullptr)
480 {
481 return;
482 }
483
484 // If we are in the ReachedGoal lighting state, add a persistent goal beacon at our current location.
485 if (globals::g_pMultimediaBoard->GetCurrentLightingState() == MultimediaBoard::MultimediaBoardLightingState::eReachedGoal)
486 {
487 // Acquire lock to update goal beacons.
488 std::lock_guard<std::mutex> lkBeaconLock(m_muGoalBeaconMutex);
489 // Calculate relative position.
490 float fCurX = static_cast<float>(stRoverUTM.dEasting - m_stOriginUTM.dEasting);
491 float fCurY = static_cast<float>(stRoverUTM.dAltitude - m_stOriginUTM.dAltitude);
492 float fCurZ = static_cast<float>(stRoverUTM.dNorthing - m_stOriginUTM.dNorthing);
493
494 // Check for deduplication (within 2 meters).
495 bool bAdd = true;
496 if (!m_vGoalBeacons.empty())
497 {
498 // Calculate distance to last added beacon.
499 const DisplayWaypoint& stLast = m_vGoalBeacons.back();
500 float fDistance = std::hypot(fCurX - stLast.fX, fCurZ - stLast.fZ);
501 // If the distance is less than 2 meters, do not add.
502 if (fDistance < 2.0f)
503 {
504 bAdd = false;
505 }
506 }
507
508 // Check if our last beacon is in the same location.
509 if (bAdd)
510 {
511 // Construct beacon.
512 DisplayWaypoint stWaypoint;
513 stWaypoint.fX = fCurX;
514 stWaypoint.fY = fCurY;
515 stWaypoint.fZ = fCurZ;
516 stWaypoint.nType = 8;
517 m_vGoalBeacons.push_back(stWaypoint);
518
519 // Submit logger message.
520 LOG_DEBUG(logging::g_qSharedLogger, "VisualizationHandler: Added persistent Goal Beacon at current location.");
521 }
522 }
523}
Here is the caller graph for this function:

◆ UpdateDetections()

void VisualizationHandler::UpdateDetections ( )
private

Updates the persistent detections for visualization.

Author
clayjay3 (clayt.nosp@m.onra.nosp@m.ycowe.nosp@m.n@gm.nosp@m.ail.c.nosp@m.om)
Date
2026-01-22
357{
358 // Check that required global handlers are valid.
359 if (globals::g_pTagDetectionHandler == nullptr || globals::g_pObjectDetectionHandler == nullptr)
360 {
361 return;
362 }
363
364 // Helper to process detections
365 std::function ProcessDetection = [&](float fX, float fY, float fZ, int nType)
366 {
367 // Acquire lock to update detections.
368 std::lock_guard<std::mutex> lkDetectionsLock(m_muDetectionMutex);
369
370 // Deduplication.
371 for (DisplayDetection& stExistingDetection : m_vDetections)
372 {
373 // Same type check
374 if (stExistingDetection.nType == nType)
375 {
376 // Calculate the distance between existing detection and new detection.
377 float dist = std::hypot(stExistingDetection.fX - fX, stExistingDetection.fZ - fZ);
378 // If close to another detection of the same type, update the existing location.
379 if (dist < 1.5f)
380 {
381 // Update the existing detection's position to be the average of the two detections.
382 stExistingDetection.fX = (stExistingDetection.fX + fX) / 2.0f;
383 stExistingDetection.fY = (stExistingDetection.fY + fY) / 2.0f;
384 stExistingDetection.fZ = (stExistingDetection.fZ + fZ) / 2.0f;
385
386 return;
387 }
388 }
389 }
390
391 // Construct a new detection and add it to our member list.
392 DisplayDetection stDetection;
393 stDetection.fX = fX;
394 stDetection.fY = fY;
395 stDetection.fZ = fZ;
396 stDetection.nType = nType;
397 m_vDetections.push_back(stDetection);
398 };
399
400 // Prepare Detector Vectors
401 std::vector<std::shared_ptr<TagDetector>> vTagDetectors = {globals::g_pTagDetectionHandler->GetTagDetector(TagDetectionHandler::TagDetectors::eHeadMainCam),
402 globals::g_pTagDetectionHandler->GetTagDetector(TagDetectionHandler::TagDetectors::eRearCam)};
403
404 std::vector<std::shared_ptr<ObjectDetector>> vObjDetectors = {
405 globals::g_pObjectDetectionHandler->GetObjectDetector(ObjectDetectionHandler::ObjectDetectors::eHeadMainCam),
406 globals::g_pObjectDetectionHandler->GetObjectDetector(ObjectDetectionHandler::ObjectDetectors::eRearCam)};
407
409 // Tags.
411
412 // Get the best Aruco and Torch tags.
413 tagdetectutils::ArucoTag stBestAruco;
414 tagdetectutils::ArucoTag stBestTorchTag;
415 statemachine::IdentifyTargetMarker(vTagDetectors, stBestAruco, stBestTorchTag);
416 // Process Aruco Tag if valid.
417 if (stBestAruco.nID != -1 && stBestAruco.stGeolocatedPosition.eType != geoops::WaypointType::eUNKNOWN)
418 {
419 geoops::UTMCoordinate stUTM = stBestAruco.stGeolocatedPosition.GetUTMCoordinate();
420 float fX = static_cast<float>(stUTM.dEasting - m_stOriginUTM.dEasting);
421 float fY = static_cast<float>(stUTM.dAltitude - m_stOriginUTM.dAltitude);
422 float fZ = static_cast<float>(stUTM.dNorthing - m_stOriginUTM.dNorthing);
423 ProcessDetection(fX, fY, fZ, 10);
424 }
425 // Process Torch Tag if valid.
426 if (stBestTorchTag.nID != -1 && stBestTorchTag.stGeolocatedPosition.eType != geoops::WaypointType::eUNKNOWN)
427 {
428 geoops::UTMCoordinate stUTM = stBestTorchTag.stGeolocatedPosition.GetUTMCoordinate();
429 float fX = static_cast<float>(stUTM.dEasting - m_stOriginUTM.dEasting);
430 float fY = static_cast<float>(stUTM.dAltitude - m_stOriginUTM.dAltitude);
431 float fZ = static_cast<float>(stUTM.dNorthing - m_stOriginUTM.dNorthing);
432 ProcessDetection(fX, fY, fZ, 10);
433 }
434
436 // Mallets, Bottles, Picks.
438
439 // Get the best detected object.
441 statemachine::IdentifyTargetObject(vObjDetectors, stObject);
442 // Process object if valid.
443 if (stObject.dConfidence > 0.6 && stObject.stGeolocatedPosition.eType != geoops::WaypointType::eUNKNOWN)
444 {
445 geoops::UTMCoordinate stUTM = stObject.stGeolocatedPosition.GetUTMCoordinate();
446 float fX = static_cast<float>(stUTM.dEasting - m_stOriginUTM.dEasting);
447 float fY = static_cast<float>(stUTM.dAltitude - m_stOriginUTM.dAltitude);
448 float fZ = static_cast<float>(stUTM.dNorthing - m_stOriginUTM.dNorthing);
449
450 // Determine type based on detection type.
451 int nType = 0;
452 switch (stObject.eDetectionType)
453 {
454 case objectdetectutils::ObjectDetectionType::eMallet: nType = 11; break;
455 case objectdetectutils::ObjectDetectionType::eWaterBottle: nType = 12; break;
456 case objectdetectutils::ObjectDetectionType::eRockPick: nType = 13; break;
457 default: break;
458 }
459
460 // If we have a type, process the detection.
461 if (nType != 0)
462 {
463 ProcessDetection(fX, fY, fZ, nType);
464 }
465 }
466}
std::shared_ptr< ObjectDetector > GetObjectDetector(ObjectDetectors eDetectorName)
Accessor for ObjectDetector detectors.
Definition ObjectDetectionHandler.cpp:153
std::shared_ptr< TagDetector > GetTagDetector(TagDetectors eDetectorName)
Accessor for TagDetector detectors.
Definition TagDetectionHandler.cpp:163
int IdentifyTargetMarker(const std::vector< std::shared_ptr< TagDetector > > &vTagDetectors, tagdetectutils::ArucoTag &stArucoTarget, tagdetectutils::ArucoTag &stTorchTarget, const int nTargetTagID=static_cast< int >(manifest::Autonomy::AUTONOMYWAYPOINTTYPES::ANY))
Identify a target marker in the rover's vision, using OpenCV detection.
Definition TagDetectionChecker.hpp:100
int IdentifyTargetObject(const std::vector< std::shared_ptr< ObjectDetector > > &vObjectDetectors, objectdetectutils::Object &stObjectTarget, const geoops::WaypointType &eDesiredDetectionType=geoops::WaypointType::eUNKNOWN)
Identify a target object in the rover's vision, using Torch detection.
Definition ObjectDetectionChecker.hpp:101
const geoops::UTMCoordinate & GetUTMCoordinate() const
Accessor for the geoops::UTMCoordinate member variable.
Definition GeospatialOperations.hpp:508
Represents a single detected object.
Definition ObjectDetectionUtility.hpp:73
Represents a single ArUco tag.
Definition TagDetectionUtilty.hpp:57
Here is the call graph for this function:
Here is the caller graph for this function:

◆ OnRequestTelemetry()

std::vector< char > VisualizationHandler::OnRequestTelemetry ( const std::string &  szQuery)
private

Handles telemetry requests from the web server.

Parameters
szQuery- The query string from the request.
Returns
std::vector<char> - The binary response data.
Author
clayjay3 (clayt.nosp@m.onra.nosp@m.ycowe.nosp@m.n@gm.nosp@m.ail.c.nosp@m.om)
Date
2026-01-22
535{
536 (void) szQuery;
537
538 // Acquire resource lock for reading the path.
539 std::lock_guard<std::mutex> lkPathLock(m_muPathMutex);
540
541 // Calculate buffer size.
542 // Header = Pose(3f) + Heading(1f) + LPower(1f) + RPower(1f) + Count(1u)
543 size_t siPathBytes = m_vPathHistory.size() * 4 * sizeof(float);
544 size_t siHeader = (6 * sizeof(float)) + (1 * sizeof(uint32_t));
545 std::vector<char> vBuffer;
546 vBuffer.reserve(siHeader + siPathBytes);
547
548 // Create a util function for adding a float and to the buffer.
549 std::function PushFloat = [&](float fFloat)
550 {
551 const char* pStart = reinterpret_cast<const char*>(&fFloat);
552 vBuffer.insert(vBuffer.end(), pStart, pStart + sizeof(float));
553 };
554 // Create a util function for adding a uint32_t to the buffer.
555 std::function PushUint = [&](uint32_t unInt32)
556 {
557 const char* pStart = reinterpret_cast<const char*>(&unInt32);
558 vBuffer.insert(vBuffer.end(), pStart, pStart + sizeof(uint32_t));
559 };
560
561 // Push Rover Pose.
562 geoops::RoverPose stPose = globals::g_pStateMachineHandler->SmartRetrieveRoverPose();
563 if (m_bOriginSet)
564 {
565 PushFloat(static_cast<float>(stPose.GetUTMCoordinate().dEasting - m_stOriginUTM.dEasting));
566 PushFloat(static_cast<float>(stPose.GetUTMCoordinate().dAltitude - m_stOriginUTM.dAltitude));
567 PushFloat(static_cast<float>(stPose.GetUTMCoordinate().dNorthing - m_stOriginUTM.dNorthing));
568 }
569 else
570 {
571 PushFloat(0.0f);
572 PushFloat(0.0f);
573 PushFloat(0.0f);
574 }
575
576 // Push Compass Heading.
577 PushFloat(static_cast<float>(stPose.GetCompassHeading()));
578
579 // Push Drive Powers.
580 diffdrive::DrivePowers stPowers = {0.0, 0.0};
581 if (globals::g_pDriveBoard)
582 {
583 stPowers = globals::g_pDriveBoard->GetDrivePowers();
584 }
585 PushFloat(static_cast<float>(stPowers.dLeftDrivePower));
586 PushFloat(static_cast<float>(stPowers.dRightDrivePower));
587
588 // Push Path History size.
589 PushUint(static_cast<uint32_t>(m_vPathHistory.size()));
590 // Push each point in the path history.
591 for (const DisplayPoint& pt : m_vPathHistory)
592 {
593 PushFloat(pt.fX);
594 PushFloat(pt.fY);
595 PushFloat(pt.fZ);
596 PushFloat(static_cast<float>(pt.nState));
597 }
598
599 return vBuffer;
600}
diffdrive::DrivePowers GetDrivePowers() const
Accessor for the current drive powers of the robot.
Definition DriveBoard.cpp:346
::uint32_t uint32_t
This struct is used to store the left and right drive powers for the robot. Storing these values in a...
Definition DifferentialDrive.hpp:73
double GetCompassHeading() const
Accessor for the Compass Heading private member.
Definition GeospatialOperations.hpp:787
Here is the call graph for this function:
Here is the caller graph for this function:

◆ OnRequestMap()

std::vector< char > VisualizationHandler::OnRequestMap ( const std::string &  szQuery)
private

Handles map data requests from the web server.

Parameters
szQuery- The query string from the request.
Returns
std::vector<char> - The binary response data.
Author
clayjay3 (clayt.nosp@m.onra.nosp@m.ycowe.nosp@m.n@gm.nosp@m.ail.c.nosp@m.om)
Date
2026-01-22
843{
844 // Declare instance variables with default values.
845 float fCenterX = 0.0f;
846 float fCenterY = 0.0f;
847 float fRadius = 50.0f;
848 float fMinScore = 0.0f;
849
850 // Helper to parse float values from query string.
851 std::function ParseVal = [&](std::string szKey) -> float
852 {
853 size_t siPos = szQuery.find(szKey + "=");
854 if (siPos != std::string::npos)
855 {
856 // Extract substring value.
857 size_t siEnd = szQuery.find("&", siPos);
858 std::string szSub = szQuery.substr(siPos + szKey.length() + 1, siEnd - (siPos + szKey.length() + 1));
859
860 // Convert to float.
861 try
862 {
863 return std::stof(szSub);
864 }
865 catch (...)
866 {
867 return 0.0f;
868 }
869 }
870
871 return 0.0f;
872 };
873
874 // Parse values from query string.
875 fCenterX = ParseVal("x");
876 fCenterY = ParseVal("y");
877 fRadius = ParseVal("r");
878 fMinScore = ParseVal("s");
879
880 // Clamp radius to minimum of 10 meters.
881 if (fRadius < 10.0f)
882 {
883 fRadius = 10.0f;
884 }
885
886 // Calculate absolute UTM coordinates for center point.
887 double dAbsE = m_stOriginUTM.dEasting + fCenterX;
888 double dAbsN = m_stOriginUTM.dNorthing + fCenterY;
889 double dSearchRadius = fRadius * 1.5;
890
891 // Construct LiDAR point filter.
893 stFilter.dEasting = dAbsE;
894 stFilter.dNorthing = dAbsN;
895 stFilter.dRadius = dSearchRadius;
896 // Retrieve LiDAR points from global LiDAR handler.
897 std::vector<LiDARHandler::PointRow> vPoints;
898 // Check that global LiDAR handler is valid.
899 if (globals::g_pLiDARHandler)
900 {
901 // Get LiDAR data from handler.
902 vPoints = globals::g_pLiDARHandler->GetLiDARData(stFilter);
903 }
904
905 // Prepare buffer.
906 std::vector<char> vBuffer;
907 size_t siBytes = sizeof(uint32_t) + (vPoints.size() * 4 * sizeof(float));
908 vBuffer.reserve(siBytes);
909 // Insert number of points placeholder.
910 uint32_t unPtCount = 0;
911 vBuffer.resize(sizeof(uint32_t));
912
913 // Loop through each point and add to buffer if within radius and above min score.
914 for (const LiDARHandler::PointRow& stPoint : vPoints)
915 {
916 // Check traversal score.
917 if (stPoint.dTraversalScore < fMinScore)
918 {
919 continue;
920 }
921
922 // Calculate relative coordinates.
923 float fRelX = static_cast<float>(stPoint.dEasting - m_stOriginUTM.dEasting);
924 float fRelZ = static_cast<float>(stPoint.dNorthing - m_stOriginUTM.dNorthing);
925 float fRelY = static_cast<float>(stPoint.dAltitude - m_stOriginUTM.dAltitude);
926
927 // Check if within radius.
928 if (std::abs(fRelX - fCenterX) <= fRadius && std::abs(fRelZ - fCenterY) <= fRadius)
929 {
930 unPtCount++;
931 const float aData[4] = {fRelX, fRelY, fRelZ, static_cast<float>(stPoint.dTraversalScore)};
932 const char* pStart = reinterpret_cast<const char*>(aData);
933 vBuffer.insert(vBuffer.end(), pStart, pStart + sizeof(aData));
934 }
935 }
936
937 // Write actual point count to the start of the buffer.
938 uint32_t* pHead = reinterpret_cast<uint32_t*>(vBuffer.data());
939 *pHead = unPtCount;
940
941 return vBuffer;
942}
Struct representing a single LiDAR point row from the database.
Definition LiDARHandler.h:54
Here is the call graph for this function:
Here is the caller graph for this function:

◆ OnRequestPlannedPath()

std::vector< char > VisualizationHandler::OnRequestPlannedPath ( const std::string &  szQuery)
private

Handles planned path requests from the web server.

Parameters
szQuery- The query string from the request.
Returns
std::vector<char> - The binary response data.
Author
clayjay3 (clayt.nosp@m.onra.nosp@m.ycowe.nosp@m.n@gm.nosp@m.ail.c.nosp@m.om)
Date
2026-01-22
612{
613 (void) szQuery;
614
615 // Acquire lock to read planned path.
616 std::lock_guard<std::mutex> lk(m_muPlannedPathMutex);
617
618 // Prepare buffer.
619 std::vector<char> vBuffer;
620 size_t siBytes = sizeof(uint32_t) + (m_vPlannedPath.size() * 3 * sizeof(float));
621 vBuffer.reserve(siBytes);
622
623 // Insert number of points.
624 uint32_t unCount = static_cast<uint32_t>(m_vPlannedPath.size());
625 const char* pCount = reinterpret_cast<const char*>(&unCount);
626 vBuffer.insert(vBuffer.end(), pCount, pCount + sizeof(uint32_t));
627
628 // Loop through each of the points in the planned path and add them to the buffer to be sent to the client.
629 for (const DisplayPoint& stPoint : m_vPlannedPath)
630 {
631 const float data[3] = {stPoint.fX, stPoint.fY, stPoint.fZ};
632 const char* pStart = reinterpret_cast<const char*>(data);
633 vBuffer.insert(vBuffer.end(), pStart, pStart + sizeof(data));
634 }
635
636 return vBuffer;
637}
Here is the caller graph for this function:

◆ OnRequestWaypoints()

std::vector< char > VisualizationHandler::OnRequestWaypoints ( const std::string &  szQuery)
private

Handles waypoint requests from the web server.

Parameters
szQuery- The query string from the request.
Returns
std::vector<char> - The binary response data.
Author
clayjay3 (clayt.nosp@m.onra.nosp@m.ycowe.nosp@m.n@gm.nosp@m.ail.c.nosp@m.om)
Date
2026-01-22
649{
650 (void) szQuery;
651
652 // Acquire locks to read waypoints and goal beacons.
653 std::lock_guard<std::mutex> lkWaypointLock(m_muWaypointMutex);
654 std::lock_guard<std::mutex> lkBeaconLock(m_muGoalBeaconMutex);
655
656 // Get the total number of points.
657 size_t siTotalPoints = m_vWaypoints.size() + m_vGoalBeacons.size();
658
659 // Prepare buffer.
660 std::vector<char> vBuffer;
661 size_t siBytes = sizeof(uint32_t) + (siTotalPoints * (3 * sizeof(float) + sizeof(int)));
662 vBuffer.reserve(siBytes);
663 // Insert number of points.
664 uint32_t unCount = static_cast<uint32_t>(siTotalPoints);
665 const char* pStart = reinterpret_cast<const char*>(&unCount);
666 vBuffer.insert(vBuffer.end(), pStart, pStart + sizeof(uint32_t));
667
668 // Helper to push a DisplayWaypoint to the buffer.
669 std::function PushPoint = [&](const DisplayWaypoint& stWaypoint)
670 {
671 const char* pX = reinterpret_cast<const char*>(&stWaypoint.fX);
672 vBuffer.insert(vBuffer.end(), pX, pX + sizeof(float));
673
674 const char* pY = reinterpret_cast<const char*>(&stWaypoint.fY);
675 vBuffer.insert(vBuffer.end(), pY, pY + sizeof(float));
676
677 const char* pZ = reinterpret_cast<const char*>(&stWaypoint.fZ);
678 vBuffer.insert(vBuffer.end(), pZ, pZ + sizeof(float));
679
680 const char* pT = reinterpret_cast<const char*>(&stWaypoint.nType);
681 vBuffer.insert(vBuffer.end(), pT, pT + sizeof(int));
682 };
683
684 // Loop through each of the waypoints and goal beacons and add them to the buffer.
685 for (const DisplayWaypoint& stWaypoint : m_vWaypoints)
686 {
687 PushPoint(stWaypoint);
688 }
689 for (const DisplayWaypoint& stWaypoint : m_vGoalBeacons)
690 {
691 PushPoint(stWaypoint);
692 }
693
694 return vBuffer;
695}
Here is the caller graph for this function:

◆ OnRequestDetections()

std::vector< char > VisualizationHandler::OnRequestDetections ( const std::string &  szQuery)
private

Handles detection requests from the web server.

Parameters
szQuery- The query string from the request.
Returns
std::vector<char> - The binary response data.
Author
clayjay3 (clayt.nosp@m.onra.nosp@m.ycowe.nosp@m.n@gm.nosp@m.ail.c.nosp@m.om)
Date
2026-01-22
707{
708 (void) szQuery;
709
710 // Acquire lock to read detections.
711 std::lock_guard<std::mutex> lkDetectionsLock(m_muDetectionMutex);
712
713 // Prepare buffer.
714 std::vector<char> vBuffer;
715 size_t siBytes = sizeof(uint32_t) + (m_vDetections.size() * (3 * sizeof(float) + sizeof(int)));
716 vBuffer.reserve(siBytes);
717 // Insert number of detections.
718 uint32_t unCount = static_cast<uint32_t>(m_vDetections.size());
719 const char* pStart = reinterpret_cast<const char*>(&unCount);
720 vBuffer.insert(vBuffer.end(), pStart, pStart + sizeof(uint32_t));
721
722 // Loop through each of the detections and add them to the buffer.
723 for (const DisplayDetection& stDetection : m_vDetections)
724 {
725 // Insert position data.
726 const float aData[3] = {stDetection.fX, stDetection.fY, stDetection.fZ};
727 const char* pStart = reinterpret_cast<const char*>(aData);
728 vBuffer.insert(vBuffer.end(), pStart, pStart + sizeof(aData));
729 // Insert type data.
730 const char* pTypeStart = reinterpret_cast<const char*>(&stDetection.nType);
731 vBuffer.insert(vBuffer.end(), pTypeStart, pTypeStart + sizeof(int));
732 }
733
734 return vBuffer;
735}
Here is the caller graph for this function:

◆ OnRequestDetectionList()

std::vector< char > VisualizationHandler::OnRequestDetectionList ( const std::string &  szQuery)
private

Handles detection list requests from the web server.

Parameters
szQuery- The query string from the request.
Returns
std::vector<char> - The binary response data.
Author
Targed (ltkli.nosp@m.onel.nosp@m.@gmai.nosp@m.l.co.nosp@m.m)
Date
2026-01-30
747{
748 (void) szQuery;
749 std::string szJson = "[";
750 std::string szDir = logging::g_szLoggingOutputPath + "/detections/";
751
752 // Helper function to escape JSON strings
753 std::function<std::string(const std::string&)> fnEscapeJson = [](const std::string& szInput) -> std::string
754 {
755 std::string szOutput;
756 szOutput.reserve(szInput.size());
757 for (char c : szInput)
758 {
759 switch (c)
760 {
761 case '"': szOutput += "\\\""; break;
762 case '\\': szOutput += "\\\\"; break;
763 case '\b': szOutput += "\\b"; break;
764 case '\f': szOutput += "\\f"; break;
765 case '\n': szOutput += "\\n"; break;
766 case '\r': szOutput += "\\r"; break;
767 case '\t': szOutput += "\\t"; break;
768 default:
769 if (static_cast<unsigned char>(c) < 0x20)
770 {
771 // Escape control characters
772 char szBuf[7];
773 snprintf(szBuf, sizeof(szBuf), "\\u%04x", static_cast<unsigned char>(c));
774 szOutput += szBuf;
775 }
776 else
777 {
778 szOutput += c;
779 }
780 break;
781 }
782 }
783 return szOutput;
784 };
785
786 // Ensure the directory exists
787 if (std::filesystem::exists(szDir) && std::filesystem::is_directory(szDir))
788 {
789 std::vector<std::filesystem::directory_entry> vEntries;
790
791 // Iterate over files in the directory and collect them
792 for (const std::filesystem::directory_entry& stEntry : std::filesystem::directory_iterator(szDir))
793 {
794 if (stEntry.is_regular_file())
795 {
796 // Get filename
797 std::string szFilename = stEntry.path().filename().string();
798
799 // Simple filter for image extensions
800 if (szFilename.ends_with(".png") || szFilename.ends_with(".jpg"))
801 {
802 vEntries.push_back(stEntry);
803 }
804 }
805 }
806
807 // Sort entries chronologically by last write time (oldest first, newest last)
808 std::sort(vEntries.begin(),
809 vEntries.end(),
810 [](const std::filesystem::directory_entry& a, const std::filesystem::directory_entry& b)
811 { return std::filesystem::last_write_time(a) < std::filesystem::last_write_time(b); });
812
813 bool bFirst = true;
814 // Build JSON array using the sorted entries
815 for (const std::filesystem::directory_entry& stEntry : vEntries)
816 {
817 std::string szFilename = stEntry.path().filename().string();
818
819 if (!bFirst)
820 szJson += ",";
821 // Append escaped filename to JSON array
822 szJson += "\"" + fnEscapeJson(szFilename) + "\"";
823 bFirst = false;
824 }
825 }
826
827 szJson += "]";
828
829 // Convert string to vector
830 return std::vector<char>(szJson.begin(), szJson.end());
831}
Here is the caller graph for this function:

◆ OnRequestLibThree()

std::vector< char > VisualizationHandler::OnRequestLibThree ( const std::string &  szQuery)
private

Serves the local three.min.js file.

Parameters
szQuery- The query string (unused).
Returns
std::vector<char> - The binary content of the file.
Author
clayjay3 (clayt.nosp@m.onra.nosp@m.ycowe.nosp@m.n@gm.nosp@m.ail.c.nosp@m.om)
Date
2026-01-24
954{
955 (void) szQuery;
956 return LoadFileToBuffer(constants::VISUALIZER_THREEJS_PATH);
957}
std::vector< char > LoadFileToBuffer(const std::string &szPath)
Helper to load a file into a byte buffer.
Definition VisualizationHandler.cpp:983
Here is the call graph for this function:
Here is the caller graph for this function:

◆ OnRequestLibOrbit()

std::vector< char > VisualizationHandler::OnRequestLibOrbit ( const std::string &  szQuery)
private

Serves the local OrbitControls.js file.

Parameters
szQuery- The query string (unused).
Returns
std::vector<char> - The binary content of the file.
Author
clayjay3 (clayt.nosp@m.onra.nosp@m.ycowe.nosp@m.n@gm.nosp@m.ail.c.nosp@m.om)
Date
2026-01-24
969{
970 (void) szQuery;
971 return LoadFileToBuffer(constants::VISUALIZER_ORBITCONTROLS_PATH);
972}
Here is the call graph for this function:
Here is the caller graph for this function:

◆ LoadFileToBuffer()

std::vector< char > VisualizationHandler::LoadFileToBuffer ( const std::string &  szPath)
private

Helper to load a file into a byte buffer.

Parameters
szPath- The file path.
Returns
std::vector<char> - The file content as a byte buffer.
Author
clayjay3 (clayt.nosp@m.onra.nosp@m.ycowe.nosp@m.n@gm.nosp@m.ail.c.nosp@m.om)
Date
2026-01-24
984{
985 std::ifstream stdFile(szPath, std::ios::binary);
986 if (!stdFile.is_open())
987 {
988 LOG_ERROR(logging::g_qSharedLogger, "VisualizationHandler: Failed to load asset: {}", szPath);
989 return {};
990 }
991
992 return std::vector<char>((std::istreambuf_iterator<char>(stdFile)), std::istreambuf_iterator<char>());
993}
Here is the caller graph for this function:

◆ Base64Encode()

std::string VisualizationHandler::Base64Encode ( const std::vector< char > &  vData)
private

Encodes binary data to a Base64 string.

Parameters
vData- The binary data to encode.
Returns
std::string - The Base64 encoded string.
Author
clayjay3 (clayt.nosp@m.onra.nosp@m.ycowe.nosp@m.n@gm.nosp@m.ail.c.nosp@m.om)
Date
2026-01-24
1005{
1006 // Create instance variables.
1007 const std::string szBase64Chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
1008 "abcdefghijklmnopqrstuvwxyz"
1009 "0123456789+/";
1010 std::string szReturnString;
1011 int nIter = 0;
1012 int nJter = 0;
1013 unsigned char aCharArray3[3];
1014 unsigned char aCharArray4[4];
1015
1016 // Loop through each character in the data.
1017 for (char chCharacter : vData)
1018 {
1019 // Fill the 3-byte array.
1020 aCharArray3[nIter++] = chCharacter;
1021
1022 // If we have 3 bytes, encode to 4 Base64 characters.
1023 if (nIter == 3)
1024 {
1025 // Convert to Base64.
1026 aCharArray4[0] = (aCharArray3[0] & 0xfc) >> 2;
1027 aCharArray4[1] = ((aCharArray3[0] & 0x03) << 4) + ((aCharArray3[1] & 0xf0) >> 4);
1028 aCharArray4[2] = ((aCharArray3[1] & 0x0f) << 2) + ((aCharArray3[2] & 0xc0) >> 6);
1029 aCharArray4[3] = aCharArray3[2] & 0x3f;
1030 // Append to return string.
1031 for (nIter = 0; (nIter < 4); nIter++)
1032 {
1033 szReturnString += szBase64Chars[aCharArray4[nIter]];
1034 }
1035
1036 nIter = 0;
1037 }
1038 }
1039
1040 // Handle padding for remaining bytes.
1041 if (nIter)
1042 {
1043 // Fill remaining bytes with zeros.
1044 for (nJter = nIter; nJter < 3; nJter++)
1045 {
1046 aCharArray3[nJter] = '\0';
1047 }
1048 // Convert to Base64.
1049 aCharArray4[0] = (aCharArray3[0] & 0xfc) >> 2;
1050 aCharArray4[1] = ((aCharArray3[0] & 0x03) << 4) + ((aCharArray3[1] & 0xf0) >> 4);
1051 aCharArray4[2] = ((aCharArray3[1] & 0x0f) << 2) + ((aCharArray3[2] & 0xc0) >> 6);
1052 aCharArray4[3] = aCharArray3[2] & 0x3f;
1053 // Append to return string.
1054 for (nJter = 0; (nJter < nIter + 1); nJter++)
1055 {
1056 szReturnString += szBase64Chars[aCharArray4[nJter]];
1057 }
1058 // Add padding '=' characters.
1059 while ((nIter++ < 3))
1060 {
1061 szReturnString += '=';
1062 }
1063 }
1064
1065 return szReturnString;
1066}
Here is the caller graph for this function:

◆ GetEmbeddedHtml()

std::string VisualizationHandler::GetEmbeddedHtml ( )
private

Gets the embedded HTML for the visualization page.

Returns
std::string - The HTML content as a string.
Author
clayjay3 (clayt.nosp@m.onra.nosp@m.ycowe.nosp@m.n@gm.nosp@m.ail.c.nosp@m.om)
Date
2026-01-22
1077{
1078 return R"RAW_HTML(
1079<!DOCTYPE html>
1080<html>
1081<head>
1082 <title>MRDT 3D Visualizer</title>
1083 <style>
1084 body { margin: 0; overflow: hidden; background: #222; font-family: sans-serif; }
1085 #ui-layer {
1086 position: absolute;
1087 top: 10px;
1088 left: 10px;
1089 color: #0f0;
1090 background: rgba(0,0,0,0.5);
1091 padding: 10px;
1092 border-radius: 5px;
1093 pointer-events: none;
1094 min-width: 250px;
1095 z-index: 10;
1096 }
1097 #eta-box {
1098 position: absolute;
1099 top: 10px;
1100 left: 50%;
1101 transform: translateX(-50%);
1102 color: #0f0;
1103 background: rgba(0,0,0,0.5);
1104 padding: 10px;
1105 border-radius: 5px;
1106 font-size: 16px;
1107 font-weight: bold;
1108 pointer-events: none;
1109 z-index: 10;
1110 }
1111 #marker-layer {
1112 position: absolute;
1113 top: 0; left: 0; width: 100%; height: 100%;
1114 pointer-events: none;
1115 overflow: hidden;
1116 }
1117 .hud-marker {
1118 position: absolute;
1119 padding: 4px 8px;
1120 background: rgba(0, 0, 0, 0.7);
1121 color: white;
1122 border: 2px solid white;
1123 border-radius: 4px;
1124 font-size: 14px;
1125 font-weight: bold;
1126 white-space: nowrap;
1127 transform: translate(-50%, -50%);
1128 transition: opacity 0.2s;
1129 }
1130 .hud-marker::after {
1131 content: '';
1132 position: absolute;
1133 top: 50%; left: 50%;
1134 width: 0; height: 0;
1135 }
1136 #legend-layer {
1137 position: absolute;
1138 bottom: 20px;
1139 left: 10px;
1140 color: #fff;
1141 background: rgba(0,0,0,0.7);
1142 padding: 10px;
1143 border-radius: 5px;
1144 pointer-events: none;
1145 min-width: 150px;
1146 z-index: 10;
1147 }
1148 .legend-section { margin-bottom: 10px; border-bottom: 1px solid #555; padding-bottom: 5px; }
1149 .legend-item { display: flex; align-items: center; gap: 10px; margin-bottom: 5px; font-size: 12px; }
1150 .color-box { width: 15px; height: 15px; border: 1px solid #aaa; }
1151 .circle-box { width: 12px; height: 12px; border-radius: 50%; }
1152 .control-group { margin-bottom: 10px; pointer-events: auto; }
1153 label { display: block; font-size: 12px; color: #aaa; }
1154 input[type=range] { width: 100%; }
1155 .val-disp { float: right; color: #fff; }
1156 .btn-group { position: absolute; bottom: 20px; right: 20px; display: flex; gap: 10px; z-index: 10; }
1157 .hud-btn { padding: 10px 20px; background: #444; color: white; border: 2px solid #666; cursor: pointer; font-size: 16px; z-index: 999; }
1158 .hud-btn.active { background: #00aa00; border-color: #00ff00; }
1159 .key { color: #fff; font-weight: bold; border: 1px solid #666; padding: 2px 5px; border-radius: 3px; background: #333; }
1160 h3 { margin-top: 0; border-bottom: 1px solid #555; padding-bottom: 5px; }
1161
1162 #detection-panel { position: absolute; top: 10px; right: 10px; width: auto; background: rgba(0,0,0,0.7); padding: 10px; border-radius: 5px; z-index: 10; transition: width 0.2s; }
1163 #detection-panel h3 { color: #0f0; margin-top: 0; }
1164 .gallery-item { cursor: pointer; }
1165 .gallery-item img { width: 100%; border: 2px solid #666; border-radius: 4px; transition: border-color 0.2s; }
1166 .gallery-item img:hover { border-color: #0f0; }
1167
1168 #detection-modal { display: none; position: fixed; z-index: 1000; left: 0; top: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.9); align-items: center; justify-content: center; }
1169 #detection-modal img { max-width: 90%; max-height: 90%; border: 3px solid #0f0; }
1170 #modal-caption { position: absolute; bottom: 80px; color: #0f0; font-size: 18px; text-align: center; width: 100%; }
1171 #modal-open-btn { position: absolute; bottom: 30px; left: 50%; transform: translateX(-50%); padding: 10px 30px; background: #444; color: white; border: 2px solid #0f0; cursor: pointer; font-size: 16px; border-radius: 5px; }
1172 #modal-open-btn:hover { background: #00aa00; }
1173
1174 .modal-close { position: absolute; top: 20px; right: 40px; color: #fff; font-size: 40px; font-weight: bold; cursor: pointer; }
1175 .modal-close:hover { color: #0f0; }
1176
1177 .modal-nav { position: absolute; top: 50%; transform: translateY(-50%); color: #fff; font-size: 60px; font-weight: bold; cursor: pointer; padding: 20px; user-select: none; z-index: 1001; transition: color 0.2s; }
1178 .modal-nav:hover { color: #0f0; }
1179 .modal-nav.left { left: 20px; }
1180 .modal-nav.right { right: 20px; }
1181 </style>
1182 <script type="importmap">
1183 {
1184 "imports": {
1185 "three": "/lib/three.js",
1186 "three/addons/controls/OrbitControls.js": "/lib/orbit.js"
1187 }
1188 }
1189 </script>
1190</head>
1191<body>
1192 <div id="ui-layer">
1193 <h3 id="settings-toggle" style="cursor: pointer; pointer-events: auto; margin: 0; border: none; padding: 0; user-select: none;">Settings &#9654;</h3>
1194 <div id="settings-content" style="display: none; margin-top: 10px; border-top: 1px solid #555; padding-top: 10px;">
1195 <div class="control-group">
1196 <label style="color: #ccc; cursor: pointer; display: flex; align-items: center; gap: 5px;">
1197 <input type="checkbox" id="cb-ground" checked> Lock Rover to Terrain Height
1198 </label>
1199 </div>
1200 <div class="control-group">
1201 <label>Load Radius (m) <span id="val-rad" class="val-disp">50</span></label>
1202 <input type="range" id="sl-rad" min="10" max="200" value="50" step="10">
1203 </div>
1204 <div class="control-group">
1205 <label>Border Tol. (m) <span id="val-tol" class="val-disp">10</span></label>
1206 <input type="range" id="sl-tol" min="5" max="50" value="10" step="1">
1207 </div>
1208 <div class="control-group">
1209 <label>Min Score <span id="val-score" class="val-disp">0.0</span></label>
1210 <input type="range" id="sl-score" min="0.0" max="1.0" value="0.0" step="0.05">
1211 </div>
1212 <div id="status" style="margin-top:10px; color: #fff;">Status: Free Cam</div>
1213 <div id="stats" style="margin-top:5px; color: #aaa; font-size:12px;">Points: 0</div>
1214 </div>
1215 </div>
1216
1217 <div id="eta-box">ETA: Calculating...</div>
1218
1219 <div id="legend-layer">
1220 <div class="legend-section" id="det-legend">
1221 <strong>Detections</strong>
1222 </div>
1223 <div class="legend-section" id="state-legend">
1224 <strong>State Key</strong>
1225 </div>
1226 </div>
1227
1228 <div id="detection-panel">
1229 <h3>Latest Detection</h3>
1230 <div id="detection-gallery-items"></div>
1231 </div>
1232
1233 <div id="detection-modal" onclick="closeDetectionModal()">
1234 <span class="modal-close" onclick="closeDetectionModal()">&times;</span>
1235 <div class="modal-nav left" onclick="prevDetection(event)">&#10094;</div>
1236 <img id="modal-image" src="" alt="Detection" onclick="event.stopPropagation()">
1237 <div class="modal-nav right" onclick="nextDetection(event)">&#10095;</div>
1238 <div id="modal-caption"></div>
1239 <button id="modal-open-btn" onclick="event.stopPropagation()">Open in New Tab</button>
1240 </div>
1241
1242 <div id="marker-layer"></div>
1243
1244 <div class="btn-group">
1245 <button id="snap-btn" class="hud-btn" onclick="snapToRover()">Snap (Space)</button>
1246 <button id="follow-btn" class="hud-btn" onclick="toggleFollow()">Follow (F)</button>
1247 </div>
1248
1249<script type="module">
1250 import * as THREE from 'three';
1251 import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
1252
1253 let camera, scene, renderer, controls, roverMesh, pathLine, plannedPathLine, currentPoints;
1254 let waypointGroup, detectionGroup;
1255 let markerLayer;
1256 let activeWaypoints = [];
1257 let leftArrow, rightArrow;
1258 let beaconGeo, detectionTex;
1259
1260 let mapCenter = { x: 0, y: 0 };
1261 let cfgRadius = 50;
1262 let cfgTolerance = 10;
1263 let cfgMinScore = 0.0;
1264 let cfgLockGround = true;
1265 let isFetchingMap = false;
1266 let isFollowing = false;
1267 let targetPos = new THREE.Vector3();
1268 let targetHeading = 0.0;
1269 let prevRoverPos = new THREE.Vector3();
1270 const keys = { w:false, a:false, s:false, d:false, q:false, e:false, shift:false };
1271 let lastTime = performance.now();
1272
1273 // ETA Variables
1274 let lastTelemetryTime = 0;
1275 let lastTelemetryPos = new THREE.Vector3();
1276 let avgSpeed = 0.0;
1277 let pathDistance = 0.0;
1278 const speedHistory = [];
1279 let lastPathPoint = null;
1280
1281 // Detection Gallery Tracking
1282 let detectionFilenames = [];
1283 let currentModalIndex = 0;
1284
1285 const typeColors = {};
1286 const typeNames = {};
1287
1288 // State Colors
1289 const stateColors = {
1290 0: { name: "Idle", color: "#888888" },
1291 1: { name: "Navigating", color: "#00ffff" },
1292 2: { name: "Search Pattern", color: "#0000ff" },
1293 3: { name: "Approach Marker", color: "#ffffff" },
1294 4: { name: "Approach Object", color: "#ffaa00" },
1295 5: { name: "Verify Pos", color: "#00550e" },
1296 6: { name: "Verify Marker", color: "#06ac00" },
1297 7: { name: "Verify Object", color: "#78ff66" },
1298 8: { name: "Reversing", color: "#ff0000" },
1299 9: { name: "Stuck", color: "#330000" }
1300 };
1301
1302 // Detection Colors
1303 const detectColors = {
1304 10: { name: "Tag (Aruco)", color: "#aa00ff" }, // Purple
1305 11: { name: "Mallet", color: "#ffa500" }, // Orange
1306 12: { name: "Bottle", color: "#0088ff" }, // Blue
1307 13: { name: "Pick", color: "#ffee00" } // Yellow
1308 };
1309
1310 // Terrain Height Sampler
1311 function getTerrainHeight(rx, rz, defaultY) {
1312 if (!currentPoints || !currentPoints.geometry || !currentPoints.geometry.attributes.position) return defaultY;
1313 const positions = currentPoints.geometry.attributes.position.array;
1314 let sumY = 0;
1315 let count = 0;
1316 const radiusSq = 2.25; // 1.5m radius for averaging terrain
1317
1318 for (let i = 0; i < positions.length; i += 3) {
1319 const dx = positions[i] - rx;
1320 const dz = positions[i+2] - rz;
1321 if (dx*dx + dz*dz < radiusSq) {
1322 sumY += positions[i+1];
1323 count++;
1324 }
1325 }
1326 return count > 0 ? (sumY / count) : defaultY;
1327 }
1328
1329 // Thick Line / InstancedMesh Renderer (Replaces Firefox-broken LineBasicMaterial)
1330 function createThickPath(vertices, colors, radius, defaultColorHex) {
1331 if (vertices.length < 6) return null;
1332
1333 const numSegments = (vertices.length / 3) - 1;
1334 const cylinderGeo = new THREE.CylinderGeometry(radius, radius, 1, 8, 1, false);
1335 cylinderGeo.translate(0, 0.5, 0);
1336 cylinderGeo.rotateX(Math.PI / 2);
1337
1338 const mat = new THREE.MeshBasicMaterial();
1339 if (!colors) mat.color.setHex(defaultColorHex);
1340
1341 const mesh = new THREE.InstancedMesh(cylinderGeo, mat, numSegments);
1342 const p1 = new THREE.Vector3();
1343 const p2 = new THREE.Vector3();
1344 const dummy = new THREE.Object3D();
1345 const col = new THREE.Color();
1346
1347 for (let i = 0; i < numSegments; i++) {
1348 const idx = i * 3;
1349 p1.set(vertices[idx], vertices[idx+1], vertices[idx+2]);
1350 p2.set(vertices[idx+3], vertices[idx+4], vertices[idx+5]);
1351
1352 const dist = p1.distanceTo(p2);
1353 if (dist < 0.001) {
1354 dummy.scale.set(0, 0, 0);
1355 dummy.updateMatrix();
1356 mesh.setMatrixAt(i, dummy.matrix);
1357 continue;
1358 }
1359
1360 dummy.position.copy(p1);
1361 dummy.lookAt(p2);
1362 dummy.scale.set(1, 1, dist);
1363 dummy.updateMatrix();
1364 mesh.setMatrixAt(i, dummy.matrix);
1365
1366 if (colors) {
1367 col.setRGB(colors[idx], colors[idx+1], colors[idx+2]);
1368 mesh.setColorAt(i, col);
1369 }
1370 }
1371
1372 mesh.instanceMatrix.needsUpdate = true;
1373 if (colors) mesh.instanceColor.needsUpdate = true;
1374 return mesh;
1375 }
1376
1377 init();
1378 animate();
1379
1380 function init() {
1381 markerLayer = document.getElementById('marker-layer');
1382 scene = new THREE.Scene();
1383 scene.background = new THREE.Color(0x111111);
1384 scene.add(new THREE.GridHelper(100, 100));
1385 scene.add(new THREE.AxesHelper(2));
1386
1387 camera = new THREE.PerspectiveCamera(60, window.innerWidth/window.innerHeight, 0.1, 10000);
1388 camera.position.set(0, 10, -10);
1389
1390 renderer = new THREE.WebGLRenderer({ antialias: true });
1391 renderer.setSize(window.innerWidth, window.innerHeight);
1392 document.body.appendChild(renderer.domElement);
1393
1394 controls = new OrbitControls(camera, renderer.domElement);
1395 controls.enableDamping = true;
1396 controls.maxDistance = 5000;
1397
1398 const geometry = new THREE.BoxGeometry(1, 0.5, 1.5);
1399 geometry.translate(0, 0.25, 0);
1400 const material = new THREE.MeshBasicMaterial({ color: 0xff00ff, wireframe: true });
1401 roverMesh = new THREE.Mesh(geometry, material);
1402 scene.add(roverMesh);
1403
1404 // Drive Vectors (Arrows)
1405 const arrowDir = new THREE.Vector3(0, 0, -1);
1406 const arrowOrigin = new THREE.Vector3(0, 0, 0);
1407 const arrowLen = 1;
1408 const arrowCol = 0xffff00;
1409 leftArrow = new THREE.ArrowHelper(arrowDir, arrowOrigin, arrowLen, arrowCol);
1410 rightArrow = new THREE.ArrowHelper(arrowDir, arrowOrigin, arrowLen, arrowCol);
1411 roverMesh.add(leftArrow);
1412 roverMesh.add(rightArrow);
1413 leftArrow.position.set(-0.6, 0, 0);
1414 rightArrow.position.set(0.6, 0, 0);
1415
1416 beaconGeo = new THREE.BoxGeometry(0.5, 10000, 0.5);
1417 const canvas = document.createElement('canvas');
1418 canvas.width = 32; canvas.height = 32;
1419 const ctx = canvas.getContext('2d');
1420 ctx.beginPath();
1421 ctx.arc(16,16,14,0,2*Math.PI);
1422 ctx.fillStyle = 'white';
1423 ctx.fill();
1424 detectionTex = new THREE.CanvasTexture(canvas);
1425
1426 waypointGroup = new THREE.Group();
1427 scene.add(waypointGroup);
1428
1429 detectionGroup = new THREE.Group();
1430 scene.add(detectionGroup);
1431
1432 const config = [
1433 { id: 0, name: "NAV", color: "#00ffff" },
1434 { id: 1, name: "TAG", color: "#ffffff" },
1435 { id: 2, name: "MALLET", color: "#ffa500" },
1436 { id: 3, name: "BOTTLE", color: "#0088ff" },
1437 { id: 4, name: "PICK", color: "#ffee00" },
1438 { id: 5, name: "OBJ", color: "#aaaaaa" },
1439 { id: 6, name: "OBSTACLE", color: "#ff0000" },
1440 { id: 7, name: "UNKNOWN", color: "#000000" },
1441 { id: 8, name: "GOAL REACHED", color: "#00ff00" }
1442 ];
1443
1444 config.forEach(c => {
1445 typeNames[c.id] = c.name;
1446 typeColors[c.id] = new THREE.Color(c.color);
1447 });
1448
1449 // Legend: Detections
1450 const detLegendDiv = document.getElementById('det-legend');
1451 for (const [id, data] of Object.entries(detectColors)) {
1452 const item = document.createElement('div');
1453 item.className = 'legend-item';
1454 item.innerHTML = `<div class="circle-box" style="background:${data.color}"></div><span>${data.name}</span>`;
1455 detLegendDiv.appendChild(item);
1456 }
1457
1458 // Legend: States
1459 const stateLegendDiv = document.getElementById('state-legend');
1460 for (const [id, data] of Object.entries(stateColors)) {
1461 const item = document.createElement('div');
1462 item.className = 'legend-item';
1463 item.innerHTML = `<div class="color-box" style="background:${data.color}"></div><span>${data.name}</span>`;
1464 stateLegendDiv.appendChild(item);
1465 }
1466
1467 // Toggles & Inputs
1468 const cbGround = document.getElementById('cb-ground');
1469 if (cbGround) {
1470 cbGround.addEventListener('change', (e) => {
1471 cfgLockGround = e.target.checked;
1472 if (cfgLockGround) {
1473 targetPos.y = getTerrainHeight(targetPos.x, targetPos.z, targetPos.y);
1474 }
1475 });
1476 }
1477 document.getElementById('sl-rad').oninput = (e) => {
1478 cfgRadius = parseInt(e.target.value);
1479 document.getElementById('val-rad').innerText = cfgRadius;
1480 checkBoundary(true);
1481 };
1482 document.getElementById('sl-tol').oninput = (e) => {
1483 cfgTolerance = parseInt(e.target.value);
1484 document.getElementById('val-tol').innerText = cfgTolerance;
1485 };
1486 document.getElementById('sl-score').oninput = (e) => {
1487 cfgMinScore = parseFloat(e.target.value);
1488 document.getElementById('val-score').innerText = cfgMinScore.toFixed(2);
1489 checkBoundary(true);
1490 };
1491
1492 // UI Layer Collapsible Toggle
1493 const settingsToggle = document.getElementById('settings-toggle');
1494 const settingsContent = document.getElementById('settings-content');
1495 settingsToggle.addEventListener('click', () => {
1496 if (settingsContent.style.display === 'none') {
1497 settingsContent.style.display = 'block';
1498 settingsToggle.innerHTML = 'Settings &#9660;';
1499 } else {
1500 settingsContent.style.display = 'none';
1501 settingsToggle.innerHTML = 'Settings &#9654;';
1502 }
1503 });
1504
1505 window.addEventListener('keydown', (e) => onKey(e, true));
1506 window.addEventListener('keyup', (e) => onKey(e, false));
1507 window.addEventListener('resize', onWindowResize);
1508
1509 window.toggleFollow = () => {
1510 isFollowing = !isFollowing;
1511 document.getElementById('follow-btn').classList.toggle('active', isFollowing);
1512 document.getElementById('status').innerText = isFollowing ? "Status: Locked" : "Status: Free Cam";
1513 if(isFollowing) controls.target.copy(roverMesh.position);
1514 };
1515 window.snapToRover = () => {
1516 camera.position.copy(roverMesh.position).add(new THREE.Vector3(0,10,-10));
1517 controls.target.copy(roverMesh.position);
1518 };
1519
1520 requestTelemetryLoop();
1521 setInterval(fetchPlannedPath, 2000);
1522 setInterval(fetchWaypoints, 2000);
1523 setInterval(fetchDetections, 1000);
1524 setInterval(fetchDetectionsList, 5000);
1525 fetchMapSquare(0, 0);
1526 }
1527
1528 // --- RECURSIVE LOOP ---
1529 async function requestTelemetryLoop() {
1530 if (!document.hidden) await fetchTelemetry();
1531 setTimeout(requestTelemetryLoop, 50);
1532 }
1533
1534 async function fetchTelemetry() {
1535 try {
1536 const response = await fetch('/api/telemetry');
1537 if (!response.ok) return;
1538 const buffer = await response.arrayBuffer();
1539 updateTelemetry(buffer);
1540 } catch (e) { }
1541 }
1542
1543 async function fetchPlannedPath() {
1544 try {
1545 const response = await fetch('/api/planned_path');
1546 const buffer = await response.arrayBuffer();
1547 updatePlannedPath(buffer);
1548 } catch(e) {}
1549 }
1550
1551 async function fetchWaypoints() {
1552 try {
1553 const response = await fetch('/api/waypoints');
1554 const buffer = await response.arrayBuffer();
1555 updateWaypoints(buffer);
1556 } catch(e) {}
1557 }
1558
1559 async function fetchDetections() {
1560 try {
1561 const response = await fetch('/api/detections');
1562 const buffer = await response.arrayBuffer();
1563 updateDetections(buffer);
1564 } catch(e) {}
1565 }
1566
1567 async function fetchDetectionsList() {
1568 try {
1569 const response = await fetch('/api/detection_list');
1570 const filenames = await response.json();
1571 updateDetectionGallery(filenames);
1572 } catch(e) {
1573 console.error('Failed to fetch detection list:', e);
1574 }
1575 }
1576
1577 function updateDetectionGallery(filenames) {
1578 detectionFilenames = filenames;
1579 const panel = document.getElementById('detection-panel');
1580 const gallery = document.getElementById('detection-gallery-items');
1581 const header = panel ? panel.querySelector('h3') : null;
1582 if (!gallery) return;
1583
1584 gallery.innerHTML = '';
1585
1586 if (filenames.length === 0) {
1587 if (panel) panel.style.width = 'auto';
1588 if (header) header.style.display = 'none';
1589 gallery.innerHTML = '<div style="color:#aaa; font-size:12px; text-align:center;">No detections</div>';
1590 return;
1591 }
1592
1593 if (panel) panel.style.width = '250px';
1594 if (header) header.style.display = 'block';
1595
1596 const latestIndex = filenames.length - 1;
1597 const filename = filenames[latestIndex];
1598
1599 const item = document.createElement('div');
1600 item.className = 'gallery-item';
1601
1602 const img = document.createElement('img');
1603 img.src = `/detections/${filename}`;
1604 img.alt = filename;
1605 img.title = "Click to view full gallery";
1606 img.addEventListener('click', () => showDetectionModal(latestIndex));
1607
1608 const info = document.createElement('div');
1609 info.style.color = '#fff';
1610 info.style.fontSize = '14px';
1611 info.style.textAlign = 'center';
1612 info.style.marginTop = '8px';
1613 info.innerText = `View all ${filenames.length} images`;
1614
1615 item.appendChild(img);
1616 item.appendChild(info);
1617 gallery.appendChild(item);
1618 }
1619
1620 window.showDetectionModal = function(index) {
1621 const modal = document.getElementById('detection-modal');
1622 const img = document.getElementById('modal-image');
1623 const caption = document.getElementById('modal-caption');
1624 const openBtn = document.getElementById('modal-open-btn');
1625
1626 if (modal && img && caption && detectionFilenames.length > 0) {
1627 currentModalIndex = index;
1628 const filename = detectionFilenames[currentModalIndex];
1629 const src = `/detections/${filename}`;
1630
1631 img.src = src;
1632 caption.innerText = `${filename} (${currentModalIndex + 1} of ${detectionFilenames.length})`;
1633
1634 if (openBtn) {
1635 openBtn.onclick = () => window.open(src, '_blank');
1636 }
1637 modal.style.display = 'flex';
1638 }
1639 }
1640
1641 window.closeDetectionModal = function() {
1642 const modal = document.getElementById('detection-modal');
1643 if (modal) modal.style.display = 'none';
1644 }
1645
1646 window.nextDetection = function(e) {
1647 e.stopPropagation();
1648 if (detectionFilenames.length === 0) return;
1649 let newIdx = currentModalIndex + 1;
1650 if (newIdx >= detectionFilenames.length) newIdx = 0;
1651 showDetectionModal(newIdx);
1652 }
1653
1654 window.prevDetection = function(e) {
1655 e.stopPropagation();
1656 if (detectionFilenames.length === 0) return;
1657 let newIdx = currentModalIndex - 1;
1658 if (newIdx < 0) newIdx = detectionFilenames.length - 1;
1659 showDetectionModal(newIdx);
1660 }
1661
1662 function updateArrow(arrow, power) {
1663 const absPwr = Math.abs(power);
1664 const dir = power >= 0 ? new THREE.Vector3(0, 0, -1) : new THREE.Vector3(0, 0, 1);
1665 arrow.setDirection(dir);
1666 arrow.setLength(Math.max(absPwr * 2.0, 0.001), 0.2, 0.1);
1667 const col = power >= 0 ? 0x00ff00 : 0xff0000;
1668 arrow.setColor(col);
1669 }
1670
1671 function updateTelemetry(buffer) {
1672 const view = new DataView(buffer);
1673 const rx = view.getFloat32(0, true);
1674 const ry = view.getFloat32(4, true);
1675 const rz = view.getFloat32(8, true);
1676 const rh = view.getFloat32(12, true);
1677
1678 // Calculate Speed using actual un-flattened 3D position
1679 const now = performance.now();
1680 const newPos = new THREE.Vector3(rx, ry, -rz);
1681 if (lastTelemetryTime > 0) {
1682 const dt = (now - lastTelemetryTime) / 1000.0;
1683 if (dt > 0.1) {
1684 const dist = newPos.distanceTo(lastTelemetryPos);
1685 const instSpeed = dist / dt;
1686 speedHistory.push(instSpeed);
1687 if (speedHistory.length > 20) speedHistory.shift();
1688 avgSpeed = speedHistory.reduce((a,b)=>a+b, 0) / speedHistory.length;
1689 }
1690 }
1691 lastTelemetryPos.copy(newPos);
1692 lastTelemetryTime = now;
1693
1694 // Drive Powers
1695 const leftPwr = view.getFloat32(16, true);
1696 const rightPwr = view.getFloat32(20, true);
1697 updateArrow(leftArrow, leftPwr);
1698 updateArrow(rightArrow, rightPwr);
1699
1700 let targetY = ry;
1701 if (cfgLockGround) {
1702 targetY = getTerrainHeight(rx, -rz, ry);
1703 }
1704 targetPos.set(rx, targetY, -rz);
1705 targetHeading = -rh * (Math.PI / 180.0);
1706
1707 checkBoundary(false);
1708
1709 const pathCount = view.getUint32(24, true); // Offset 24
1710 if (pathCount > 0) {
1711 if (pathLine) {
1712 scene.remove(pathLine);
1713 if (pathLine.geometry) pathLine.geometry.dispose();
1714 if (pathLine.material) pathLine.material.dispose();
1715 }
1716 const floats = new Float32Array(buffer, 28, pathCount * 4); // Offset 28
1717 const vertices = [];
1718 const colors = [];
1719 const c = new THREE.Color();
1720
1721 for(let i=0; i<floats.length; i+=4) {
1722 vertices.push(floats[i], floats[i+1], -floats[i+2]);
1723 const state = Math.floor(floats[i+3]);
1724 const hex = stateColors[state] ? stateColors[state].color : "#ffffff";
1725 c.set(hex);
1726 colors.push(c.r, c.g, c.b);
1727 }
1728
1729 pathLine = createThickPath(vertices, colors, 0.1, 0xffffff);
1730 if (pathLine) scene.add(pathLine);
1731 }
1732 }
1733
1734 function updatePlannedPath(buffer) {
1735 const view = new DataView(buffer);
1736 const count = view.getUint32(0, true);
1737 if (plannedPathLine) {
1738 scene.remove(plannedPathLine);
1739 if (plannedPathLine.geometry) plannedPathLine.geometry.dispose();
1740 if (plannedPathLine.material) plannedPathLine.material.dispose();
1741 plannedPathLine = null;
1742 }
1743
1744 pathDistance = 0.0;
1745 lastPathPoint = null;
1746
1747 if (count > 0) {
1748 const floats = new Float32Array(buffer, 4, count * 3);
1749 const vertices = [];
1750 for(let i=0; i<floats.length; i+=3) {
1751 vertices.push(floats[i], floats[i+1], -floats[i+2]);
1752 }
1753
1754 // Calculate total path distance (sum of segments)
1755 // Add distance from rover to first point
1756 if(vertices.length >= 3) {
1757 const firstPt = new THREE.Vector3(vertices[0], vertices[1], vertices[2]);
1758 pathDistance += targetPos.distanceTo(firstPt);
1759 // Store Last Point
1760 const lastIdx = vertices.length - 3;
1761 lastPathPoint = new THREE.Vector3(vertices[lastIdx], vertices[lastIdx+1], vertices[lastIdx+2]);
1762 }
1763 // Add segments
1764 for(let i=0; i<vertices.length-3; i+=3) {
1765 const p1 = new THREE.Vector3(vertices[i], vertices[i+1], vertices[i+2]);
1766 const p2 = new THREE.Vector3(vertices[i+3], vertices[i+4], vertices[i+5]);
1767 pathDistance += p1.distanceTo(p2);
1768 }
1769
1770 plannedPathLine = createThickPath(vertices, null, 0.1, 0xeeff00);
1771 if (plannedPathLine) scene.add(plannedPathLine);
1772 }
1773 }
1774
1775 function updateWaypoints(buffer) {
1776 while(waypointGroup.children.length > 0){
1777 const child = waypointGroup.children[0];
1778 waypointGroup.remove(child);
1779 if (child.material) child.material.dispose();
1780 }
1781 markerLayer.innerHTML = '';
1782 activeWaypoints = [];
1783
1784 const view = new DataView(buffer);
1785 const count = view.getUint32(0, true);
1786 if(count === 0) return;
1787
1788 let offset = 4;
1789
1790 for(let i=0; i<count; i++) {
1791 const x = view.getFloat32(offset, true);
1792 const y = view.getFloat32(offset+4, true);
1793 const z = view.getFloat32(offset+8, true);
1794 const type = view.getInt32(offset+12, true);
1795 offset += 16;
1796
1797 const col = typeColors[type] || typeColors[7];
1798 const beaconMat = new THREE.MeshBasicMaterial({
1799 color: col,
1800 transparent: true,
1801 opacity: 0.3,
1802 depthTest: false
1803 });
1804 const beacon = new THREE.Mesh(beaconGeo, beaconMat);
1805 beacon.position.set(x, 0, -z);
1806 waypointGroup.add(beacon);
1807
1808 const div = document.createElement('div');
1809 div.className = 'hud-marker';
1810 div.innerText = typeNames[type] || "UNK";
1811 div.style.borderColor = "#" + col.getHexString();
1812 markerLayer.appendChild(div);
1813
1814 activeWaypoints.push({
1815 div: div,
1816 pos: new THREE.Vector3(x, 0, -z)
1817 });
1818 }
1819 }
1820
1821 function updateDetections(buffer) {
1822 while(detectionGroup.children.length > 0){
1823 const child = detectionGroup.children[0];
1824 detectionGroup.remove(child);
1825 if (child.geometry) child.geometry.dispose();
1826 if (child.material) child.material.dispose();
1827 }
1828
1829 const view = new DataView(buffer);
1830 const count = view.getUint32(0, true);
1831 if(count === 0) return;
1832
1833 let offset = 4;
1834
1835 for(let i=0; i<count; i++) {
1836 const x = view.getFloat32(offset, true);
1837 const y = view.getFloat32(offset+4, true);
1838 const z = view.getFloat32(offset+8, true);
1839 const type = view.getInt32(offset+12, true);
1840 offset += 16;
1841
1842 let col = "#ffffff";
1843 if(detectColors[type]) col = detectColors[type].color;
1844
1845 const mat = new THREE.PointsMaterial({
1846 color: col,
1847 map: detectionTex,
1848 size: 2.0, // Large persistent dot
1849 sizeAttenuation: true,
1850 alphaTest: 0.5,
1851 transparent: true
1852 });
1853 const geo = new THREE.BufferGeometry();
1854 geo.setAttribute('position', new THREE.Float32BufferAttribute([x, y, -z], 3));
1855
1856 const pt = new THREE.Points(geo, mat);
1857 detectionGroup.add(pt);
1858 }
1859 }
1860
1861 function updateHUD() {
1862 const width = window.innerWidth;
1863 const height = window.innerHeight;
1864 const pad = 30;
1865
1866 // Update ETA Box
1867 const etaBox = document.getElementById('eta-box');
1868
1869 // Reached End Logic
1870 let bReached = false;
1871 if (lastPathPoint && roverMesh.position.distanceTo(lastPathPoint) < 2.0) {
1872 bReached = true;
1873 }
1874
1875 if (bReached) {
1876 etaBox.innerText = "Status: Reached End of Path";
1877 etaBox.style.color = "#00ff00"; // Green
1878 } else {
1879 etaBox.style.color = "#0f0"; // Default Green
1880 if (avgSpeed < 0.05) {
1881 etaBox.innerText = "ETA: Stopped";
1882 } else {
1883 const timeSec = pathDistance / avgSpeed;
1884 if (!isFinite(timeSec) || timeSec < 0) {
1885 etaBox.innerText = "ETA: --:--";
1886 } else {
1887 const min = Math.floor(timeSec / 60);
1888 const sec = Math.floor(timeSec % 60);
1889 etaBox.innerText = `ETA: ${min}m ${sec}s (${avgSpeed.toFixed(2)} m/s)`;
1890 }
1891 }
1892 }
1893
1894 activeWaypoints.forEach(wp => {
1895 const target = wp.pos.clone();
1896 target.y = roverMesh.position.y + 3.0;
1897 target.project(camera);
1898
1899 let x = (target.x * .5 + .5) * width;
1900 let y = (target.y * -.5 + .5) * height;
1901
1902 const isBehind = target.z > 1;
1903
1904 if (isBehind) {
1905 x = width - x;
1906 y = height - y;
1907 }
1908
1909 const cx = width / 2;
1910 const cy = height / 2;
1911 const dx = x - cx;
1912 const dy = y - cy;
1913
1914 if (!isBehind && x >= pad && x <= width - pad && y >= pad && y <= height - pad) {
1915 y -= 40;
1916 wp.div.style.opacity = "0.9";
1917 } else {
1918 let t = Infinity;
1919 if (dx > 0) t = Math.min(t, (width - pad - cx) / dx);
1920 if (dx < 0) t = Math.min(t, (pad - cx) / dx);
1921 if (dy > 0) t = Math.min(t, (height - pad - cy) / dy);
1922 if (dy < 0) t = Math.min(t, (pad - cy) / dy);
1923
1924 x = cx + dx * t;
1925 y = cy + dy * t;
1926 wp.div.style.opacity = "0.6";
1927 }
1928
1929 wp.div.style.left = x + 'px';
1930 wp.div.style.top = y + 'px';
1931
1932 // Use 2D horizontal distance for distance display.
1933 const dxPos = roverMesh.position.x - wp.pos.x;
1934 const dzPos = roverMesh.position.z - wp.pos.z;
1935 const dist = Math.sqrt(dxPos*dxPos + dzPos*dzPos);
1936
1937 wp.div.innerText = `${wp.div.innerText.split(' ')[0]} ${Math.round(dist)}m`;
1938 });
1939 }
1940
1941 function checkBoundary(force) {
1942 if(isFetchingMap && !force) return;
1943 const roverX = targetPos.x;
1944 const roverN = -targetPos.z;
1945 const distE = Math.abs(roverX - mapCenter.x);
1946 const distN = Math.abs(roverN - mapCenter.y);
1947 const limit = cfgRadius - cfgTolerance;
1948
1949 if (force || distE > limit || distN > limit) {
1950 fetchMapSquare(roverX, roverN);
1951 }
1952 }
1953
1954 async function fetchMapSquare(x, y) {
1955 if(isFetchingMap) return;
1956 isFetchingMap = true;
1957 try {
1958 const url = `/api/map?x=${x.toFixed(2)}&y=${y.toFixed(2)}&r=${cfgRadius}&s=${cfgMinScore}`;
1959 const response = await fetch(url);
1960 const buffer = await response.arrayBuffer();
1961 loadMapPoints(buffer);
1962 mapCenter = { x: x, y: y };
1963 } catch(e) { console.error(e); }
1964 isFetchingMap = false;
1965 }
1966
1967 function loadMapPoints(buffer) {
1968 const view = new DataView(buffer);
1969 const count = view.getUint32(0, true);
1970
1971 if(currentPoints) {
1972 scene.remove(currentPoints);
1973 currentPoints.geometry.dispose();
1974 currentPoints.material.dispose();
1975 currentPoints = null;
1976 }
1977
1978 if(count === 0) {
1979 document.getElementById('stats').innerText = "Points: 0";
1980 return;
1981 }
1982
1983 const pts = [];
1984 const colors = [];
1985 const c = new THREE.Color();
1986 const floats = new Float32Array(buffer, 4, count * 4);
1987
1988 for(let i=0; i<floats.length; i+=4) {
1989 pts.push(floats[i], floats[i+1], -floats[i+2]);
1990 c.setHSL(floats[i+3] * 0.33, 1.0, 0.5);
1991 colors.push(c.r, c.g, c.b);
1992 }
1993
1994 const geo = new THREE.BufferGeometry();
1995 geo.setAttribute('position', new THREE.Float32BufferAttribute(pts, 3));
1996 geo.setAttribute('color', new THREE.Float32BufferAttribute(colors, 3));
1997 const mat = new THREE.PointsMaterial({ size: 0.5, vertexColors: true });
1998
1999 currentPoints = new THREE.Points(geo, mat);
2000 scene.add(currentPoints);
2001 document.getElementById('stats').innerText = "Points: " + count;
2002 }
2003
2004 function onKey(e, p) {
2005 if(keys.hasOwnProperty(e.key.toLowerCase())) keys[e.key.toLowerCase()] = p;
2006 if(e.key === 'Shift') keys.shift = p;
2007 if(p && e.key==='f') window.toggleFollow();
2008 if(p && e.key===' ') window.snapToRover();
2009 }
2010 function onWindowResize() { camera.aspect = window.innerWidth / window.innerHeight; camera.updateProjectionMatrix(); renderer.setSize(window.innerWidth, window.innerHeight); }
2011
2012 function animate() {
2013 requestAnimationFrame(animate);
2014 const now = performance.now();
2015 const dt = Math.min((now - lastTime) / 1000.0, 0.1);
2016 lastTime = now;
2017
2018 prevRoverPos.copy(roverMesh.position);
2019 const lerpFactor = 5.0 * dt;
2020 roverMesh.position.lerp(targetPos, lerpFactor);
2021 const dRot = targetHeading - roverMesh.rotation.y;
2022 roverMesh.rotation.y += Math.atan2(Math.sin(dRot), Math.cos(dRot)) * lerpFactor;
2023
2024 if(isFollowing) {
2025 camera.position.add(new THREE.Vector3().subVectors(roverMesh.position, prevRoverPos));
2026 controls.target.copy(roverMesh.position);
2027 } else {
2028 const spd = (keys.shift?15:5)*dt;
2029 const fwd = new THREE.Vector3(); camera.getWorldDirection(fwd); fwd.y=0; fwd.normalize();
2030 const rgt = new THREE.Vector3().crossVectors(fwd, camera.up).normalize();
2031 if(keys.w) camera.position.addScaledVector(fwd, spd);
2032 if(keys.s) camera.position.addScaledVector(fwd, -spd);
2033 if(keys.d) camera.position.addScaledVector(rgt, spd);
2034 if(keys.a) camera.position.addScaledVector(rgt, -spd);
2035 if(keys.q) camera.position.y += spd;
2036 if(keys.e) camera.position.y -= spd;
2037 controls.target.add(new THREE.Vector3(0,0,0).addScaledVector(fwd, (keys.w-keys.s)*spd).addScaledVector(rgt, (keys.d-keys.a)*spd));
2038 }
2039 controls.update();
2040
2041 updateHUD();
2042
2043 renderer.render(scene, camera);
2044 }
2045</script>
2046</body>
2047</html>
2048 )RAW_HTML";
2049}
Here is the caller graph for this function:

◆ GenerateStaticHtml()

std::string VisualizationHandler::GenerateStaticHtml ( const std::vector< LiDARHandler::PointRow > &  vLidar)
private

Generates a static HTML page with embedded LiDAR data and embedded dependencies.

Parameters
vLidar- Vector of LiDAR point rows to include in the HTML.
Returns
std::string - The generated HTML content as a string.
Author
clayjay3 (clayt.nosp@m.onra.nosp@m.ycowe.nosp@m.n@gm.nosp@m.ail.c.nosp@m.om)
Date
2026-01-22
2061{
2062 // 1. Load and Encode Assets
2063 std::string szThreeJS = Base64Encode(LoadFileToBuffer(constants::VISUALIZER_THREEJS_PATH));
2064 std::string szOrbitJS = Base64Encode(LoadFileToBuffer(constants::VISUALIZER_ORBITCONTROLS_PATH));
2065
2066 if (szThreeJS.empty() || szOrbitJS.empty())
2067 {
2068 LOG_ERROR(logging::g_qSharedLogger, "VisualizationHandler: Cannot generate monolithic export. Missing assets.");
2069 return "<html><body>Error: Missing ThreeJS assets on rover. Ensure 'assets/three.module.js' and 'assets/OrbitControls.js' exist.</body></html>";
2070 }
2071
2072 std::stringstream stdSS;
2073 stdSS << std::fixed << std::setprecision(3);
2074
2075 // Write Header
2076 stdSS << R"RAW(
2077<!DOCTYPE html>
2078<html>
2079<head>
2080 <title>Mars Rover Static Export</title>
2081 <style>
2082 body { margin: 0; overflow: hidden; background: #222; font-family: sans-serif; }
2083 #ui-layer { position: absolute; top: 10px; left: 10px; color: #0f0; background: rgba(0,0,0,0.5); padding: 10px; border-radius: 5px; pointer-events: none; min-width: 250px; z-index: 10; }
2084 #marker-layer { position: absolute; top: 0; left: 0; width: 100%; height: 100%; pointer-events: none; overflow: hidden; }
2085 .hud-marker { position: absolute; padding: 4px 8px; background: rgba(0, 0, 0, 0.7); color: white; border: 2px solid white; border-radius: 4px; font-size: 14px; font-weight: bold; white-space: nowrap; transform: translate(-50%, -50%); transition: opacity 0.2s; }
2086 .hud-marker::after { content: ''; position: absolute; top: 50%; left: 50%; width: 0; height: 0; }
2087 #legend-layer { position: absolute; bottom: 20px; left: 10px; color: #fff; background: rgba(0,0,0,0.7); padding: 10px; border-radius: 5px; pointer-events: none; min-width: 150px; z-index: 10; }
2088 .legend-section { margin-bottom: 10px; border-bottom: 1px solid #555; padding-bottom: 5px; }
2089 .legend-item { display: flex; align-items: center; gap: 10px; margin-bottom: 5px; font-size: 12px; }
2090 .color-box { width: 15px; height: 15px; border: 1px solid #aaa; }
2091 .circle-box { width: 12px; height: 12px; border-radius: 50%; }
2092 .control-group { margin-bottom: 10px; pointer-events: auto; }
2093 label { display: block; font-size: 12px; color: #aaa; }
2094 input[type=range] { width: 100%; }
2095 .val-disp { float: right; color: #fff; }
2096 .btn-group { position: absolute; bottom: 20px; right: 20px; display: flex; gap: 10px; z-index: 10; }
2097 .hud-btn { padding: 10px 20px; background: #444; color: white; border: 2px solid #666; cursor: pointer; font-size: 16px; z-index: 999; }
2098 .hud-btn.active { background: #00aa00; border-color: #00ff00; }
2099 h3 { margin-top: 0; border-bottom: 1px solid #555; padding-bottom: 5px; }
2100 </style>
2101 <script type="importmap">
2102 {
2103 "imports": {
2104 "three": "data:text/javascript;base64,)RAW";
2105
2106 // Inject ThreeJS Base64
2107 stdSS << szThreeJS;
2108
2109 stdSS << R"RAW(",
2110 "three/addons/controls/OrbitControls.js": "data:text/javascript;base64,)RAW";
2111
2112 // Inject OrbitControls Base64
2113 stdSS << szOrbitJS;
2114
2115 stdSS << R"RAW("
2116 }
2117 }
2118 </script>
2119</head>
2120<body>
2121 <div id="ui-layer">
2122 <h3 id="settings-toggle" style="cursor: pointer; pointer-events: auto; margin: 0; border: none; padding: 0; user-select: none;">Static Export &#9654;</h3>
2123 <div id="settings-content" style="display: none; margin-top: 10px; border-top: 1px solid #555; padding-top: 10px;">
2124 <div class="control-group">
2125 <label style="color: #ccc; cursor: pointer; display: flex; align-items: center; gap: 5px;">
2126 <input type="checkbox" id="cb-ground" checked> Lock Rover to Terrain Height
2127 </label>
2128 </div>
2129 <div class="control-group">
2130 <label>Min Score <span id="val-score" class="val-disp">0.0</span></label>
2131 <input type="range" id="sl-score" min="0.0" max="1.0" value="0.0" step="0.05">
2132 </div>
2133 <div id="stats" style="margin-top:5px; color: #aaa; font-size:12px;">Points: 0</div>
2134 </div>
2135 </div>
2136 <div id="legend-layer">
2137 <div class="legend-section" id="det-legend"><strong>Detections</strong></div>
2138 <div class="legend-section" id="state-legend"><strong>State Key</strong></div>
2139 </div>
2140 <div id="marker-layer"></div>
2141 <div class="btn-group">
2142 <button class="hud-btn" onclick="snapToRover()">Snap (Space)</button>
2143 </div>
2144)RAW";
2145
2146 stdSS << "<script>\n";
2147
2148 // LiDAR Data Loop.
2149 stdSS << " const RAW_LIDAR = [";
2150 for (size_t siIter = 0; siIter < vLidar.size(); ++siIter)
2151 {
2152 const LiDARHandler::PointRow& stPoint = vLidar[siIter];
2153 stdSS << (stPoint.dEasting - m_stOriginUTM.dEasting) << "," << (stPoint.dAltitude - m_stOriginUTM.dAltitude) << ","
2154 << (stPoint.dNorthing - m_stOriginUTM.dNorthing) << "," << stPoint.dTraversalScore;
2155 if (siIter < vLidar.size() - 1)
2156 stdSS << ",";
2157 }
2158 stdSS << "];\n";
2159
2160 // Path History Loop.
2161 {
2162 // Acquire lock for thread safety.
2163 std::lock_guard<std::mutex> lkPathLock(m_muPathMutex);
2164 stdSS << " const RAW_PATH = [";
2165
2166 // Loop through each point in the path history.
2167 for (size_t siIter = 0; siIter < m_vPathHistory.size(); ++siIter)
2168 {
2169 const DisplayPoint& stPoint = m_vPathHistory[siIter];
2170 stdSS << stPoint.fX << "," << stPoint.fY << "," << stPoint.fZ << "," << stPoint.nState;
2171 if (siIter < m_vPathHistory.size() - 1)
2172 stdSS << ",";
2173 }
2174 stdSS << "];\n";
2175 }
2176
2177 // Planned Path Loop.
2178 {
2179 // Acquire lock for thread safety.
2180 std::lock_guard<std::mutex> lkPlannedPathLock(m_muPlannedPathMutex);
2181 stdSS << " const RAW_PLANNED = [";
2182
2183 // Loop through each point in the planned path.
2184 for (size_t siIter = 0; siIter < m_vPlannedPath.size(); ++siIter)
2185 {
2186 const DisplayPoint& stPoint = m_vPlannedPath[siIter];
2187 stdSS << stPoint.fX << "," << stPoint.fY << "," << stPoint.fZ;
2188 if (siIter < m_vPlannedPath.size() - 1)
std::string Base64Encode(const std::vector< char > &vData)
Encodes binary data to a Base64 string.
Definition VisualizationHandler.cpp:1004
Here is the call graph for this function:
Here is the caller graph for this function:

◆ ThreadedContinuousCode()

void VisualizationHandler::ThreadedContinuousCode ( )
overrideprivatevirtual

The main continuous code for the VisualizationHandler.

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

Implements AutonomyThread< void >.

84{
85 // Check that required global handlers are valid.
86 if (globals::g_pStateMachineHandler == nullptr || globals::g_pNavigationBoard == nullptr)
87 {
88 return;
89 }
90
91 // Retrieve Rover UTM Position.
92 geoops::RoverPose stRoverPose = globals::g_pStateMachineHandler->SmartRetrieveRoverPose();
93 geoops::UTMCoordinate stRoverUTM = stRoverPose.GetUTMCoordinate();
94
95 // Set origin if not set.
96 if (!m_bOriginSet)
97 {
98 // Only set origin if we have a valid position.
99 if (std::abs(stRoverUTM.dEasting) > 1.0 || std::abs(stRoverUTM.dNorthing) > 1.0)
100 {
101 m_stOriginUTM = stRoverUTM;
102 m_bOriginSet = true;
103 LOG_DEBUG(logging::g_qSharedLogger, "VisualizationHandler: Origin set to E:{}, N:{}", m_stOriginUTM.dEasting, m_stOriginUTM.dNorthing);
104 LOG_INFO(logging::g_qSharedLogger, "VisualizationHandler: WebServer has been started on http://0.0.0.0:{}!", m_nPort);
105 }
106 }
107
108 // Update Data if origin is set.
109 if (m_bOriginSet)
110 {
111 this->UpdatePathHistory(stRoverUTM);
112 this->UpdateGoalBeacons(stRoverUTM);
113 this->UpdateDetections();
114
115 static std::chrono::system_clock::time_point tmLastAuxUpdate = std::chrono::system_clock::now();
116 // Update auxiliary data at 1 Hz.
117 if (std::chrono::duration_cast<std::chrono::seconds>(std::chrono::system_clock::now() - tmLastAuxUpdate).count() >= 1)
118 {
119 this->UpdatePlannedPath();
120 this->UpdateWaypoints();
121 tmLastAuxUpdate = std::chrono::system_clock::now();
122 }
123 }
124}
void UpdatePathHistory(const geoops::UTMCoordinate &stRoverUTM)
Update the path history with the current rover position.
Definition VisualizationHandler.cpp:233
void UpdateGoalBeacons(const geoops::UTMCoordinate &stRoverUTM)
Updates the persistent goal beacons for visualization.
Definition VisualizationHandler.cpp:476
void UpdateWaypoints()
Updates the waypoints from the waypoint handler.
Definition VisualizationHandler.cpp:311
void UpdatePlannedPath()
Updates the planned path from the waypoint handler.
Definition VisualizationHandler.cpp:274
void UpdateDetections()
Updates the persistent detections for visualization.
Definition VisualizationHandler.cpp:356
Here is the call graph for this function:

◆ PooledLinearCode()

void VisualizationHandler::PooledLinearCode ( )
overrideprivatevirtual

The pooled linear code for the VisualizationHandler. This is not used.

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

Implements AutonomyThread< void >.

133{}

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