diff --git a/apps/roadrunner-view/public/about/About.md b/apps/roadrunner-view/public/about/About.md index 929309d..69a75a5 100644 --- a/apps/roadrunner-view/public/about/About.md +++ b/apps/roadrunner-view/public/about/About.md @@ -1,14 +1,22 @@ -Roadrunner is a portfolio vehicle-simulation application built to demonstrate full-stack, cloud, mapping, and distributed-system development. This project was started in part to gain real life experience with Kafka and Redis. +About Roadrunner +================ -The application uses Google sign-in and Amazon Cognito for authentication. When you sign in, Roadrunner may receive basic account information such as your name, email address, and user identifier. Roadrunner does not receive or store your Google or Cognito password. +Roadrunner is a full-stack, cloud-native vehicle simulation application built to demonstrate real-time telemetry streaming, interactive 2D/3D geospatial visualization, and distributed systems architecture. -Roadrunner may use cookies, browser storage, or authentication tokens to maintain login sessions and secure API requests. Third-party providers such as Google, Amazon Cognito, AWS, and Mapbox may process limited technical data needed to provide authentication, hosting, mapping, routing, logging, or API functionality. +Architecture Overview +--------------------- +- **Backend Engine** (`apps/roadrunner`): Java 17 / Spring Boot service performing vehicle trajectory generation, route interpolation, REST control plane, and real-time Kafka & Redis telemetry streaming. +- **Frontend Viewer** (`apps/roadrunner-view`): React / TypeScript application with dual Mapbox GL JS & Google Maps 3D WebGL rendering, 1st-person driver view perspective, and contextual help documentation. +- **Infrastructure & Cloud** (`orchestration/`): Terraform automation, AWS Aurora Serverless PostGIS, S3 + CloudFront CDN, Amazon Cognito auth, and Kubernetes / Minikube manifests. -Roadrunner may collect limited technical logs, API request information, and simulation data for debugging, monitoring, security, and demonstration purposes. It is not intended to collect sensitive personal information, and you should not enter confidential or sensitive data into the application. +GitHub Repository +----------------- +The project is consolidated in a single monorepo on GitHub: +- [Roadrunner Monorepo](https://github.com/SteveTarter/roadrunner) -Roadrunner is a portfolio/demo project, not a production transportation, dispatch, navigation, or safety system. It may change over time, contain bugs, or experience downtime. Roadrunner does not sell user data. - -Roadrunner's code is hosted on Github in three repos: -- [Roadrunner](https://github.com/SteveTarter/roadrunner): Spring Boot-based Backend -- [Roadrunner Viewer](https://github.com/SteveTarter/roadrunner-view): React-based viewer -- [Roadrunner K8S Orchestration](https://github.com/SteveTarter/roadrunner-k8s-orchestration): Install to minikube or AWS kubernetes cluster \ No newline at end of file +Privacy Notice & Disclaimer +--------------------------- +- **Authentication**: Roadrunner uses Google Sign-In and Amazon Cognito for authentication. When you sign in, Roadrunner receives basic account information (name, email address, user ID). Passwords are managed directly by Cognito/Google and are never received or stored by Roadrunner. +- **Cookies & Storage**: Tokens, cookies, or browser storage are used solely to maintain authenticated sessions and secure API requests. +- **Data & Logging**: Limited technical logs and simulation telemetry data are collected for monitoring, security, and demo purposes. Do not submit sensitive personal information. +- **Disclaimer**: Roadrunner is a portfolio demonstration application, not a production transportation, dispatch, navigation, or safety system. \ No newline at end of file diff --git a/apps/roadrunner-view/public/guide/ActiveVehiclePlot.md b/apps/roadrunner-view/public/guide/ActiveVehiclePlot.md index 2092555..cfc86b7 100644 --- a/apps/roadrunner-view/public/guide/ActiveVehiclePlot.md +++ b/apps/roadrunner-view/public/guide/ActiveVehiclePlot.md @@ -4,10 +4,10 @@ When invoked from the Home page, the plot opens with the plot zoomed out to show Below is a typical view after invoking from the Home Page: -![Active Vehicle Plot Unzoomed](./images/VehiclePlot-7Day.png) +![Active Vehicle Plot Unzoomed](/guide/images/VehiclePlot-7Day.png) You can zoom in on a particular run using the mouse wheel, or by using pinch gestures. Zooming in allows easier selection of the specific time desired. When the chart is clicked or double tapped, the playback time is set to that time. Below shows the plot zoomed in: -![Active Vehicle Plot Zoomed](./images/VehiclePlot-Zoomed.png) +![Active Vehicle Plot Zoomed](/guide/images/VehiclePlot-Zoomed.png) When accessed from the Driver View, the Active Vehicle Plot zooms in to the time period that the selected vehicle ran. This enables jumping back or forward in that vehicle's life, allowing "getting to the good part" easier. Since criss-cross patterns are often centered on "interesting" places, this is useful. Below is a typical Active Vehicle Plot accessed from the Driver Page. The red vertical line shows the current playback time. -![Active Vehicle Plot Driver View](./images/VehiclePlot-DriverView.png) +![Active Vehicle Plot Driver View](/guide/images/VehiclePlot-DriverView.png) diff --git a/apps/roadrunner-view/public/guide/BookmarksPanel.md b/apps/roadrunner-view/public/guide/BookmarksPanel.md index 32e77c6..b472f51 100644 --- a/apps/roadrunner-view/public/guide/BookmarksPanel.md +++ b/apps/roadrunner-view/public/guide/BookmarksPanel.md @@ -1,2 +1,2 @@ The Bookmarks Panel displays a curated list of past simulation sessions, with a description of what you are about to get into. The list is curated by the administrator. Below is typical content: -![Bookmarks Panel](./images/BookmarksPanel.png) +![Bookmarks Panel](/guide/images/BookmarksPanel.png) diff --git a/apps/roadrunner-view/public/guide/CreateCrissCrossPanel.md b/apps/roadrunner-view/public/guide/CreateCrissCrossPanel.md index 059522c..6b8c774 100644 --- a/apps/roadrunner-view/public/guide/CreateCrissCrossPanel.md +++ b/apps/roadrunner-view/public/guide/CreateCrissCrossPanel.md @@ -1,7 +1,7 @@ _Note: The following feature require your user account to be assigned to the 'creator' group in Amazon Cognito. This access must be requested from the administrator. Users in this group may create up to 30 sessions a day._ The Create Criss-Cross Panel allows a 'creator' user to start a number of vehicles at once. The user specifies a center point by specifying an address or clicking on the map, the number of vehicles to start, and the radius of the circle. Once the center has been selected, a circle appears on the map showing the selected radius. Along the circle are dots that show where a vehicle will be created. -![Create Criss-Cross Panel Select](./images/CrissCross-Select.png) +![Create Criss-Cross Panel Select](/guide/images/CrissCross-Select.png) When the "Generate" button is pressed, vehicles will be created in the requested pattern. Each vehicle is given a route that takes it from its starting point, through the center point, and ending at the point on the opposite side of the circle. -![Create Criss-Cross Panel Generate](./images/CrissCross-Generate.png) +![Create Criss-Cross Panel Generate](/guide/images/CrissCross-Generate.png) Pro Tip: Press the Generate button multiple times so you can watch a vehicle travel the route just in front of you. \ No newline at end of file diff --git a/apps/roadrunner-view/public/guide/CreateVehiclePanel.md b/apps/roadrunner-view/public/guide/CreateVehiclePanel.md index ef99965..056febf 100644 --- a/apps/roadrunner-view/public/guide/CreateVehiclePanel.md +++ b/apps/roadrunner-view/public/guide/CreateVehiclePanel.md @@ -6,4 +6,4 @@ If specifying a point by address, autocomplete is used to aid choosing the point Below is the Create Vehicle Panel: -![Create Vehicle Panel](./images/CreateVehiclePanel.png) +![Create Vehicle Panel](/guide/images/CreateVehiclePanel.png) diff --git a/apps/roadrunner-view/public/guide/SimTable.md b/apps/roadrunner-view/public/guide/SimTable.md index a7a9f52..c43f300 100644 --- a/apps/roadrunner-view/public/guide/SimTable.md +++ b/apps/roadrunner-view/public/guide/SimTable.md @@ -3,4 +3,4 @@ Sim Table Panel The Sim Table Panel provides a listing of all vehicle simulations that have occurred during the retention period. For each simulation session, the vehicle ID, initiating user, and start time are listed. A playback button is provided to allow starting playback of that simulation session. The table has controls that allow paging through the sessions. There is also a button to set the playback time to "Now". A typical Sim Table Panel is shown below: -![Sim Table Panel](./images/SimTable.png) +![Sim Table Panel](/guide/images/SimTable.png) diff --git a/apps/roadrunner-view/public/guide/UserGuide.md b/apps/roadrunner-view/public/guide/UserGuide.md index 9be427f..e7363d3 100644 --- a/apps/roadrunner-view/public/guide/UserGuide.md +++ b/apps/roadrunner-view/public/guide/UserGuide.md @@ -3,20 +3,24 @@ Roadrunner: How to Use the Map Interface Welcome to the Roadrunner vehicle-simulation viewer! The application provides a real-time, interactive map to monitor and manage simulated vehicle fleets. Below is a guide to the tools and panels available on the Home Page. +Contextual Help +--------------- +Look for the **`?` Help Icon** in panel header bars (such as the Simulation Table, Active Vehicle Plot, Bookmarks, and Creator panels). Clicking any help icon opens contextual documentation directly over the map without interrupting your active simulation playback. + Map View Controls ----------------- Pinned to the right side of the screen, you will find a floating toolbar to customize your map view. -* ![Fit All Button](./images/FitAll.png) Click this button to automatically adjust the map's zoom and pan to fit all currently active vehicles within your viewing area. +* ![Fit All Button](/guide/images/FitAll.png) Click this button to automatically adjust the map's zoom and pan to fit all currently active vehicles within your viewing area. -* ![Street/Satellite Button](./images/Satellite.png) Use this button to seamlessly switch the map background between a standard street layout and a satellite imagery view. +* ![Street/Satellite Button](/guide/images/Satellite.png) Use this button to seamlessly switch the map background between a standard street layout and a satellite imagery view. -* ![Show All Routes Button](./images/ShowAllRoutes.png) ![Hide All Routes Button](./images/HideAllRoutes.png)These buttons allow you to globally toggle the visibility of the route lines drawn behind the vehicles. _(Note: You can also click directly near an individual vehicle on the map to toggle its specific route and popup.)_ +* ![Show All Routes Button](/guide/images/ShowAllRoutes.png) ![Hide All Routes Button](/guide/images/HideAllRoutes.png) These buttons allow you to globally toggle the visibility of the route lines drawn behind the vehicles. _(Note: You can also click directly near an individual vehicle on the map to toggle its specific route and popup.)_ -* ![Interpolation Button](./images/Interpolation.png) This tool toggles vehicle movement interpolation on and off. Enable smoothing for fluid vehicle animations, or disable it to see the raw, un-interpolated data points coming from the backend. +* ![Interpolation Button](/guide/images/Interpolation.png) This tool toggles vehicle movement interpolation on and off. Enable smoothing for fluid vehicle animations, or disable it to see the raw, un-interpolated data points coming from the backend. -* ![Active Vehicle Chart Button](./images/VehicleChart.png) Clicking this opens a detailed data chart/plot for active vehicles directly over the map view. +* ![Active Vehicle Chart Button](/guide/images/VehicleChart.png) Clicking this opens a detailed data chart/plot for active vehicles directly over the map view. Application Menu (Manage Panels) @@ -26,17 +30,18 @@ The top navigation bar contains additional management panels for running and mon ### Standard Features -* **Bookmarks:** Opens a panel where you can select from a curated list of previously saved simulation sessions. Selecting a bookmark will adjust the application clock and transport you to the Driver View for that specific vehicle's historical run. [Learn more about the Bookmarks panel.](/guide/bookmarks) +* **Bookmarks:** Opens a panel where you can select from a curated list of previously saved simulation sessions. Selecting a bookmark will adjust the application clock and transport you to the Driver View for that specific vehicle's historical run. -* **Sim Table:** Opens a tabular layout displaying simulation sessions that have occured within the retention period. [Learn more about the Sim Table.](/guide/sim-table) +* **Sim Table:** Opens a tabular layout displaying simulation sessions that have occurred within the retention period. -* **Active Vehicle Plot:** Opens a chart displaying vehicle activity over the retention period. Allows an alternate way to select a timeline to view past or current simulations. [Learn more about the Active Vehicle Plot.](/guide/active-vehicle-plot) +* **Active Vehicle Plot:** Opens a chart displaying vehicle activity over the retention period. Allows an alternate way to select a timeline to view past or current simulations. ### Creator Features _Note: The following features require your user account to be assigned to the 'creator' group in Amazon Cognito._ -* **Create Vehicle:** Opens the creation panel, allowing you to establish a starting point, destination, and parameters for a new vehicle simulation. [Learn more about the Create Vehicle Panel.](/guide/create-vehicle-panel) +* **Create Vehicle:** Opens the creation panel, allowing you to establish a starting point, destination, and parameters for a new vehicle simulation. + +* **Create Criss-Cross:** Opens a specialized panel designed to generate a "criss-cross" simulation scenario, ideal for testing intersection routing or specific demonstration paths. -* **Create criss-cross:** Opens a specialized panel designed to generate a "criss-cross" simulation scenario, ideal for testing intersection routing or specific demonstration paths. [Learn more about the Create Criss-Cross Panel.](/guide/create-criss-cross-panel) diff --git a/apps/roadrunner-view/src/App.tsx b/apps/roadrunner-view/src/App.tsx index 74d93a1..3c4d32b 100644 --- a/apps/roadrunner-view/src/App.tsx +++ b/apps/roadrunner-view/src/App.tsx @@ -10,7 +10,7 @@ import { ProfilePage } from './components/ProfilePage/ProfilePage'; import { DriverViewPage } from './components/DriverViewPage/DriverViewPage'; import { library } from '@fortawesome/fontawesome-svg-core'; import fontawesome from '@fortawesome/fontawesome' -import { faHome, faSatellite, faMap, faUpRightAndDownLeftFromCenter, faEye, faEyeSlash, faArrowLeft, faArrowRight, fa0 } from '@fortawesome/free-solid-svg-icons'; +import { faHome, faSatellite, faMap, faUpRightAndDownLeftFromCenter, faEye, faEyeSlash, faArrowLeft, faArrowRight, fa0, faQuestionCircle } from '@fortawesome/free-solid-svg-icons'; import { faPowerOff, faUser } from '@fortawesome/fontawesome-free-solid' import { AboutPage } from './components/AboutPage/AboutPage'; import { GuidePage } from './components/GuidePage/GuidePage'; @@ -18,8 +18,10 @@ import { Vehicle3DMapPage } from './components/Vehicle3DMapPage/Vehicle3DMapPage import { GoogleHomePage } from './components/GoogleHomePage/GoogleHomePage'; import { GoogleVehicle3DMapPage } from './components/GoogleVehicle3DMapPage/GoogleVehicle3DMapPage'; import { GoogleDriverViewPage } from './components/GoogleDriverViewPage/GoogleDriverViewPage'; +import { HelpProvider } from './context/HelpContext'; +import { HelpDrawer } from './components/Shared/HelpDrawer'; -library.add(faHome, faSatellite, faMap, faUpRightAndDownLeftFromCenter, faEye, faEyeSlash, faArrowLeft, faArrowRight, fa0 ); +library.add(faHome, faSatellite, faMap, faUpRightAndDownLeftFromCenter, faEye, faEyeSlash, faArrowLeft, faArrowRight, fa0, faQuestionCircle ); fontawesome.library.add(faPowerOff, faUser); const HomeRedirect = () => { @@ -70,48 +72,51 @@ const WrappedGoogleDriverViewPage = () => ( export const App = () => { return ( -
-
- - } /> - } />} - /> - } />} - /> - } />} - /> - } />} - /> - } - /> - } - /> - } - /> - } />} - /> - } />} - /> - + +
+
+ + } /> + } />} + /> + } />} + /> + } />} + /> + } />} + /> + } + /> + } + /> + } + /> + } />} + /> + } />} + /> + +
+
-
+ ); } diff --git a/apps/roadrunner-view/src/components/GoogleVehicle3DMapPage/GoogleVehicle3DMapPage.tsx b/apps/roadrunner-view/src/components/GoogleVehicle3DMapPage/GoogleVehicle3DMapPage.tsx index dd91463..7b7496d 100644 --- a/apps/roadrunner-view/src/components/GoogleVehicle3DMapPage/GoogleVehicle3DMapPage.tsx +++ b/apps/roadrunner-view/src/components/GoogleVehicle3DMapPage/GoogleVehicle3DMapPage.tsx @@ -10,7 +10,6 @@ import { faHome, faMagic, faBars, - faUpRightAndDownLeftFromCenter, faChartLine, faChevronDown, faChevronUp, @@ -527,43 +526,7 @@ export const GoogleVehicle3DMapPage = () => { } }, [isDataLoaded, isMapReady, vehicleList, hasCenteredInitially]); - // Fit view bounds to contain all active vehicles - const fitAllOnScreen = useCallback(() => { - if (!isDataLoaded || vehicleList.length === 0) return; - let minLng = 180, maxLng = -180, minLat = 90, maxLat = -90; - vehicleList.forEach(v => { - minLng = Math.min(minLng, v.degLongitude); - maxLng = Math.max(maxLng, v.degLongitude); - minLat = Math.min(minLat, v.degLatitude); - maxLat = Math.max(maxLat, v.degLatitude); - }); - - const centerLat = (minLat + maxLat) / 2; - const centerLng = (minLng + maxLng) / 2; - const deltaLat = maxLat - minLat; - const deltaLng = maxLng - minLng; - - const distLat = deltaLat * 111000; - const distLng = deltaLng * 111000 * Math.cos(centerLat * Math.PI / 180); - const maxDist = Math.max(distLat, distLng, 500); - - const mapEl = mapRef.current; - if (mapEl) { - isProgrammaticUpdateRef.current = true; - mapEl.flyCameraTo({ - endCamera: { - center: { lat: centerLat, lng: centerLng, altitude: 0 }, - range: maxDist * 1.5, - heading: -20, - tilt: 65, - altitudeMode: 'RELATIVE_TO_GROUND' - }, - durationMillis: 0 - }); - isProgrammaticUpdateRef.current = false; - } - }, [isDataLoaded, vehicleList]); @@ -778,14 +741,6 @@ export const GoogleVehicle3DMapPage = () => { )} - diff --git a/apps/roadrunner-view/src/components/HomePage/CreateVehiclePanel.tsx b/apps/roadrunner-view/src/components/HomePage/CreateVehiclePanel.tsx index 793a19b..358fe41 100644 --- a/apps/roadrunner-view/src/components/HomePage/CreateVehiclePanel.tsx +++ b/apps/roadrunner-view/src/components/HomePage/CreateVehiclePanel.tsx @@ -9,6 +9,7 @@ import { PointPicker } from '../Shared/PointPicker'; import { useState } from "react"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { faTimes } from "@fortawesome/free-solid-svg-icons"; +import { HelpIconButton } from "../Shared/HelpIconButton"; export const CreateVehiclePanel = (props: { setIsCreateVehicleActive: any, @@ -114,7 +115,10 @@ export const CreateVehiclePanel = (props: { -
Create Vehicle
+
+ Create Vehicle + +
diff --git a/apps/roadrunner-view/src/components/HomePage/CrissCrossPanel.tsx b/apps/roadrunner-view/src/components/HomePage/CrissCrossPanel.tsx index adcb640..07836ff 100644 --- a/apps/roadrunner-view/src/components/HomePage/CrissCrossPanel.tsx +++ b/apps/roadrunner-view/src/components/HomePage/CrissCrossPanel.tsx @@ -6,6 +6,7 @@ import { CONFIG, getRestUrl } from "../../config"; import { PointPicker } from '../Shared/PointPicker'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { faTimes } from "@fortawesome/free-solid-svg-icons"; +import { HelpIconButton } from "../Shared/HelpIconButton"; export const CrissCrossPanel = (props: { setIsCrissCrossActive: any, @@ -234,7 +235,10 @@ export const CrissCrossPanel = (props: { return ( -
Create Criss-Cross
+
+ Create Criss-Cross + +
diff --git a/apps/roadrunner-view/src/components/HomePage/SimulationTable.tsx b/apps/roadrunner-view/src/components/HomePage/SimulationTable.tsx index 92410a0..819bf72 100644 --- a/apps/roadrunner-view/src/components/HomePage/SimulationTable.tsx +++ b/apps/roadrunner-view/src/components/HomePage/SimulationTable.tsx @@ -4,7 +4,7 @@ import { MaterialReactTable } from 'material-react-table'; import { getRestUrl } from "../../config"; import { usePlayback } from "../../context/PlaybackContext"; import { Button } from "react-bootstrap"; -import { useNavigate } from "react-router-dom"; +import { HelpIconButton } from "../Shared/HelpIconButton"; const timeFormatOptions: Intl.DateTimeFormatOptions = { timeZone: 'UTC', @@ -21,8 +21,6 @@ export const SimulationTable = (props: { toggleSimTable: any, returnToNow: any, }) => { - const navigate = useNavigate(); - const [data, setData] = useState([]); const [rowCount, setRowCount] = useState(0); @@ -58,88 +56,80 @@ export const SimulationTable = (props: { "Content-Type": "application/json", Authorization: `Bearer ${accessToken}`, }, - credentials: 'include', - signal: controller.signal, + signal: controller.signal }); - - const pagedModel = await res.json(); - const sessions = pagedModel._embedded?.simulationSessions ?? []; - - setData(sessions); - setRowCount(pagedModel.page?.totalElements ?? 0); - } catch (err:any) { - if (err.name !== "AbortError") { - console.error("Fetch error:", err); + const result = await res.json(); + + // Handle single-object response if API returns array or object + if (result && Array.isArray(result.sessions)) { + setData(result.sessions); + setRowCount(result.totalCount || result.sessions.length); + } else if (Array.isArray(result)) { + setData(result as any); + setRowCount(result.length); + } + } catch (err: any) { + if (err.name !== 'AbortError') { + console.error("Failed to load simulation sessions", err); } } } loadPage(); - return () => controller.abort(); }, [pagination.pageIndex, pagination.pageSize, dataSource]); - const [isMobile, setIsMobile] = useState(window.innerWidth < 768); - - useEffect(() => { - const handleResize = () => { - setIsMobile(window.innerWidth < 768); - }; - window.addEventListener('resize', handleResize); - return () => window.removeEventListener('resize', handleResize); - }, []); - - const columns = useMemo(() => { - const cols = [ + const columns = useMemo( + () => [ { - accessorKey: 'id', - header: 'Vehicle', - size: isMobile ? 85 : 150, - Cell: ({ cell }: any) => { - const val = cell.getValue() || ""; - return isMobile && val.length > 8 ? val.substring(0, 8) : val; - } - } - ]; - - if (!isMobile) { - cols.push({ - accessorKey: 'username', - header: 'Username', - size: 200, - Cell: ({ cell }: any) => cell.getValue() || "unknown-user" - }); - } - - cols.push({ - accessorKey: 'start', - header: 'Start Time', - size: isMobile ? 120 : 200, - Cell: ({ cell }: any) => new Date(cell.getValue()).toLocaleTimeString([], timeFormatOptions) + 'Z' - }); - - cols.push({ - header: 'Actions', - size: isMobile ? 80 : 120, - Cell: ({ row }: any) => { - return ( + accessorKey: 'vehicleId', + header: 'Vehicle ID', + size: 150, + }, + { + accessorFn: (row: any) => { + const date = new Date(row.start); + return date.toLocaleString('en-US', timeFormatOptions); + }, + id: 'start', + header: 'Start Time (UTC)', + size: 180, + }, + { + accessorFn: (row: any) => { + const date = new Date(row.end); + return date.toLocaleString('en-US', timeFormatOptions); + }, + id: 'end', + header: 'End Time (UTC)', + size: 180, + }, + { + accessorFn: (row: any) => `${row.pointCount ?? 0} pts`, + id: 'pointCount', + header: 'Data Points', + size: 120, + }, + { + id: 'actions', + header: 'Actions', + size: 120, + Cell: ({ row }: { row: any }) => ( - ); - } - }); - - return cols; - }, [navigate, setPlaybackSession, isMobile]); + ), + }, + ], + [props, setPlaybackSession] + ); return (
+
+
+ Simulation Sessions + +
+
{ cancelled = true; }; }, []); + const { openHelp } = useHelp(); + const logoutAndReturn = async () => { try { await signOut({ global: true }); @@ -135,12 +138,14 @@ export const AppNavBar = ({ }; const gotoAboutPage = useCallback(() => { - navigate('/about'); - }, [navigate]); + openHelp('about'); + setIsOpen(false); + }, [openHelp]); const gotoGuidePage = useCallback(() => { - navigate('/guide/overview'); - }, [navigate]); + openHelp('overview'); + setIsOpen(false); + }, [openHelp]); const gotoHomePage = useCallback(() => { @@ -172,7 +177,6 @@ export const AppNavBar = ({ {additionalMenuItems && additionalMenuItems(() => setIsOpen(false))} gotoGuidePage()} style={{ cursor: "pointer" }} @@ -182,7 +186,6 @@ export const AppNavBar = ({ gotoAboutPage()} style={{ cursor: "pointer" }} diff --git a/apps/roadrunner-view/src/components/Shared/ActiveVehiclePlot.tsx b/apps/roadrunner-view/src/components/Shared/ActiveVehiclePlot.tsx index 630c1dd..0963d72 100644 --- a/apps/roadrunner-view/src/components/Shared/ActiveVehiclePlot.tsx +++ b/apps/roadrunner-view/src/components/Shared/ActiveVehiclePlot.tsx @@ -2,6 +2,7 @@ import React, { useMemo, useState, useEffect, useCallback } from 'react'; import { usePlayback } from "../../context/PlaybackContext"; import { useSimulationSessionData } from '../../hooks/useSimulationSessionData'; import { Button } from "react-bootstrap"; +import { HelpIconButton } from "./HelpIconButton"; import { LineChart, Line, @@ -385,7 +386,12 @@ export const ActiveVehiclePlot = (props: { boxShadow: '0 4px 15px rgba(0,0,0,0.2)' }} > -
{getDynamicTitle(domain[0], domain[1])}
+
+
+ {getDynamicTitle(domain[0], domain[1])} + +
+
{ + const { activeTopicId, isOpen, closeHelp, openHelp } = useHelp(); + const [content, setContent] = useState(''); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + + const topicKey = activeTopicId && HELP_TOPICS[activeTopicId] ? activeTopicId : 'overview'; + const topic = HELP_TOPICS[topicKey]; + + useEffect(() => { + if (!isOpen || !topic) return; + + setLoading(true); + setError(null); + + fetch(`${topic.markdownUrl}?t=${Date.now()}`, { cache: 'no-cache' }) + .then((res) => { + if (!res.ok) { + throw new Error(`Failed to load documentation (${res.status})`); + } + return res.text(); + }) + .then((text) => { + setContent(text); + setLoading(false); + }) + .catch((err) => { + console.error('Help loading error:', err); + setError(`Unable to load guide content for "${topic.title}".`); + setLoading(false); + }); + }, [isOpen, topic]); + + return ( + + +
+ openHelp(e.target.value)} + className="fw-bold" + > + {Object.entries(HELP_TOPICS).map(([key, t]) => ( + + ))} + +
+
+ + + {loading && ( +
+ +
Loading guide...
+
+ )} + + {error && ( + + {error} + + )} + + {!loading && !error && ( +
+ { + let imageSrc = props.src || ''; + if (imageSrc.startsWith('./images/')) { + imageSrc = '/guide/' + imageSrc.substring(2); + } else if (imageSrc.startsWith('images/')) { + imageSrc = '/guide/' + imageSrc; + } else if (imageSrc.startsWith('./')) { + imageSrc = '/guide/' + imageSrc.substring(2); + } + + return ( + {props.alt + ); + } + }} + > + {content} + +
+ )} +
+
+ ); +}; diff --git a/apps/roadrunner-view/src/components/Shared/HelpIconButton.tsx b/apps/roadrunner-view/src/components/Shared/HelpIconButton.tsx new file mode 100644 index 0000000..0b21ff8 --- /dev/null +++ b/apps/roadrunner-view/src/components/Shared/HelpIconButton.tsx @@ -0,0 +1,39 @@ +import React from 'react'; +import { Button } from 'react-bootstrap'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { faQuestionCircle } from '@fortawesome/free-solid-svg-icons'; +import { useHelp } from '../../context/HelpContext'; + +interface HelpIconButtonProps { + topicId: string; + title?: string; + className?: string; + variant?: string; + size?: 'sm' | 'lg'; +} + +export const HelpIconButton: React.FC = ({ + topicId, + title = 'View Help', + className = '', + variant = 'link', + size +}) => { + const { openHelp } = useHelp(); + + return ( + + ); +}; diff --git a/apps/roadrunner-view/src/components/Shared/MarkdownPageLayout.tsx b/apps/roadrunner-view/src/components/Shared/MarkdownPageLayout.tsx index d93fe8d..970c1e4 100644 --- a/apps/roadrunner-view/src/components/Shared/MarkdownPageLayout.tsx +++ b/apps/roadrunner-view/src/components/Shared/MarkdownPageLayout.tsx @@ -15,7 +15,7 @@ export function MarkdownPageLayout({ title, markdownUrl, onClose }: MarkdownPage // Fetch the markdown text when the component mounts or the URL changes useEffect(() => { - fetch(markdownUrl) + fetch(`${markdownUrl}?t=${Date.now()}`, { cache: 'no-cache' }) .then((response) => { if (!response.ok) { throw new Error(`Failed to load markdown: ${response.statusText}`); @@ -79,23 +79,33 @@ export function MarkdownPageLayout({ title, markdownUrl, onClose }: MarkdownPage ( - {props.alt - ) + img: ({node, ...props}) => { + let imageSrc = props.src || ''; + if (imageSrc.startsWith('./images/')) { + imageSrc = '/guide/' + imageSrc.substring(2); + } else if (imageSrc.startsWith('images/')) { + imageSrc = '/guide/' + imageSrc; + } else if (imageSrc.startsWith('./')) { + imageSrc = '/guide/' + imageSrc.substring(2); + } + + return ( + {props.alt + ); + } }} > {content} diff --git a/apps/roadrunner-view/src/components/Vehicle3DMapPage/Vehicle3DMapPage.tsx b/apps/roadrunner-view/src/components/Vehicle3DMapPage/Vehicle3DMapPage.tsx index 923ff45..1d40759 100644 --- a/apps/roadrunner-view/src/components/Vehicle3DMapPage/Vehicle3DMapPage.tsx +++ b/apps/roadrunner-view/src/components/Vehicle3DMapPage/Vehicle3DMapPage.tsx @@ -11,7 +11,6 @@ import { faMap, faMagic, faBars, - faUpRightAndDownLeftFromCenter, faCompass, faChartLine, faChevronDown, @@ -360,35 +359,6 @@ export const Vehicle3DMapPage = () => { } }, [threeDMap, vehicleList]); - // Fit view bounds to contain all active vehicles - const fitAllOnScreen = useCallback(() => { - if (!isDataLoaded || vehicleList.length === 0) return; - - let minLongitude = 360.0; - let minLatitude = 360.0; - let maxLongitude = -360.0; - let maxLatitude = -360.0; - - vehicleList.forEach((vehicle) => { - minLatitude = Math.min(minLatitude, vehicle.degLatitude); - minLongitude = Math.min(minLongitude, vehicle.degLongitude); - maxLatitude = Math.max(maxLatitude, vehicle.degLatitude); - maxLongitude = Math.max(maxLongitude, vehicle.degLongitude); - }); - - // Expand bounds buffer slightly - const deltaLng = Math.max(0.005, maxLongitude - minLongitude); - const deltaLat = Math.max(0.005, maxLatitude - minLatitude); - - threeDMap?.fitBounds([ - [minLongitude - deltaLng * 0.1, minLatitude - deltaLat * 0.1], - [maxLongitude + deltaLng * 0.1, maxLatitude + deltaLat * 0.1] - ], { - pitch: mapViewState.pitch, - bearing: mapViewState.bearing, - duration: 1000 - }); - }, [isDataLoaded, vehicleList, threeDMap, mapViewState.pitch, mapViewState.bearing]); // Toggle Street vs Satellite maps const toggleMapStyle = useCallback(() => { @@ -618,14 +588,6 @@ export const Vehicle3DMapPage = () => { )} -