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.
 
IPSGetIPS ()
 Accessor for the Frame I P S private member.
 

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 > 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
 
- 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 // Register Asset Endpoints.
42 m_pWebServer->RegisterEndpoint("/lib/three.js", std::bind(&VisualizationHandler::OnRequestLibThree, this, std::placeholders::_1));
43 m_pWebServer->RegisterEndpoint("/lib/orbit.js", std::bind(&VisualizationHandler::OnRequestLibOrbit, this, std::placeholders::_1));
44
45 // Register Data Endpoints.
46 m_pWebServer->SetHtmlContent(this->GetEmbeddedHtml());
47 m_pWebServer->RegisterEndpoint("/api/telemetry", std::bind(&VisualizationHandler::OnRequestTelemetry, this, std::placeholders::_1));
48 m_pWebServer->RegisterEndpoint("/api/map", std::bind(&VisualizationHandler::OnRequestMap, this, std::placeholders::_1));
49 m_pWebServer->RegisterEndpoint("/api/planned_path", std::bind(&VisualizationHandler::OnRequestPlannedPath, this, std::placeholders::_1));
50 m_pWebServer->RegisterEndpoint("/api/waypoints", std::bind(&VisualizationHandler::OnRequestWaypoints, this, std::placeholders::_1));
51 m_pWebServer->RegisterEndpoint("/api/detections", std::bind(&VisualizationHandler::OnRequestDetections, this, std::placeholders::_1));
52
53 // Set main thread's max iteration rate.
54 this->SetMainThreadIPSLimit(20); // 20 Hz
55}
void SetMainThreadIPSLimit(int nMaxIterationsPerSecond=0)
Mutator for the Main Thread Max I P S private member.
Definition AutonomyThread.hpp:467
std::vector< char > OnRequestMap(const std::string &szQuery)
Handles map data requests from the web server.
Definition VisualizationHandler.cpp:733
std::vector< char > OnRequestDetections(const std::string &szQuery)
Handles detection requests from the web server.
Definition VisualizationHandler.cpp:693
std::vector< char > OnRequestLibThree(const std::string &szQuery)
Serves the local three.min.js file.
Definition VisualizationHandler.cpp:844
std::string GetEmbeddedHtml()
Gets the embedded HTML for the visualization page.
Definition VisualizationHandler.cpp:967
std::vector< char > OnRequestPlannedPath(const std::string &szQuery)
Handles planned path requests from the web server.
Definition VisualizationHandler.cpp:598
std::vector< char > OnRequestWaypoints(const std::string &szQuery)
Handles waypoint requests from the web server.
Definition VisualizationHandler.cpp:635
std::vector< char > OnRequestTelemetry(const std::string &szQuery)
Handles telemetry requests from the web server.
Definition VisualizationHandler.cpp:521
std::vector< char > OnRequestLibOrbit(const std::string &szQuery)
Serves the local OrbitControls.js file.
Definition VisualizationHandler.cpp:859
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
65{
66 // Stop Web Server.
67 m_pWebServer.reset();
68}

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
138{
139 // Check that origin is set.
140 if (!m_bOriginSet)
141 {
142 LOG_WARNING(logging::g_qSharedLogger, "VisualizationHandler: Cannot save, origin not set.");
143 return;
144 }
145
146 // Submit logger message at start of saving process.
147 LOG_INFO(logging::g_qSharedLogger, "VisualizationHandler: Saving visualization to {}...", szFilename);
148
149 // Gather LiDAR Bounds based on Path History
150 double dMinE = 1e9, dMaxE = -1e9, dMinN = 1e9, dMaxN = -1e9;
151
152 {
153 // Acquire lock to read path history.
154 std::lock_guard<std::mutex> lkPathLock(m_muPathMutex);
155
156 // If no path history, use current rover position.
157 if (m_vPathHistory.empty())
158 {
159 geoops::RoverPose stPose = globals::g_pStateMachineHandler->SmartRetrieveRoverPose();
160 dMinE = dMaxE = stPose.GetUTMCoordinate().dEasting;
161 dMinN = dMaxN = stPose.GetUTMCoordinate().dNorthing;
162 }
163 else
164 {
165 // Loop through each point in the path history to find extremes.
166 for (const DisplayPoint& stPoint : m_vPathHistory)
167 {
168 double dAbsE = m_stOriginUTM.dEasting + stPoint.fX;
169 double dAbsN = m_stOriginUTM.dNorthing + stPoint.fZ;
170 if (dAbsE < dMinE)
171 dMinE = dAbsE;
172 if (dAbsE > dMaxE)
173 dMaxE = dAbsE;
174 if (dAbsN < dMinN)
175 dMinN = dAbsN;
176 if (dAbsN > dMaxN)
177 dMaxN = dAbsN;
178 }
179 }
180 }
181
182 // Calculate LiDAR filter parameters.
183 double dBuffer = 60.0;
184 double dCenterE = (dMinE + dMaxE) / 2.0;
185 double dCenterN = (dMinN + dMaxN) / 2.0;
186 double dRadius = std::max(dMaxE - dMinE, dMaxN - dMinN) / 2.0 + dBuffer;
187 std::vector<LiDARHandler::PointRow> vLidarPoints;
188 // Check that global LiDAR handler is valid.
189 if (globals::g_pLiDARHandler)
190 {
191 // Construct point filter.
193 stFilter.dEasting = dCenterE;
194 stFilter.dNorthing = dCenterN;
195 stFilter.dRadius = dRadius;
196 // Get LiDAR data from handler.
197 vLidarPoints = globals::g_pLiDARHandler->GetLiDARData(stFilter);
198 }
199
200 // Generate HTML content (including embedded JS).
201 std::string szHtml = GenerateStaticHtml(vLidarPoints);
202
203 std::ofstream stdOutFile(szFilename);
204 if (stdOutFile.is_open())
205 {
206 // Write file.
207 stdOutFile << szHtml;
208 stdOutFile.close();
209
210 // Submit logger message that we're done saving.
211 LOG_INFO(logging::g_qSharedLogger, "VisualizationHandler: Saved successfully.");
212 }
213 else
214 {
215 LOG_ERROR(logging::g_qSharedLogger, "VisualizationHandler: Failed to open file for writing.");
216 }
217}
std::vector< PointRow > GetLiDARData(const PointFilter &stPointFilter)
Retrieves LiDAR data points from the database based on the specified filter.
Definition LiDARHandler.cpp:155
geoops::RoverPose SmartRetrieveRoverPose(bool bVIOHeading=true, bool bVIOTracking=false)
Retrieve the rover's current position and heading. Automatically picks between getting the position/h...
Definition StateMachineHandler.cpp:445
std::string GenerateStaticHtml(const std::vector< LiDARHandler::PointRow > &vLidar)
Generates a static HTML page with embedded LiDAR data and embedded dependencies.
Definition VisualizationHandler.cpp:1707
Struct for filtering LiDAR points during queries.
Definition LiDARHandler.h:78
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
228{
229 // Acquire lock to update path history.
230 std::lock_guard<std::mutex> lkPathLock(m_muPathMutex);
231
232 // Add new point if moved more than 0.5 meters from last point.
233 static geoops::UTMCoordinate stLastPos = {};
234 if (std::hypot(stRoverUTM.dEasting - stLastPos.dEasting, stRoverUTM.dNorthing - stLastPos.dNorthing) > 0.5)
235 {
236 // Create new point, set its values, and append to history.
237 DisplayPoint stPoint;
238 stPoint.fX = static_cast<float>(stRoverUTM.dEasting - m_stOriginUTM.dEasting);
239 stPoint.fY = static_cast<float>(stRoverUTM.dAltitude - m_stOriginUTM.dAltitude);
240 stPoint.fZ = static_cast<float>(stRoverUTM.dNorthing - m_stOriginUTM.dNorthing);
241 stPoint.fScore = 0.0f;
242
243 // Check if our global state machine handler is valid to get current state.
244 if (globals::g_pStateMachineHandler)
245 {
246 // Set state.
247 stPoint.nState = static_cast<int>(globals::g_pStateMachineHandler->GetCurrentState());
248 }
249 else
250 {
251 // Default to Idle state.
252 stPoint.nState = 0;
253 }
254
255 // Append to history and update last position.
256 m_vPathHistory.push_back(stPoint);
257 stLastPos = stRoverUTM;
258 }
259}
statemachine::States GetCurrentState() const
Accessor for the Current State private member.
Definition StateMachineHandler.cpp:413
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
269{
270 // Check that global waypoint handler is valid.
271 if (globals::g_pWaypointHandler == nullptr)
272 {
273 return;
274 }
275
276 // Retrieve raw planned path that the GeoPlanner put in the waypoint handler for us.
277 // This may or may not exist or be the active path that the rover is following.
278 std::vector<geoops::Waypoint> vRawPath = globals::g_pWaypointHandler->RetrievePath("GeoPlannerPath");
279
280 // Acquire lock and update planned path.
281 std::lock_guard<std::mutex> lkPathLock(m_muPlannedPathMutex);
282 // Clear the old path and reserve space for the new path.
283 m_vPlannedPath.clear();
284 m_vPlannedPath.reserve(vRawPath.size());
285 // Loop through raw path and convert to DisplayPoint format.
286 for (const geoops::Waypoint& stWaypoint : vRawPath)
287 {
288 const geoops::UTMCoordinate& stUTMCoord = stWaypoint.GetUTMCoordinate();
289 DisplayPoint stPoint;
290 stPoint.fX = static_cast<float>(stUTMCoord.dEasting - m_stOriginUTM.dEasting);
291 stPoint.fY = static_cast<float>(stUTMCoord.dAltitude - m_stOriginUTM.dAltitude);
292 stPoint.fZ = static_cast<float>(stUTMCoord.dNorthing - m_stOriginUTM.dNorthing);
293 stPoint.fScore = 0.0f;
294 m_vPlannedPath.push_back(stPoint);
295 }
296}
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
306{
307 // Check that global waypoint handler is valid.
308 if (globals::g_pWaypointHandler == nullptr)
309 {
310 return;
311 }
312
313 // Retrieve all waypoints and obstacles from the WaypointHandler.
314 std::vector<geoops::Waypoint> vTargets = globals::g_pWaypointHandler->GetAllWaypoints();
315 std::vector<geoops::Waypoint> vObstacles = globals::g_pWaypointHandler->GetAllObstacles();
316
317 // Acquire a lock before updating waypoints list and clear existing waypoints.
318 std::lock_guard<std::mutex> lkWaypointLock(m_muWaypointMutex);
319
320 // Clear old waypoints from list.
321 m_vWaypoints.clear();
322 // Helper to process waypoints and obstacles.
323 std::function ProcessList = [&](const std::vector<geoops::Waypoint>& vWaypoints)
324 {
325 // Loop through all the given waypoints and add them to this classes waypoints list.
326 for (const geoops::Waypoint& stWaypoint : vWaypoints)
327 {
328 const geoops::UTMCoordinate& stUTMCoord = stWaypoint.GetUTMCoordinate();
329 DisplayWaypoint stPoint;
330 stPoint.fX = static_cast<float>(stUTMCoord.dEasting - m_stOriginUTM.dEasting);
331 stPoint.fY = static_cast<float>(stUTMCoord.dAltitude - m_stOriginUTM.dAltitude);
332 stPoint.fZ = static_cast<float>(stUTMCoord.dNorthing - m_stOriginUTM.dNorthing);
333 stPoint.nType = static_cast<int>(stWaypoint.eType);
334 m_vWaypoints.push_back(stPoint);
335 }
336 };
337
338 // Use our utility function to process and add the waypoints and objects from the WaypointHandler to the member variables.
339 ProcessList(vTargets);
340 ProcessList(vObstacles);
341}
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
464{
465 // Check that global multimedia board is valid.
466 if (globals::g_pMultimediaBoard == nullptr)
467 {
468 return;
469 }
470
471 // If we are in the ReachedGoal lighting state, add a persistent goal beacon at our current location.
472 if (globals::g_pMultimediaBoard->GetCurrentLightingState() == MultimediaBoard::MultimediaBoardLightingState::eReachedGoal)
473 {
474 // Acquire lock to update goal beacons.
475 std::lock_guard<std::mutex> lkBeaconLock(m_muGoalBeaconMutex);
476 // Calculate relative position.
477 float fCurX = static_cast<float>(stRoverUTM.dEasting - m_stOriginUTM.dEasting);
478 float fCurY = static_cast<float>(stRoverUTM.dAltitude - m_stOriginUTM.dAltitude);
479 float fCurZ = static_cast<float>(stRoverUTM.dNorthing - m_stOriginUTM.dNorthing);
480
481 // Check for deduplication (within 2 meters).
482 bool bAdd = true;
483 if (!m_vGoalBeacons.empty())
484 {
485 // Calculate distance to last added beacon.
486 const DisplayWaypoint& stLast = m_vGoalBeacons.back();
487 float fDistance = std::hypot(fCurX - stLast.fX, fCurZ - stLast.fZ);
488 // If the distance is less than 2 meters, do not add.
489 if (fDistance < 2.0f)
490 {
491 bAdd = false;
492 }
493 }
494
495 // Check if our last beacon is in the same location.
496 if (bAdd)
497 {
498 // Construct beacon.
499 DisplayWaypoint stWaypoint;
500 stWaypoint.fX = fCurX;
501 stWaypoint.fY = fCurY;
502 stWaypoint.fZ = fCurZ;
503 stWaypoint.nType = 8;
504 m_vGoalBeacons.push_back(stWaypoint);
505
506 // Submit logger message.
507 LOG_DEBUG(logging::g_qSharedLogger, "VisualizationHandler: Added persistent Goal Beacon at current location.");
508 }
509 }
510}
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
351{
352 // Check that required global handlers are valid.
353 if (globals::g_pTagDetectionHandler == nullptr || globals::g_pObjectDetectionHandler == nullptr)
354 {
355 return;
356 }
357
358 // Helper to process detections
359 std::function ProcessDetection = [&](float fX, float fY, float fZ, int nType)
360 {
361 // Acquire lock to update detections.
362 std::lock_guard<std::mutex> lkDetectionsLock(m_muDetectionMutex);
363
364 // Deduplication.
365 for (const DisplayDetection& stExistingDetection : m_vDetections)
366 {
367 // Same type check
368 if (stExistingDetection.nType == nType)
369 {
370 // Calculate the distance between existing detection and new detection.
371 float dist = std::hypot(stExistingDetection.fX - fX, stExistingDetection.fZ - fZ);
372 // If close to another detection of the same type, skip adding.
373 if (dist < 1.5f)
374 {
375 return;
376 }
377 }
378 }
379
380 // Construct a new detection and add it to our member list.
381 DisplayDetection stDetection;
382 stDetection.fX = fX;
383 stDetection.fY = fY;
384 stDetection.fZ = fZ;
385 stDetection.nType = nType;
386 m_vDetections.push_back(stDetection);
387 };
388
389 // Prepare Detector Vectors
390 std::vector<std::shared_ptr<TagDetector>> vTagDetectors = {globals::g_pTagDetectionHandler->GetTagDetector(TagDetectionHandler::TagDetectors::eHeadMainCam)};
391
392 std::vector<std::shared_ptr<ObjectDetector>> vObjDetectors = {
393 globals::g_pObjectDetectionHandler->GetObjectDetector(ObjectDetectionHandler::ObjectDetectors::eHeadMainCam)};
394
396 // Tags.
398
399 // Get the best Aruco and Torch tags.
400 tagdetectutils::ArucoTag stBestAruco;
401 tagdetectutils::ArucoTag stBestTorchTag;
402 statemachine::IdentifyTargetMarker(vTagDetectors, stBestAruco, stBestTorchTag);
403 // Process Aruco Tag if valid.
404 if (stBestAruco.nID != -1 && stBestAruco.stGeolocatedPosition.eType != geoops::WaypointType::eUNKNOWN)
405 {
406 geoops::UTMCoordinate stUTM = stBestAruco.stGeolocatedPosition.GetUTMCoordinate();
407 float fX = static_cast<float>(stUTM.dEasting - m_stOriginUTM.dEasting);
408 float fY = static_cast<float>(stUTM.dAltitude - m_stOriginUTM.dAltitude);
409 float fZ = static_cast<float>(stUTM.dNorthing - m_stOriginUTM.dNorthing);
410 ProcessDetection(fX, fY, fZ, 10);
411 }
412 // Process Torch Tag if valid.
413 if (stBestTorchTag.nID != -1 && stBestTorchTag.stGeolocatedPosition.eType != geoops::WaypointType::eUNKNOWN)
414 {
415 geoops::UTMCoordinate stUTM = stBestTorchTag.stGeolocatedPosition.GetUTMCoordinate();
416 float fX = static_cast<float>(stUTM.dEasting - m_stOriginUTM.dEasting);
417 float fY = static_cast<float>(stUTM.dAltitude - m_stOriginUTM.dAltitude);
418 float fZ = static_cast<float>(stUTM.dNorthing - m_stOriginUTM.dNorthing);
419 ProcessDetection(fX, fY, fZ, 10);
420 }
421
423 // Mallets, Bottles, Picks.
425
426 // Get the best detected object.
428 statemachine::IdentifyTargetObject(vObjDetectors, stObject);
429 // Process object if valid.
430 if (stObject.dConfidence > 0.6 && stObject.stGeolocatedPosition.eType != geoops::WaypointType::eUNKNOWN)
431 {
432 geoops::UTMCoordinate stUTM = stObject.stGeolocatedPosition.GetUTMCoordinate();
433 float fX = static_cast<float>(stUTM.dEasting - m_stOriginUTM.dEasting);
434 float fY = static_cast<float>(stUTM.dAltitude - m_stOriginUTM.dAltitude);
435 float fZ = static_cast<float>(stUTM.dNorthing - m_stOriginUTM.dNorthing);
436
437 // Determine type based on detection type.
438 int nType = 0;
439 switch (stObject.eDetectionType)
440 {
441 case objectdetectutils::ObjectDetectionType::eMallet: nType = 11; break;
442 case objectdetectutils::ObjectDetectionType::eWaterBottle: nType = 12; break;
443 case objectdetectutils::ObjectDetectionType::eRockPick: nType = 13; break;
444 default: break;
445 }
446
447 // If we have a type, process the detection.
448 if (nType != 0)
449 {
450 ProcessDetection(fX, fY, fZ, nType);
451 }
452 }
453}
std::shared_ptr< ObjectDetector > GetObjectDetector(ObjectDetectors eDetectorName)
Accessor for ObjectDetector detectors.
Definition ObjectDetectionHandler.cpp:127
std::shared_ptr< TagDetector > GetTagDetector(TagDetectors eDetectorName)
Accessor for TagDetector detectors.
Definition TagDetectionHandler.cpp:132
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:91
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:90
const geoops::UTMCoordinate & GetUTMCoordinate() const
Accessor for the geoops::UTMCoordinate member variable.
Definition GeospatialOperations.hpp:508
Represents a single detected object. Combines attributes from TorchObject and TensorflowObject struct...
Definition ObjectDetectionUtility.hpp:74
Represents a single ArUco tag. Combines attributes from TorchTag, TensorflowTag, and the original Aru...
Definition TagDetectionUtilty.hpp:59
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
522{
523 (void) szQuery;
524
525 // Acquire resource lock for reading the path.
526 std::lock_guard<std::mutex> lkPathLock(m_muPathMutex);
527
528 // Calculate buffer size.
529 // Header = Pose(3f) + Heading(1f) + LPower(1f) + RPower(1f) + Count(1u)
530 size_t siPathBytes = m_vPathHistory.size() * 4 * sizeof(float);
531 size_t siHeader = (6 * sizeof(float)) + (1 * sizeof(uint32_t));
532 std::vector<char> vBuffer;
533 vBuffer.reserve(siHeader + siPathBytes);
534
535 // Create a util function for adding a float and to the buffer.
536 std::function PushFloat = [&](float fFloat)
537 {
538 const char* pStart = reinterpret_cast<const char*>(&fFloat);
539 vBuffer.insert(vBuffer.end(), pStart, pStart + sizeof(float));
540 };
541 // Create a util function for adding a uint32_t to the buffer.
542 std::function PushUint = [&](uint32_t unInt32)
543 {
544 const char* pStart = reinterpret_cast<const char*>(&unInt32);
545 vBuffer.insert(vBuffer.end(), pStart, pStart + sizeof(uint32_t));
546 };
547
548 // Push Rover Pose.
549 geoops::RoverPose stPose = globals::g_pStateMachineHandler->SmartRetrieveRoverPose();
550 if (m_bOriginSet)
551 {
552 PushFloat(static_cast<float>(stPose.GetUTMCoordinate().dEasting - m_stOriginUTM.dEasting));
553 PushFloat(static_cast<float>(stPose.GetUTMCoordinate().dAltitude - m_stOriginUTM.dAltitude));
554 PushFloat(static_cast<float>(stPose.GetUTMCoordinate().dNorthing - m_stOriginUTM.dNorthing));
555 }
556 else
557 {
558 PushFloat(0.0f);
559 PushFloat(0.0f);
560 PushFloat(0.0f);
561 }
562
563 // Push Compass Heading.
564 PushFloat(static_cast<float>(stPose.GetCompassHeading()));
565
566 // Push Drive Powers.
567 diffdrive::DrivePowers stPowers = {0.0, 0.0};
568 if (globals::g_pDriveBoard)
569 {
570 stPowers = globals::g_pDriveBoard->GetDrivePowers();
571 }
572 PushFloat(static_cast<float>(stPowers.dLeftDrivePower));
573 PushFloat(static_cast<float>(stPowers.dRightDrivePower));
574
575 // Push Path History size.
576 PushUint(static_cast<uint32_t>(m_vPathHistory.size()));
577 // Push each point in the path history.
578 for (const auto& pt : m_vPathHistory)
579 {
580 PushFloat(pt.fX);
581 PushFloat(pt.fY);
582 PushFloat(pt.fZ);
583 PushFloat(static_cast<float>(pt.nState));
584 }
585
586 return vBuffer;
587}
diffdrive::DrivePowers GetDrivePowers() const
Accessor for the current drive powers of the robot.
Definition DriveBoard.cpp:295
::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
734{
735 // Declare instance variables with default values.
736 float fCenterX = 0.0f;
737 float fCenterY = 0.0f;
738 float fRadius = 50.0f;
739 float fMinScore = 0.0f;
740
741 // Helper to parse float values from query string.
742 std::function ParseVal = [&](std::string szKey) -> float
743 {
744 size_t siPos = szQuery.find(szKey + "=");
745 if (siPos != std::string::npos)
746 {
747 // Extract substring value.
748 size_t siEnd = szQuery.find("&", siPos);
749 std::string szSub = szQuery.substr(siPos + szKey.length() + 1, siEnd - (siPos + szKey.length() + 1));
750
751 // Convert to float.
752 try
753 {
754 return std::stof(szSub);
755 }
756 catch (...)
757 {
758 return 0.0f;
759 }
760 }
761
762 return 0.0f;
763 };
764
765 // Parse values from query string.
766 fCenterX = ParseVal("x");
767 fCenterY = ParseVal("y");
768 fRadius = ParseVal("r");
769 fMinScore = ParseVal("s");
770
771 // Clamp radius to minimum of 10 meters.
772 if (fRadius < 10.0f)
773 {
774 fRadius = 10.0f;
775 }
776
777 // Calculate absolute UTM coordinates for center point.
778 double dAbsE = m_stOriginUTM.dEasting + fCenterX;
779 double dAbsN = m_stOriginUTM.dNorthing + fCenterY;
780 double dSearchRadius = fRadius * 1.5;
781
782 // Construct LiDAR point filter.
784 stFilter.dEasting = dAbsE;
785 stFilter.dNorthing = dAbsN;
786 stFilter.dRadius = dSearchRadius;
787 // Retrieve LiDAR points from global LiDAR handler.
788 std::vector<LiDARHandler::PointRow> vPoints;
789 // Check that global LiDAR handler is valid.
790 if (globals::g_pLiDARHandler)
791 {
792 // Get LiDAR data from handler.
793 vPoints = globals::g_pLiDARHandler->GetLiDARData(stFilter);
794 }
795
796 // Prepare buffer.
797 std::vector<char> vBuffer;
798 size_t siBytes = sizeof(uint32_t) + (vPoints.size() * 4 * sizeof(float));
799 vBuffer.reserve(siBytes);
800 // Insert number of points placeholder.
801 uint32_t unPtCount = 0;
802 vBuffer.resize(sizeof(uint32_t));
803
804 // Loop through each point and add to buffer if within radius and above min score.
805 for (const LiDARHandler::PointRow& stPoint : vPoints)
806 {
807 // Check traversal score.
808 if (stPoint.dTraversalScore < fMinScore)
809 {
810 continue;
811 }
812
813 // Calculate relative coordinates.
814 float fRelX = static_cast<float>(stPoint.dEasting - m_stOriginUTM.dEasting);
815 float fRelZ = static_cast<float>(stPoint.dNorthing - m_stOriginUTM.dNorthing);
816 float fRelY = static_cast<float>(stPoint.dAltitude - m_stOriginUTM.dAltitude);
817
818 // Check if within radius.
819 if (std::abs(fRelX - fCenterX) <= fRadius && std::abs(fRelZ - fCenterY) <= fRadius)
820 {
821 unPtCount++;
822 const float aData[4] = {fRelX, fRelY, fRelZ, static_cast<float>(stPoint.dTraversalScore)};
823 const char* pStart = reinterpret_cast<const char*>(aData);
824 vBuffer.insert(vBuffer.end(), pStart, pStart + sizeof(aData));
825 }
826 }
827
828 // Write actual point count to the start of the buffer.
829 uint32_t* pHead = reinterpret_cast<uint32_t*>(vBuffer.data());
830 *pHead = unPtCount;
831
832 return vBuffer;
833}
Struct representing a single LiDAR point row from the database.
Definition LiDARHandler.h:53
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
599{
600 (void) szQuery;
601
602 // Acquire lock to read planned path.
603 std::lock_guard<std::mutex> lk(m_muPlannedPathMutex);
604
605 // Prepare buffer.
606 std::vector<char> vBuffer;
607 size_t siBytes = sizeof(uint32_t) + (m_vPlannedPath.size() * 3 * sizeof(float));
608 vBuffer.reserve(siBytes);
609
610 // Insert number of points.
611 uint32_t unCount = static_cast<uint32_t>(m_vPlannedPath.size());
612 const char* pCount = reinterpret_cast<const char*>(&unCount);
613 vBuffer.insert(vBuffer.end(), pCount, pCount + sizeof(uint32_t));
614
615 // Loop through each of the points in the planned path and add them to the buffer to be sent to the client.
616 for (const DisplayPoint& stPoint : m_vPlannedPath)
617 {
618 const float data[3] = {stPoint.fX, stPoint.fY, stPoint.fZ};
619 const char* pStart = reinterpret_cast<const char*>(data);
620 vBuffer.insert(vBuffer.end(), pStart, pStart + sizeof(data));
621 }
622
623 return vBuffer;
624}
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
636{
637 (void) szQuery;
638
639 // Acquire locks to read waypoints and goal beacons.
640 std::lock_guard<std::mutex> lkWaypointLock(m_muWaypointMutex);
641 std::lock_guard<std::mutex> lkBeaconLock(m_muGoalBeaconMutex);
642
643 // Get the total number of points.
644 size_t siTotalPoints = m_vWaypoints.size() + m_vGoalBeacons.size();
645
646 // Prepare buffer.
647 std::vector<char> vBuffer;
648 size_t siBytes = sizeof(uint32_t) + (siTotalPoints * (3 * sizeof(float) + sizeof(int)));
649 vBuffer.reserve(siBytes);
650 // Insert number of points.
651 uint32_t unCount = static_cast<uint32_t>(siTotalPoints);
652 const char* pStart = reinterpret_cast<const char*>(&unCount);
653 vBuffer.insert(vBuffer.end(), pStart, pStart + sizeof(uint32_t));
654
655 // Helper to push a DisplayWaypoint to the buffer.
656 std::function PushPoint = [&](const DisplayWaypoint& stWaypoint)
657 {
658 const char* pX = reinterpret_cast<const char*>(&stWaypoint.fX);
659 vBuffer.insert(vBuffer.end(), pX, pX + sizeof(float));
660
661 const char* pY = reinterpret_cast<const char*>(&stWaypoint.fY);
662 vBuffer.insert(vBuffer.end(), pY, pY + sizeof(float));
663
664 const char* pZ = reinterpret_cast<const char*>(&stWaypoint.fZ);
665 vBuffer.insert(vBuffer.end(), pZ, pZ + sizeof(float));
666
667 const char* pT = reinterpret_cast<const char*>(&stWaypoint.nType);
668 vBuffer.insert(vBuffer.end(), pT, pT + sizeof(int));
669 };
670
671 // Loop through each of the waypoints and goal beacons and add them to the buffer.
672 for (const DisplayWaypoint& stWaypoint : m_vWaypoints)
673 {
674 PushPoint(stWaypoint);
675 }
676 for (const DisplayWaypoint& stWaypoint : m_vGoalBeacons)
677 {
678 PushPoint(stWaypoint);
679 }
680
681 return vBuffer;
682}
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
694{
695 (void) szQuery;
696
697 // Acquire lock to read detections.
698 std::lock_guard<std::mutex> lkDetectionsLock(m_muDetectionMutex);
699
700 // Prepare buffer.
701 std::vector<char> vBuffer;
702 size_t siBytes = sizeof(uint32_t) + (m_vDetections.size() * (3 * sizeof(float) + sizeof(int)));
703 vBuffer.reserve(siBytes);
704 // Insert number of detections.
705 uint32_t unCount = static_cast<uint32_t>(m_vDetections.size());
706 const char* pStart = reinterpret_cast<const char*>(&unCount);
707 vBuffer.insert(vBuffer.end(), pStart, pStart + sizeof(uint32_t));
708
709 // Loop through each of the detections and add them to the buffer.
710 for (const DisplayDetection& stDetection : m_vDetections)
711 {
712 // Insert position data.
713 const float aData[3] = {stDetection.fX, stDetection.fY, stDetection.fZ};
714 const char* pStart = reinterpret_cast<const char*>(aData);
715 vBuffer.insert(vBuffer.end(), pStart, pStart + sizeof(aData));
716 // Insert type data.
717 const char* pTypeStart = reinterpret_cast<const char*>(&stDetection.nType);
718 vBuffer.insert(vBuffer.end(), pTypeStart, pTypeStart + sizeof(int));
719 }
720
721 return vBuffer;
722}
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
845{
846 (void) szQuery;
847 return LoadFileToBuffer(constants::VISUALIZER_THREEJS_PATH);
848}
std::vector< char > LoadFileToBuffer(const std::string &szPath)
Helper to load a file into a byte buffer.
Definition VisualizationHandler.cpp:874
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
860{
861 (void) szQuery;
862 return LoadFileToBuffer(constants::VISUALIZER_ORBITCONTROLS_PATH);
863}
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
875{
876 std::ifstream stdFile(szPath, std::ios::binary);
877 if (!stdFile.is_open())
878 {
879 LOG_ERROR(logging::g_qSharedLogger, "VisualizationHandler: Failed to load asset: {}", szPath);
880 return {};
881 }
882
883 return std::vector<char>((std::istreambuf_iterator<char>(stdFile)), std::istreambuf_iterator<char>());
884}
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
896{
897 // Create instance variables.
898 const std::string szBase64Chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
899 "abcdefghijklmnopqrstuvwxyz"
900 "0123456789+/";
901 std::string szReturnString;
902 int nIter = 0;
903 int nJter = 0;
904 unsigned char aCharArray3[3];
905 unsigned char aCharArray4[4];
906
907 // Loop through each character in the data.
908 for (char chCharacter : vData)
909 {
910 // Fill the 3-byte array.
911 aCharArray3[nIter++] = chCharacter;
912
913 // If we have 3 bytes, encode to 4 Base64 characters.
914 if (nIter == 3)
915 {
916 // Convert to Base64.
917 aCharArray4[0] = (aCharArray3[0] & 0xfc) >> 2;
918 aCharArray4[1] = ((aCharArray3[0] & 0x03) << 4) + ((aCharArray3[1] & 0xf0) >> 4);
919 aCharArray4[2] = ((aCharArray3[1] & 0x0f) << 2) + ((aCharArray3[2] & 0xc0) >> 6);
920 aCharArray4[3] = aCharArray3[2] & 0x3f;
921 // Append to return string.
922 for (nIter = 0; (nIter < 4); nIter++)
923 {
924 szReturnString += szBase64Chars[aCharArray4[nIter]];
925 }
926
927 nIter = 0;
928 }
929 }
930
931 // Handle padding for remaining bytes.
932 if (nIter)
933 {
934 // Fill remaining bytes with zeros.
935 for (nJter = nIter; nJter < 3; nJter++)
936 {
937 aCharArray3[nJter] = '\0';
938 }
939 // Convert to Base64.
940 aCharArray4[0] = (aCharArray3[0] & 0xfc) >> 2;
941 aCharArray4[1] = ((aCharArray3[0] & 0x03) << 4) + ((aCharArray3[1] & 0xf0) >> 4);
942 aCharArray4[2] = ((aCharArray3[1] & 0x0f) << 2) + ((aCharArray3[2] & 0xc0) >> 6);
943 aCharArray4[3] = aCharArray3[2] & 0x3f;
944 // Append to return string.
945 for (nJter = 0; (nJter < nIter + 1); nJter++)
946 {
947 szReturnString += szBase64Chars[aCharArray4[nJter]];
948 }
949 // Add padding '=' characters.
950 while ((nIter++ < 3))
951 {
952 szReturnString += '=';
953 }
954 }
955
956 return szReturnString;
957}
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
968{
969 return R"RAW_HTML(
970<!DOCTYPE html>
971<html>
972<head>
973 <title>MRDT 3D Visualizer</title>
974 <style>
975 body { margin: 0; overflow: hidden; background: #222; font-family: sans-serif; }
976 #ui-layer {
977 position: absolute;
978 top: 10px;
979 left: 10px;
980 color: #0f0;
981 background: rgba(0,0,0,0.5);
982 padding: 10px;
983 border-radius: 5px;
984 pointer-events: none;
985 min-width: 250px;
986 z-index: 10;
987 }
988 #eta-box {
989 position: absolute;
990 top: 10px;
991 right: 10px;
992 color: #0f0;
993 background: rgba(0,0,0,0.5);
994 padding: 10px;
995 border-radius: 5px;
996 font-size: 16px;
997 font-weight: bold;
998 pointer-events: none;
999 z-index: 10;
1000 }
1001 #marker-layer {
1002 position: absolute;
1003 top: 0; left: 0; width: 100%; height: 100%;
1004 pointer-events: none;
1005 overflow: hidden;
1006 }
1007 .hud-marker {
1008 position: absolute;
1009 padding: 4px 8px;
1010 background: rgba(0, 0, 0, 0.7);
1011 color: white;
1012 border: 2px solid white;
1013 border-radius: 4px;
1014 font-size: 14px;
1015 font-weight: bold;
1016 white-space: nowrap;
1017 transform: translate(-50%, -50%);
1018 transition: opacity 0.2s;
1019 }
1020 .hud-marker::after {
1021 content: '';
1022 position: absolute;
1023 top: 50%; left: 50%;
1024 width: 0; height: 0;
1025 }
1026 #legend-layer {
1027 position: absolute;
1028 bottom: 20px;
1029 left: 10px;
1030 color: #fff;
1031 background: rgba(0,0,0,0.7);
1032 padding: 10px;
1033 border-radius: 5px;
1034 pointer-events: none;
1035 min-width: 150px;
1036 z-index: 10;
1037 }
1038 .legend-section { margin-bottom: 10px; border-bottom: 1px solid #555; padding-bottom: 5px; }
1039 .legend-item { display: flex; align-items: center; gap: 10px; margin-bottom: 5px; font-size: 12px; }
1040 .color-box { width: 15px; height: 15px; border: 1px solid #aaa; }
1041 .circle-box { width: 12px; height: 12px; border-radius: 50%; }
1042 .control-group { margin-bottom: 10px; pointer-events: auto; }
1043 label { display: block; font-size: 12px; color: #aaa; }
1044 input[type=range] { width: 100%; }
1045 .val-disp { float: right; color: #fff; }
1046 .btn-group { position: absolute; bottom: 20px; right: 20px; display: flex; gap: 10px; z-index: 10; }
1047 .hud-btn { padding: 10px 20px; background: #444; color: white; border: 2px solid #666; cursor: pointer; font-size: 16px; z-index: 999; }
1048 .hud-btn.active { background: #00aa00; border-color: #00ff00; }
1049 .key { color: #fff; font-weight: bold; border: 1px solid #666; padding: 2px 5px; border-radius: 3px; background: #333; }
1050 h3 { margin-top: 0; border-bottom: 1px solid #555; padding-bottom: 5px; }
1051 </style>
1052 <script type="importmap">
1053 {
1054 "imports": {
1055 "three": "/lib/three.js",
1056 "three/addons/controls/OrbitControls.js": "/lib/orbit.js"
1057 }
1058 }
1059 </script>
1060</head>
1061<body>
1062 <div id="ui-layer">
1063 <h3>Settings</h3>
1064 <div class="control-group">
1065 <label>Load Radius (m) <span id="val-rad" class="val-disp">50</span></label>
1066 <input type="range" id="sl-rad" min="10" max="200" value="50" step="10">
1067 </div>
1068 <div class="control-group">
1069 <label>Border Tol. (m) <span id="val-tol" class="val-disp">10</span></label>
1070 <input type="range" id="sl-tol" min="5" max="50" value="10" step="1">
1071 </div>
1072 <div class="control-group">
1073 <label>Min Score <span id="val-score" class="val-disp">0.0</span></label>
1074 <input type="range" id="sl-score" min="0.0" max="1.0" value="0.0" step="0.05">
1075 </div>
1076 <div id="status" style="margin-top:10px; color: #fff;">Status: Free Cam</div>
1077 <div id="stats" style="margin-top:5px; color: #aaa; font-size:12px;">Points: 0</div>
1078 </div>
1079
1080 <div id="eta-box">ETA: Calculating...</div>
1081
1082 <div id="legend-layer">
1083 <div class="legend-section" id="det-legend">
1084 <strong>Detections</strong>
1085 </div>
1086 <div class="legend-section" id="state-legend">
1087 <strong>State Key</strong>
1088 </div>
1089 </div>
1090
1091 <div id="marker-layer"></div>
1092
1093 <div class="btn-group">
1094 <button id="snap-btn" class="hud-btn" onclick="snapToRover()">Snap (Space)</button>
1095 <button id="follow-btn" class="hud-btn" onclick="toggleFollow()">Follow (F)</button>
1096 </div>
1097
1098<script type="module">
1099 import * as THREE from 'three';
1100 import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
1101
1102 let camera, scene, renderer, controls, roverMesh, pathLine, plannedPathLine, currentPoints;
1103 let waypointGroup, detectionGroup;
1104 let markerLayer;
1105 let activeWaypoints = [];
1106 let leftArrow, rightArrow;
1107
1108 let mapCenter = { x: 0, y: 0 };
1109 let cfgRadius = 50;
1110 let cfgTolerance = 10;
1111 let cfgMinScore = 0.0;
1112 let isFetchingMap = false;
1113 let isFollowing = false;
1114 let targetPos = new THREE.Vector3();
1115 let targetHeading = 0.0;
1116 let prevRoverPos = new THREE.Vector3();
1117 const keys = { w:false, a:false, s:false, d:false, q:false, e:false, shift:false };
1118 let lastTime = performance.now();
1119
1120 // ETA Variables
1121 let lastTelemetryTime = 0;
1122 let lastTelemetryPos = new THREE.Vector3();
1123 let avgSpeed = 0.0;
1124 let pathDistance = 0.0;
1125 const speedHistory = [];
1126 let lastPathPoint = null;
1127
1128 const typeColors = {};
1129 const typeNames = {};
1130
1131 // State Colors
1132 const stateColors = {
1133 0: { name: "Idle", color: "#888888" },
1134 1: { name: "Navigating", color: "#00ffff" },
1135 2: { name: "Search Pattern", color: "#0000ff" },
1136 3: { name: "Approach Marker", color: "#ffffff" },
1137 4: { name: "Approach Object", color: "#ffaa00" },
1138 5: { name: "Verify Pos", color: "#00550e" },
1139 6: { name: "Verify Marker", color: "#06ac00" },
1140 7: { name: "Verify Object", color: "#78ff66" },
1141 8: { name: "Reversing", color: "#ff0000" },
1142 9: { name: "Stuck", color: "#330000" }
1143 };
1144
1145 // Detection Colors
1146 const detectColors = {
1147 10: { name: "Tag (Aruco)", color: "#aa00ff" }, // Purple
1148 11: { name: "Mallet", color: "#ffa500" }, // Orange
1149 12: { name: "Bottle", color: "#0088ff" }, // Blue
1150 13: { name: "Pick", color: "#ffee00" } // Yellow
1151 };
1152
1153 init();
1154 animate();
1155
1156 function init() {
1157 markerLayer = document.getElementById('marker-layer');
1158 scene = new THREE.Scene();
1159 scene.background = new THREE.Color(0x111111);
1160 scene.add(new THREE.GridHelper(100, 100));
1161 scene.add(new THREE.AxesHelper(2));
1162
1163 camera = new THREE.PerspectiveCamera(60, window.innerWidth/window.innerHeight, 0.1, 10000);
1164 camera.position.set(0, 10, -10);
1165
1166 renderer = new THREE.WebGLRenderer({ antialias: true });
1167 renderer.setSize(window.innerWidth, window.innerHeight);
1168 document.body.appendChild(renderer.domElement);
1169
1170 controls = new OrbitControls(camera, renderer.domElement);
1171 controls.enableDamping = true;
1172
1173 const geometry = new THREE.BoxGeometry(1, 0.5, 1.5);
1174 const material = new THREE.MeshBasicMaterial({ color: 0xff00ff, wireframe: true });
1175 roverMesh = new THREE.Mesh(geometry, material);
1176 scene.add(roverMesh);
1177
1178 // Drive Vectors (Arrows)
1179 const arrowDir = new THREE.Vector3(0, 0, -1);
1180 const arrowOrigin = new THREE.Vector3(0, 0, 0);
1181 const arrowLen = 1;
1182 const arrowCol = 0xffff00;
1183 leftArrow = new THREE.ArrowHelper(arrowDir, arrowOrigin, arrowLen, arrowCol);
1184 rightArrow = new THREE.ArrowHelper(arrowDir, arrowOrigin, arrowLen, arrowCol);
1185 roverMesh.add(leftArrow);
1186 roverMesh.add(rightArrow);
1187 leftArrow.position.set(-0.6, 0, 0);
1188 rightArrow.position.set(0.6, 0, 0);
1189
1190 waypointGroup = new THREE.Group();
1191 scene.add(waypointGroup);
1192
1193 detectionGroup = new THREE.Group(); // New Group for detections
1194 scene.add(detectionGroup);
1195
1196 const config = [
1197 { id: 0, name: "NAV", color: "#00ffff" },
1198 { id: 1, name: "TAG", color: "#ffffff" },
1199 { id: 2, name: "MALLET", color: "#ffa500" },
1200 { id: 3, name: "BOTTLE", color: "#0088ff" },
1201 { id: 4, name: "PICK", color: "#ffee00" },
1202 { id: 5, name: "OBJ", color: "#aaaaaa" },
1203 { id: 6, name: "OBSTACLE", color: "#ff0000" },
1204 { id: 7, name: "UNKNOWN", color: "#000000" },
1205 { id: 8, name: "GOAL REACHED", color: "#00ff00" }
1206 ];
1207
1208 config.forEach(c => {
1209 typeNames[c.id] = c.name;
1210 typeColors[c.id] = new THREE.Color(c.color);
1211 });
1212
1213 // Legend: Detections
1214 const detLegendDiv = document.getElementById('det-legend');
1215 for (const [id, data] of Object.entries(detectColors)) {
1216 const item = document.createElement('div');
1217 item.className = 'legend-item';
1218 item.innerHTML = `<div class="circle-box" style="background:${data.color}"></div><span>${data.name}</span>`;
1219 detLegendDiv.appendChild(item);
1220 }
1221
1222 // Legend: States
1223 const stateLegendDiv = document.getElementById('state-legend');
1224 for (const [id, data] of Object.entries(stateColors)) {
1225 const item = document.createElement('div');
1226 item.className = 'legend-item';
1227 item.innerHTML = `<div class="color-box" style="background:${data.color}"></div><span>${data.name}</span>`;
1228 stateLegendDiv.appendChild(item);
1229 }
1230
1231 document.getElementById('sl-rad').oninput = (e) => {
1232 cfgRadius = parseInt(e.target.value);
1233 document.getElementById('val-rad').innerText = cfgRadius;
1234 checkBoundary(true);
1235 };
1236 document.getElementById('sl-tol').oninput = (e) => {
1237 cfgTolerance = parseInt(e.target.value);
1238 document.getElementById('val-tol').innerText = cfgTolerance;
1239 };
1240 document.getElementById('sl-score').oninput = (e) => {
1241 cfgMinScore = parseFloat(e.target.value);
1242 document.getElementById('val-score').innerText = cfgMinScore.toFixed(2);
1243 checkBoundary(true);
1244 };
1245
1246 window.addEventListener('keydown', (e) => onKey(e, true));
1247 window.addEventListener('keyup', (e) => onKey(e, false));
1248 window.addEventListener('resize', onWindowResize);
1249
1250 window.toggleFollow = () => {
1251 isFollowing = !isFollowing;
1252 document.getElementById('follow-btn').classList.toggle('active', isFollowing);
1253 document.getElementById('status').innerText = isFollowing ? "Status: Locked" : "Status: Free Cam";
1254 if(isFollowing) controls.target.copy(roverMesh.position);
1255 };
1256 window.snapToRover = () => {
1257 camera.position.copy(roverMesh.position).add(new THREE.Vector3(0,10,-10));
1258 controls.target.copy(roverMesh.position);
1259 };
1260
1261 requestTelemetryLoop();
1262 setInterval(fetchPlannedPath, 2000);
1263 setInterval(fetchWaypoints, 2000);
1264 setInterval(fetchDetections, 1000); // Poll detections every second
1265 fetchMapSquare(0, 0);
1266 }
1267
1268 // --- RECURSIVE LOOP ---
1269 async function requestTelemetryLoop() {
1270 if (!document.hidden) await fetchTelemetry();
1271 setTimeout(requestTelemetryLoop, 50);
1272 }
1273
1274 async function fetchTelemetry() {
1275 try {
1276 const response = await fetch('/api/telemetry');
1277 if (!response.ok) return;
1278 const buffer = await response.arrayBuffer();
1279 updateTelemetry(buffer);
1280 } catch (e) { }
1281 }
1282
1283 async function fetchPlannedPath() {
1284 try {
1285 const response = await fetch('/api/planned_path');
1286 const buffer = await response.arrayBuffer();
1287 updatePlannedPath(buffer);
1288 } catch(e) {}
1289 }
1290
1291 async function fetchWaypoints() {
1292 try {
1293 const response = await fetch('/api/waypoints');
1294 const buffer = await response.arrayBuffer();
1295 updateWaypoints(buffer);
1296 } catch(e) {}
1297 }
1298
1299 async function fetchDetections() {
1300 try {
1301 const response = await fetch('/api/detections');
1302 const buffer = await response.arrayBuffer();
1303 updateDetections(buffer);
1304 } catch(e) {}
1305 }
1306
1307 function updateArrow(arrow, power) {
1308 const absPwr = Math.abs(power);
1309 const dir = power >= 0 ? new THREE.Vector3(0, 0, -1) : new THREE.Vector3(0, 0, 1);
1310 arrow.setDirection(dir);
1311 arrow.setLength(Math.max(absPwr * 2.0, 0.001), 0.2, 0.1);
1312 const col = power >= 0 ? 0x00ff00 : 0xff0000;
1313 arrow.setColor(col);
1314 }
1315
1316 function updateTelemetry(buffer) {
1317 const view = new DataView(buffer);
1318 const rx = view.getFloat32(0, true);
1319 const ry = view.getFloat32(4, true);
1320 const rz = view.getFloat32(8, true);
1321 const rh = view.getFloat32(12, true);
1322
1323 // Calculate Speed
1324 const now = performance.now();
1325 const newPos = new THREE.Vector3(rx, ry, -rz);
1326 if (lastTelemetryTime > 0) {
1327 const dt = (now - lastTelemetryTime) / 1000.0;
1328 if (dt > 0.1) {
1329 const dist = newPos.distanceTo(lastTelemetryPos);
1330 const instSpeed = dist / dt;
1331 speedHistory.push(instSpeed);
1332 if (speedHistory.length > 20) speedHistory.shift();
1333 avgSpeed = speedHistory.reduce((a,b)=>a+b, 0) / speedHistory.length;
1334 }
1335 }
1336 lastTelemetryPos.copy(newPos);
1337 lastTelemetryTime = now;
1338
1339 // Drive Powers
1340 const leftPwr = view.getFloat32(16, true);
1341 const rightPwr = view.getFloat32(20, true);
1342 updateArrow(leftArrow, leftPwr);
1343 updateArrow(rightArrow, rightPwr);
1344
1345 targetPos.set(rx, ry, -rz);
1346 targetHeading = -rh * (Math.PI / 180.0);
1347
1348 checkBoundary(false);
1349
1350 const pathCount = view.getUint32(24, true); // Offset 24
1351 if (pathCount > 0) {
1352 if (pathLine) scene.remove(pathLine);
1353 const floats = new Float32Array(buffer, 28, pathCount * 4); // Offset 28
1354 const vertices = [];
1355 const colors = [];
1356 const c = new THREE.Color();
1357
1358 for(let i=0; i<floats.length; i+=4) {
1359 vertices.push(floats[i], floats[i+1], -floats[i+2]);
1360 const state = Math.floor(floats[i+3]);
1361 const hex = stateColors[state] ? stateColors[state].color : "#ffffff";
1362 c.set(hex);
1363 colors.push(c.r, c.g, c.b);
1364 }
1365
1366 const pathGeo = new THREE.BufferGeometry();
1367 pathGeo.setAttribute('position', new THREE.Float32BufferAttribute(vertices, 3));
1368 pathGeo.setAttribute('color', new THREE.Float32BufferAttribute(colors, 3));
1369 const mat = new THREE.LineBasicMaterial({ vertexColors: true, linewidth: 3 });
1370 pathLine = new THREE.Line(pathGeo, mat);
1371 scene.add(pathLine);
1372 }
1373 }
1374
1375 function updatePlannedPath(buffer) {
1376 const view = new DataView(buffer);
1377 const count = view.getUint32(0, true);
1378 if (plannedPathLine) {
1379 scene.remove(plannedPathLine);
1380 plannedPathLine.geometry.dispose();
1381 plannedPathLine = null;
1382 }
1383
1384 pathDistance = 0.0;
1385 lastPathPoint = null;
1386
1387 if (count > 0) {
1388 const floats = new Float32Array(buffer, 4, count * 3);
1389 const vertices = [];
1390 for(let i=0; i<floats.length; i+=3) {
1391 vertices.push(floats[i], floats[i+1], -floats[i+2]);
1392 }
1393
1394 // Calculate total path distance (sum of segments)
1395 // Add distance from rover to first point
1396 if(vertices.length >= 3) {
1397 const firstPt = new THREE.Vector3(vertices[0], vertices[1], vertices[2]);
1398 pathDistance += targetPos.distanceTo(firstPt);
1399 // Store Last Point
1400 const lastIdx = vertices.length - 3;
1401 lastPathPoint = new THREE.Vector3(vertices[lastIdx], vertices[lastIdx+1], vertices[lastIdx+2]);
1402 }
1403 // Add segments
1404 for(let i=0; i<vertices.length-3; i+=3) {
1405 const p1 = new THREE.Vector3(vertices[i], vertices[i+1], vertices[i+2]);
1406 const p2 = new THREE.Vector3(vertices[i+3], vertices[i+4], vertices[i+5]);
1407 pathDistance += p1.distanceTo(p2);
1408 }
1409
1410 const geo = new THREE.BufferGeometry();
1411 geo.setAttribute('position', new THREE.Float32BufferAttribute(vertices, 3));
1412 plannedPathLine = new THREE.Line(geo, new THREE.LineBasicMaterial({ color: 0xeeff00, linewidth: 4 }));
1413 scene.add(plannedPathLine);
1414 }
1415 }
1416
1417 function updateWaypoints(buffer) {
1418 while(waypointGroup.children.length > 0){
1419 waypointGroup.remove(waypointGroup.children[0]);
1420 }
1421 markerLayer.innerHTML = '';
1422 activeWaypoints = [];
1423
1424 const view = new DataView(buffer);
1425 const count = view.getUint32(0, true);
1426 if(count === 0) return;
1427
1428 let offset = 4;
1429 const beaconGeo = new THREE.BoxGeometry(0.5, 10000, 0.5);
1430
1431 for(let i=0; i<count; i++) {
1432 const x = view.getFloat32(offset, true);
1433 const y = view.getFloat32(offset+4, true);
1434 const z = view.getFloat32(offset+8, true);
1435 const type = view.getInt32(offset+12, true);
1436 offset += 16;
1437
1438 const col = typeColors[type] || typeColors[7];
1439 const beaconMat = new THREE.MeshBasicMaterial({
1440 color: col,
1441 transparent: true,
1442 opacity: 0.3,
1443 depthTest: false
1444 });
1445 const beacon = new THREE.Mesh(beaconGeo, beaconMat);
1446 beacon.position.set(x, 0, -z);
1447 waypointGroup.add(beacon);
1448
1449 const div = document.createElement('div');
1450 div.className = 'hud-marker';
1451 div.innerText = typeNames[type] || "UNK";
1452 div.style.borderColor = "#" + col.getHexString();
1453 markerLayer.appendChild(div);
1454
1455 activeWaypoints.push({
1456 div: div,
1457 pos: new THREE.Vector3(x, 0, -z)
1458 });
1459 }
1460 }
1461
1462 function updateDetections(buffer) {
1463 while(detectionGroup.children.length > 0){
1464 detectionGroup.remove(detectionGroup.children[0]);
1465 }
1466
1467 const view = new DataView(buffer);
1468 const count = view.getUint32(0, true);
1469 if(count === 0) return;
1470
1471 let offset = 4;
1472 // Use a simple circle texture
1473 const canvas = document.createElement('canvas');
1474 canvas.width = 32; canvas.height = 32;
1475 const ctx = canvas.getContext('2d');
1476 ctx.beginPath();
1477 ctx.arc(16,16,14,0,2*Math.PI);
1478 ctx.fillStyle = 'white';
1479 ctx.fill();
1480 const tex = new THREE.CanvasTexture(canvas);
1481
1482 for(let i=0; i<count; i++) {
1483 const x = view.getFloat32(offset, true);
1484 const y = view.getFloat32(offset+4, true);
1485 const z = view.getFloat32(offset+8, true);
1486 const type = view.getInt32(offset+12, true);
1487 offset += 16;
1488
1489 let col = "#ffffff";
1490 if(detectColors[type]) col = detectColors[type].color;
1491
1492 const mat = new THREE.PointsMaterial({
1493 color: col,
1494 map: tex,
1495 size: 2.0, // Large persistent dot
1496 sizeAttenuation: true,
1497 alphaTest: 0.5,
1498 transparent: true
1499 });
1500 const geo = new THREE.BufferGeometry();
1501 geo.setAttribute('position', new THREE.Float32BufferAttribute([x, y, -z], 3));
1502
1503 const pt = new THREE.Points(geo, mat);
1504 detectionGroup.add(pt);
1505 }
1506 }
1507
1508 function updateHUD() {
1509 const width = window.innerWidth;
1510 const height = window.innerHeight;
1511 const pad = 30;
1512
1513 // Update ETA Box
1514 const etaBox = document.getElementById('eta-box');
1515
1516 // Reached End Logic
1517 let bReached = false;
1518 if (lastPathPoint && roverMesh.position.distanceTo(lastPathPoint) < 2.0) {
1519 bReached = true;
1520 }
1521
1522 if (bReached) {
1523 etaBox.innerText = "Status: Reached End of Path";
1524 etaBox.style.color = "#00ff00"; // Green
1525 } else {
1526 etaBox.style.color = "#0f0"; // Default Green
1527 if (avgSpeed < 0.05) {
1528 etaBox.innerText = "ETA: Stopped";
1529 } else {
1530 const timeSec = pathDistance / avgSpeed;
1531 if (!isFinite(timeSec) || timeSec < 0) {
1532 etaBox.innerText = "ETA: --:--";
1533 } else {
1534 const min = Math.floor(timeSec / 60);
1535 const sec = Math.floor(timeSec % 60);
1536 etaBox.innerText = `ETA: ${min}m ${sec}s (${avgSpeed.toFixed(2)} m/s)`;
1537 }
1538 }
1539 }
1540
1541 activeWaypoints.forEach(wp => {
1542 const target = wp.pos.clone();
1543 target.y = roverMesh.position.y + 3.0;
1544 target.project(camera);
1545
1546 let x = (target.x * .5 + .5) * width;
1547 let y = (target.y * -.5 + .5) * height;
1548
1549 const isBehind = target.z > 1;
1550
1551 if (isBehind) {
1552 x = width - x;
1553 y = height - y;
1554 }
1555
1556 const cx = width / 2;
1557 const cy = height / 2;
1558 const dx = x - cx;
1559 const dy = y - cy;
1560
1561 if (!isBehind && x >= pad && x <= width - pad && y >= pad && y <= height - pad) {
1562 y -= 40;
1563 wp.div.style.opacity = "0.9";
1564 } else {
1565 let t = Infinity;
1566 if (dx > 0) t = Math.min(t, (width - pad - cx) / dx);
1567 if (dx < 0) t = Math.min(t, (pad - cx) / dx);
1568 if (dy > 0) t = Math.min(t, (height - pad - cy) / dy);
1569 if (dy < 0) t = Math.min(t, (pad - cy) / dy);
1570
1571 x = cx + dx * t;
1572 y = cy + dy * t;
1573 wp.div.style.opacity = "0.6";
1574 }
1575
1576 wp.div.style.left = x + 'px';
1577 wp.div.style.top = y + 'px';
1578
1579 // Use 2D horizontal distance for distance display.
1580 const dxPos = roverMesh.position.x - wp.pos.x;
1581 const dzPos = roverMesh.position.z - wp.pos.z;
1582 const dist = Math.sqrt(dxPos*dxPos + dzPos*dzPos);
1583
1584 wp.div.innerText = `${wp.div.innerText.split(' ')[0]} ${Math.round(dist)}m`;
1585 });
1586 }
1587
1588 function checkBoundary(force) {
1589 if(isFetchingMap && !force) return;
1590 const roverX = targetPos.x;
1591 const roverN = -targetPos.z;
1592 const distE = Math.abs(roverX - mapCenter.x);
1593 const distN = Math.abs(roverN - mapCenter.y);
1594 const limit = cfgRadius - cfgTolerance;
1595
1596 if (force || distE > limit || distN > limit) {
1597 fetchMapSquare(roverX, roverN);
1598 }
1599 }
1600
1601 async function fetchMapSquare(x, y) {
1602 if(isFetchingMap) return;
1603 isFetchingMap = true;
1604 try {
1605 const url = `/api/map?x=${x.toFixed(2)}&y=${y.toFixed(2)}&r=${cfgRadius}&s=${cfgMinScore}`;
1606 const response = await fetch(url);
1607 const buffer = await response.arrayBuffer();
1608 loadMapPoints(buffer);
1609 mapCenter = { x: x, y: y };
1610 } catch(e) { console.error(e); }
1611 isFetchingMap = false;
1612 }
1613
1614 function loadMapPoints(buffer) {
1615 const view = new DataView(buffer);
1616 const count = view.getUint32(0, true);
1617
1618 if(currentPoints) {
1619 scene.remove(currentPoints);
1620 currentPoints.geometry.dispose();
1621 currentPoints.material.dispose();
1622 currentPoints = null;
1623 }
1624
1625 if(count === 0) {
1626 document.getElementById('stats').innerText = "Points: 0";
1627 return;
1628 }
1629
1630 const pts = [];
1631 const colors = [];
1632 const c = new THREE.Color();
1633 const floats = new Float32Array(buffer, 4, count * 4);
1634
1635 for(let i=0; i<floats.length; i+=4) {
1636 pts.push(floats[i], floats[i+1], -floats[i+2]);
1637 c.setHSL(floats[i+3] * 0.33, 1.0, 0.5);
1638 colors.push(c.r, c.g, c.b);
1639 }
1640
1641 const geo = new THREE.BufferGeometry();
1642 geo.setAttribute('position', new THREE.Float32BufferAttribute(pts, 3));
1643 geo.setAttribute('color', new THREE.Float32BufferAttribute(colors, 3));
1644 const mat = new THREE.PointsMaterial({ size: 0.5, vertexColors: true });
1645
1646 currentPoints = new THREE.Points(geo, mat);
1647 scene.add(currentPoints);
1648 document.getElementById('stats').innerText = "Points: " + count;
1649 }
1650
1651 function onKey(e, p) {
1652 if(keys.hasOwnProperty(e.key.toLowerCase())) keys[e.key.toLowerCase()] = p;
1653 if(e.key === 'Shift') keys.shift = p;
1654 if(p && e.key==='f') window.toggleFollow();
1655 if(p && e.key===' ') window.snapToRover();
1656 }
1657 function onWindowResize() { camera.aspect = window.innerWidth / window.innerHeight; camera.updateProjectionMatrix(); renderer.setSize(window.innerWidth, window.innerHeight); }
1658
1659 function animate() {
1660 requestAnimationFrame(animate);
1661 const now = performance.now();
1662 const dt = (now - lastTime)/1000;
1663 lastTime = now;
1664
1665 prevRoverPos.copy(roverMesh.position);
1666 const lerpFactor = 5.0 * dt;
1667 roverMesh.position.lerp(targetPos, lerpFactor);
1668 const dRot = targetHeading - roverMesh.rotation.y;
1669 roverMesh.rotation.y += Math.atan2(Math.sin(dRot), Math.cos(dRot)) * lerpFactor;
1670
1671 if(isFollowing) {
1672 camera.position.add(new THREE.Vector3().subVectors(roverMesh.position, prevRoverPos));
1673 controls.target.copy(roverMesh.position);
1674 } else {
1675 const spd = (keys.shift?15:5)*dt;
1676 const fwd = new THREE.Vector3(); camera.getWorldDirection(fwd); fwd.y=0; fwd.normalize();
1677 const rgt = new THREE.Vector3().crossVectors(fwd, camera.up).normalize();
1678 if(keys.w) camera.position.addScaledVector(fwd, spd);
1679 if(keys.s) camera.position.addScaledVector(fwd, -spd);
1680 if(keys.d) camera.position.addScaledVector(rgt, spd);
1681 if(keys.a) camera.position.addScaledVector(rgt, -spd);
1682 if(keys.q) camera.position.y += spd;
1683 if(keys.e) camera.position.y -= spd;
1684 controls.target.add(new THREE.Vector3(0,0,0).addScaledVector(fwd, (keys.w-keys.s)*spd).addScaledVector(rgt, (keys.d-keys.a)*spd));
1685 }
1686 controls.update();
1687
1688 updateHUD();
1689
1690 renderer.render(scene, camera);
1691 }
1692</script>
1693</body>
1694</html>
1695 )RAW_HTML";
1696}
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
1708{
1709 // 1. Load and Encode Assets
1710 std::string szThreeJS = Base64Encode(LoadFileToBuffer(constants::VISUALIZER_THREEJS_PATH));
1711 std::string szOrbitJS = Base64Encode(LoadFileToBuffer(constants::VISUALIZER_ORBITCONTROLS_PATH));
1712
1713 if (szThreeJS.empty() || szOrbitJS.empty())
1714 {
1715 LOG_ERROR(logging::g_qSharedLogger, "VisualizationHandler: Cannot generate monolithic export. Missing assets.");
1716 return "<html><body>Error: Missing ThreeJS assets on rover. Ensure 'assets/three.module.js' and 'assets/OrbitControls.js' exist.</body></html>";
1717 }
1718
1719 std::stringstream stdSS;
1720 stdSS << std::fixed << std::setprecision(3);
1721
1722 // Write Header
1723 stdSS << R"RAW(
1724<!DOCTYPE html>
1725<html>
1726<head>
1727 <title>Mars Rover Static Export</title>
1728 <style>
1729 body { margin: 0; overflow: hidden; background: #222; font-family: sans-serif; }
1730 #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; }
1731 #marker-layer { position: absolute; top: 0; left: 0; width: 100%; height: 100%; pointer-events: none; overflow: hidden; }
1732 .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; }
1733 .hud-marker::after { content: ''; position: absolute; top: 50%; left: 50%; width: 0; height: 0; }
1734 #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; }
1735 .legend-section { margin-bottom: 10px; border-bottom: 1px solid #555; padding-bottom: 5px; }
1736 .legend-item { display: flex; align-items: center; gap: 10px; margin-bottom: 5px; font-size: 12px; }
1737 .color-box { width: 15px; height: 15px; border: 1px solid #aaa; }
1738 .circle-box { width: 12px; height: 12px; border-radius: 50%; }
1739 .control-group { margin-bottom: 10px; pointer-events: auto; }
1740 label { display: block; font-size: 12px; color: #aaa; }
1741 input[type=range] { width: 100%; }
1742 .val-disp { float: right; color: #fff; }
1743 .btn-group { position: absolute; bottom: 20px; right: 20px; display: flex; gap: 10px; z-index: 10; }
1744 .hud-btn { padding: 10px 20px; background: #444; color: white; border: 2px solid #666; cursor: pointer; font-size: 16px; z-index: 999; }
1745 .hud-btn.active { background: #00aa00; border-color: #00ff00; }
1746 h3 { margin-top: 0; border-bottom: 1px solid #555; padding-bottom: 5px; }
1747 </style>
1748 <script type="importmap">
1749 {
1750 "imports": {
1751 "three": "data:text/javascript;base64,)RAW";
1752
1753 // Inject ThreeJS Base64
1754 stdSS << szThreeJS;
1755
1756 stdSS << R"RAW(",
1757 "three/addons/controls/OrbitControls.js": "data:text/javascript;base64,)RAW";
1758
1759 // Inject OrbitControls Base64
1760 stdSS << szOrbitJS;
1761
1762 stdSS << R"RAW("
1763 }
1764 }
1765 </script>
1766</head>
1767<body>
1768 <div id="ui-layer">
1769 <h3>Static Export</h3>
1770 <div class="control-group">
1771 <label>Min Score <span id="val-score" class="val-disp">0.0</span></label>
1772 <input type="range" id="sl-score" min="0.0" max="1.0" value="0.0" step="0.05">
1773 </div>
1774 <div id="stats" style="margin-top:5px; color: #aaa; font-size:12px;">Points: 0</div>
1775 </div>
1776 <div id="legend-layer">
1777 <div class="legend-section" id="det-legend"><strong>Detections</strong></div>
1778 <div class="legend-section" id="state-legend"><strong>State Key</strong></div>
1779 </div>
1780 <div id="marker-layer"></div>
1781 <div class="btn-group">
1782 <button class="hud-btn" onclick="snapToRover()">Snap (Space)</button>
1783 </div>
1784)RAW";
1785
1786 stdSS << "<script>\n";
1787
1788 // LiDAR Data Loop.
1789 stdSS << " const RAW_LIDAR = [";
1790 for (size_t siIter = 0; siIter < vLidar.size(); ++siIter)
1791 {
1792 const LiDARHandler::PointRow& stPoint = vLidar[siIter];
1793 stdSS << (stPoint.dEasting - m_stOriginUTM.dEasting) << "," << (stPoint.dAltitude - m_stOriginUTM.dAltitude) << ","
1794 << (stPoint.dNorthing - m_stOriginUTM.dNorthing) << "," << stPoint.dTraversalScore;
1795 if (siIter < vLidar.size() - 1)
1796 stdSS << ",";
1797 }
1798 stdSS << "];\n";
1799
1800 // Path History Loop.
1801 {
1802 // Acquire lock for thread safety.
1803 std::lock_guard<std::mutex> lkPathLock(m_muPathMutex);
1804 stdSS << " const RAW_PATH = [";
1805
1806 // Loop through each point in the path history.
1807 for (size_t siIter = 0; siIter < m_vPathHistory.size(); ++siIter)
1808 {
1809 const DisplayPoint& stPoint = m_vPathHistory[siIter];
1810 stdSS << stPoint.fX << "," << stPoint.fY << "," << stPoint.fZ << "," << stPoint.nState;
1811 if (siIter < m_vPathHistory.size() - 1)
1812 stdSS << ",";
1813 }
1814 stdSS << "];\n";
1815 }
1816
1817 // Planned Path Loop.
1818 {
1819 // Acquire lock for thread safety.
1820 std::lock_guard<std::mutex> lkPlannedPathLock(m_muPlannedPathMutex);
1821 stdSS << " const RAW_PLANNED = [";
1822
1823 // Loop through each point in the planned path.
1824 for (size_t siIter = 0; siIter < m_vPlannedPath.size(); ++siIter)
1825 {
1826 const DisplayPoint& stPoint = m_vPlannedPath[siIter];
1827 stdSS << stPoint.fX << "," << stPoint.fY << "," << stPoint.fZ;
1828 if (siIter < m_vPlannedPath.size() - 1)
1829 stdSS << ",";
1830 }
1831 stdSS << "];\n";
1832 }
1833
1834 // Waypoints Loop.
1835 {
std::string Base64Encode(const std::vector< char > &vData)
Encodes binary data to a Base64 string.
Definition VisualizationHandler.cpp:895
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 >.

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

127{}

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