Compare commits
merge into: rakadu1:refactor/modularize
rakadu1:refactor/modularize
rakadu1:refactor/sync-centralize
pull from: rakadu1:refactor/sync-centralize
rakadu1:refactor/modularize
rakadu1:refactor/sync-centralize
95 Commits
refactor/m
...
refactor/s
| Author | SHA1 | Message | Date |
|---|---|---|---|
|
|
7f2a096444 |
Checking correct origin tracking
|
1 week ago |
|
|
b9d2cbe84f |
Testing the new commit architecture on GITEA
|
1 week ago |
|
|
639a926f95 |
fix(ui): restore missing resize handles for data explorer panel
|
4 weeks ago |
|
|
bc17f57b81 |
feat(explorer): implement vertical ADAS property view and optimize workspace layout
This commit introduces a specialized visualization for ADAS radar data and significantly
improves the Data Explorer's responsiveness at small window dimensions.
Key Changes:
- ADAS Visualization: Replaced the horizontal AG Grid for the ADAS tab with a custom
Vertical Property View. This uses a card-based layout with clean Key-Value tables,
optimizing for high-column counts and narrow panels.
- Layout Optimizations:
- Reduced minimum window dimensions for the Data Explorer from 400x300 to 250x200.
- Implemented horizontal scrolling for the tab navigation row to prevent UI breakage
at narrow widths.
- Added 'flex-shrink-0' guards to the panel header and tabs to ensure they remain
visible and stable during vertical resizing.
- UI Refinement: Cleaned up the ADAS view by removing redundant property IDs from
headers and centering all data within a structured 2-column grid.
- Documentation:
- Updated the /intel folder (readme.md, context.md, GEMINI.md) to reflect new features.
- Added "Rationale" comments to source code (index.html, dataExplorer.js) to document
architectural decisions and prevent future UI regressions.
Fixes: #ADAS-Explorer-Overhaul
|
4 weeks ago |
|
|
eb4fd856c0 |
updated readme for readability, & updated gemini.md for AI readability.
|
2 months ago |
|
|
8bffad0abc |
docs: restore original README content
|
2 months ago |
|
|
82c9862ec2 |
Updating the intelligence.
|
2 months ago |
|
|
ef6480621b |
refactor(ui): Unified Floating Panel Engine & Persistent Dashboard Memory
- Decoupled 'God Mode' from GridStack into a standalone floating fixed overlay. - Extracted 'makeDraggableAndResizable' utility with persistent localStorage memory. - Implemented "Soft-Reload" for GridStack: restores panel positions by gs-id without destructive DOM replacement. - Added viewport-aware "Rescue Logic" to pull off-screen panels back into view on window resize. - Re-architected God Mode auto-hide sequence (5s delay + 3s countdown with visual warning). - Implemented 'Auto-Focus' logic: clicking any floating panel populates it to the front (z-index: 40). - Hardened all p5.js sketches with 0-width guards to prevent crashes during container shifts. - Cleaned up redundant UI text overlays and unified the visual presentation. |
2 months ago |
|
|
e57f3abaed |
refactor(ui): Decoupled floating panels from gridstack with persistent UX
- Migrated God Mode Zoom to a standalone floating/draggable overlay.
- Extracted a unified [makeDraggableAndResizable](cci
|
2 months ago |
|
|
a819a72e01 |
Removed duplicated source of naming the zoom sketch.
|
2 months ago |
|
|
18a7c5640e |
feat(ui): Refactor Zoom Sketch to floating panel with auto-hide UX
- Extracted drag/resize logic from Data Explorer into reusable `ui.js` utility. - Migrated Zoom Sketch from GridStack into a free-floating, draggable, and resizable `z-30` overlay (default size 900x455). - Restored and optimized auto-hide timing lifecycle for Close-Up mode: - 0-5s: Clean freeze for data inspection without hovering. - 5-8s: Fading visual countdown warning prior to panel closure. - Implemented state flag `zoomPanelExplicitlyClosed` guaranteeing the "X" button correctly masks rapid re-triggers. - Bound 'g' toggle shortcut to aggressively wipe/close panels when exiting God Mode entirely. - Added visual "Mouse pointer Out of Bounds" overlay when cursor exits main radar canvas. |
2 months ago |
|
|
4506cbc569 |
feat(offline): enable GridStack offline-first fallback and harden local launch scripts
- Downloaded GridStack.js and CSS assets (v10.1.2) to the vendor folder for offline support. - Configured index.html with local-first script loading and CDN fallback logic. - Hardened server.py to explicitly bind to 127.0.0.1 and anchor to the script directory. - Optimized Visualization_Start.bat with working directory anchoring and a delayed browser launch to ensure the server is fully ready. |
2 months ago |
|
|
dc19e848d0 |
feat(layout): implement GridStack.js for resizable dashboards and fix p5.js resize stability
- Implemented GridStack.js as the core dashboard manager, enabling responsive dragging and resizing of the Radar, Video, and SpeedGraph panels. - Replaced global windowResized events with debounced ResizeObserver instances attached to the specific panel containers. - Fixed a massive memory leak in p5.js sketches by explicitly calling .remove() on old Graphics buffers before recreation. - Optimized zoomSketch.js by removing destructive canvas teardown calls from radarSketch and using smoothed camera coordinates for tooltip bounding logic. - Patched tooltip overflow bugs in drawUtils.js and speedGraphSketch.js to intelligently clamp within panel edges, even in narrow columns. - Disabled native p5 windowResized triggers to prevent double-firing when moving the web app across multiple monitors. |
2 months ago |
|
|
ea2d367500 |
ARAS Visualizer Version 3.3.0 - Executive Summary
Features & Enhancements: - Stability: Implemented custom server.py with zero-cache headers and V3.3.0 asset versioning to ensure latest code availability. - Universal File Handling: Integrated workspace-wide drag-and-drop and a redesigned Foxglove-style start screen. - Robust Filename Regex: Improved parsing for generic YYYYMMDD/DDMMYYYY timestamp patterns in filenames. - Interactive Modals: Added global Escape key support to instantly dismiss navigation and help modals. - Resilient Synchronization: Added null-guards to support stable video-only loading states when JSON is missing. Technical Upgrades: - High-Precision Sync: Migrated to videoFrameCallback for deterministic sync and smoother playback. - Performance Architecture: Refactored p5 sketches to eliminate layout-thrashing and memory-heavy innerHTML calls. - Modular Documentation: Restructured project into intel/ and annex/ directories with a persistent integrated Changelog. - Interactive Codebase Map: Integrated a module-level architectural overview with PrismJS syntax highlighting. Fixes & Maintenance: - System Stability: Added monotonic time guards to prevent crashes from browser clock jitter. - Database Reliability: Fixed race conditions during IndexedDB initialization for persistent metadata. - Management Utilities: Added simple_log_cfg.py for automated radar command extraction from logs. - Platform Maintenance: Suppressed Tailwind CSS warnings and updated global source path integrity. |
2 months ago |
|
|
2b53a589a7 |
V3.3.0. Stable release: Implemented cache-busting server, robust filename regex, and global ESC-key modal dismiss. Added null-guards for video-only loading and log-cfg utility.
|
2 months ago |
|
|
4cb40527ea |
**Release Draft: v3.2.7 (March 18, 2026)**
This release introduces a redesigned startup flow, universal drag-and-drop, and critical stability fixes for video-only telemetry. --- **New Features** * **Universal Drag-and-Drop:** Expanded functionality to allow file loading from any screen within the application. * **Redesigned Startup Flow:** Implemented a Foxglove-style SPA loading screen for a smoother initialization experience. * **In-App Changelog:** Added a dedicated button and modal to the header for viewing version updates directly. * **Codebase Overview:** Enhanced the architectural view with a structured file system tree and improved map visibility. **Fixes and Performance** * **Video-Only Stability:** Fixed crashes occurring during video-only file loading and added regression tests to ensure long-term stability. * **Render Pipeline:** Optimized the visualization engine and refined the zoom camera UX for more fluid interaction. * **Robust Parsing:** Improved date-time parsing logic and overlay resilience. * **UI Refinement:** Resized the visual map, hid redundant scrollbars, and updated the application favicon. **Maintenance** * **Infrastructure:** Restructured internal documentation into dedicated `annex/` and `intel/` directories. * **Configuration:** Updated `.gitignore` and refined internal documentation paths. |
2 months ago |
|
|
3d775be7b0 |
feat(ui): implement universal drag-and-drop and expanded start screen
- Expanded start screen card and drop zone visuals for better accessibility. - Implemented a global #global-drag-overlay that appears on drag over the entire window. - Attached drag-and-drop listeners to document.body for universal file loading. - Refined dragCounter logic to prevent overlay flickering during child element transit. |
2 months ago |
|
|
2ebeaf0fab |
fix: resolve crashes during video-only file loading
- Added null check for appState.vizData in sync.js to prevent TypeError during visualization reset. - Implemented key validation guards in db.js manual offset functions to avoid IndexedDB DataErrors. - Updated fileLoader.js to bypass manual offset lookup when no JSON filename is available. - Added regression test 'tests/regression_video_only.test.js' to verify stable partial load paths. |
2 months ago |
|
|
30ef74be6e |
Tried to supress Tailwind CSS warning.
|
2 months ago |
|
|
57d644e5e6 |
Ran command: `git diff`
Ran command: `git diff -U0`
fix: resolve console warnings and add configurable debug logging
This commit addresses active console warnings and reduces logging noise in the SpeedGraph component.
### Changes:
- **index.html**: Fixed `[Deprecation]` warning by replacing the non-standard `-webkit-appearance: slider-vertical` with modern CSS properties (`writing-mode: vertical-lr; direction: rtl;`).
- **speedGraphSketch.js**:
- Resolved `pop() was called without matching push()` error by removing an orphaned `pop()` call in [drawStaticGraphToBuffer](cci
|
2 months ago |
|
|
1556231f17 |
feat: robust date-time parsing and persistent overlay resilience
IMPROVEMENTS:
- **utils.js**: Enhanced [extractTimestampInfo](cci
|
2 months ago |
|
|
4d8c03ffe2 |
feat(ui): redesign startup flow with Foxglove-style SPA loading screen
This commit refactors the initialization phase of the Visualizer to act as a Single Page Application (SPA). A persistent workspace shell is now initially hidden behind a "Data Synchronizer" Start Screen, which handles file ingestion before seamlessly revealing the canvas. Part 1: Implement SPA Start Screen Modal and Data Routing * steps/index.html: Added the full-screen `start-screen-modal` overlay to intercept the user on load. Removed text placeholders inside the video and canvas containers since the workspace is now exclusively visible when populated. * steps/src/dom.js: Exported references for the new modal, drop zone, and primary load buttons. * steps/src/main.js: Routed standard drag-and-drop and click events to target the new Start Screen elements rather than the legacy footer buttons, initiating the existing file pipeline. Part 2: Integrate Loading Progress Natively * steps/index.html: Added a hidden `start-progress-container` directly inside the Start Screen card to prevent overlapping popups. * steps/src/dom.js: Exported references to the native progress bar and text elements. * steps/src/modal.js: Refactored `showLoadingModal`, `updateLoadingModal`, and `hideModal` to dynamically route progress updates. If the Start Screen is active, it renders the inline progress bar and disables background buttons rather than spawning the generic popup modal. * steps/src/fileLoader.js: Updated `processFilePipeline` to fully dismiss the `start-screen-modal` upon successful data ingestion and precomputation, seamlessly transitioning the user. Part 3: Replicate Accessibility and Theme Controls * steps/index.html: Duplicated the Quick Start Guide, Codebase Overview, What's New, and Theme Toggle buttons, positioning them in the top-right corner of the Start Screen. Also fixed a `-webkit-appearance` CSS warning on the range slider. * steps/src/dom.js: Exported references for the new accessibility buttons and theme toggle SVGs. * steps/src/ui.js: Attached existing `toggleGuideModal`, `toggleCodebaseModal`, and `toggleChangelogModal` event listeners to the new replicated buttons, granting users access to documentation before data load. * steps/src/theme.js: Updated `initializeTheme()` so switching the theme from either the workspace or the Start Screen bidirectionally syncs the Light/Dark SVGs on both buttons simultaneously. |
2 months ago |
|
|
d5f7ef02f7 |
perf(viz): optimize render pipeline and refine zoom camera UX
Refactors the rendering logic for both main radar and zoom views to support
high-refresh-rate monitors and 4K displays without performance loss.
Key Changes:
1. Performance & Display:
- Uncapped Frame Rate: Set `p.frameRate(144)` in both sketches. This removes
artificial throttling on high-refresh monitors (75Hz+), eliminating
beat-frequency judder.
- 4K Quality: Removed `p.pixelDensity(1)` restrictions. High-end PCs will
now render at full device resolution (Retina/4K) instead of being downscaled.
2. Zoom Sketch Optimization:
- Viewport Culling: Implemented background slicing logic to only render the
visible portion of the static background image, significantly reducing GPU
bandwidth usage during zoom.
3. UX & Animation:
- Camera Smoothing: Applied a frame-rate independent Lerp (factor 0.5) to the
zoom camera. This mimics the weight and feel of the main radar cursor.
- Visual "Lead": Added a `leadFactor` (0.2) to the dashed hover circle. It
now interpolates between the camera and mouse, making controls feel
instant/elastic even while the view smooths out.
- Coordinate Fixes: Updated tooltip and connector logic to map correctly
from the smoothed camera space to screen space.
|
2 months ago |
|
|
19576ccb49 |
feat(zoom): optimize rendering pipeline and polish camera UX
Addresses performance bottlenecks on high-end PCs and refines the
zoom interaction model to eliminate jitter while maintaining responsiveness.
Performance Optimizations:
1. High-DPI Scaling Fix:
- Forced `p.pixelDensity(1)` in zoomSketch.
- Issue: Retinal/4K screens were defaulting to pixelDensity 2.0+, causing
the GPU to render 4x the necessary pixels (e.g., 1000x1000 for a 500x500 canvas).
This bandwidth saturation caused FPS drops on powerful GPUs.
2. Refresh Rate Uncap:
- Set `p.frameRate(144)` explicitly.
- Issue: p5.js often throttles to 60fps. On 75Hz+ monitors, this caused a
"beat frequency" judder where update cycles missed display refresh cycles.
UX & Animation Logic:
1. Camera Smoothing (The "Cinematic" Feel):
- Decoupled the Zoom Camera position from the Raw Mouse position.
- Applied a Lerp smoothing factor of 0.5 (aligned with main radarSketch
cursor logic) to create fluid motion without feeling sluggish.
2. Visual "Lead" / Elasticity:
- Introduced `zoomLeadFactor` (0.2) for the dashed hover circle.
- The circle position is interpolated between the Smoothed Camera and
the Raw Mouse.
- Benefit: This creates a visual cue that "leads" the camera, making the
controls feel responsive/instant even while the view smoothly catches up.
3. Coordinate System Fixes:
- Reverted manual world-to-screen reprojection for tooltips.
- Adopted a relative screen-space transform:
(ItemScreenPos - CameraPos) * ZoomFactor.
- This ensures connector lines lock perfectly to visual elements regardless
of camera lag.
|
2 months ago |
|
|
c5c7069f74 |
style(overview): resize visual map and hide scrollbars
- Expanded Visual Navigation Map to utilize full width and increased height by 30%. - Removed visible scrollbar from the Project Files panel. - Adjusted padding in the file tree view for better alignment. |
2 months ago |
|
|
78fb576da6 |
Adding the root folder as well.
|
2 months ago |
|
|
aea83b2323 |
Added the tree structure to file system in code base overview.
|
2 months ago |
|
|
ecd521ee4d |
updating the intel
|
2 months ago |
|
|
973471ca97 |
feat(ui): add changelog button and modal, repositioned to header end
|
2 months ago |
|
|
3a12ca6765 |
chore: update internal paths for documentation moved to annex/
|
2 months ago |
|
|
f5b4ebfa7a |
updated favicon, gitignore
|
2 months ago |
|
|
f70785bdad |
Updated the changelog.
|
3 months ago |
|
|
27f582e9c9 |
Changelogs_Added
|
3 months ago |
|
|
b24f6f460e |
fix(viz): add stability guards for smoothing and data mapping
- Clamp delta time to minimum of 0 to prevent negative interpolation. - Add protection against division by zero in SNR color mapping. - Ensure trajectory points exist before fading alpha calculation. - Standardize p.deltaTime usage for cross-browser stability. |
3 months ago |
|
|
76d2fb1d47 |
feat(viz): implement frame-rate independent smoothing for UI and sketches
Refactors animation and interpolation logic to ensure consistent behavior across different monitor refresh rates (e.g., 60Hz vs 144Hz). Changes: - Implement time-delta adjusted smoothing for FPS counter and mouse tracking. - Update IFT graph scaling in 'dom.js' to use performance-based delta time. - Remove hardcoded 60FPS limit in zoom sketch to allow native refresh rates. - Fix vertical tooltip jitter in 'drawUtils.js' by using smoothed mouse coordinates. - Add 'lastOverlayUpdateTime' to global state for cross-module time tracking. |
3 months ago |
|
|
b63b3ae294 |
* Fixed a bug in handleCloseUpDisplay where p.mouseY was being used directly instead of the passed-in (and potentially smoothed) mouseY value.
|
3 months ago |
|
|
53aa70afe3 |
feat(viz): implement smooth mouse tracking for zoom tooltip
Adds linear interpolation (lerp) to mouse coordinates in the zoom view to prevent jitter and provide a smoother user experience when inspecting radar points. Changes: - Add `smoothedMouseX` and `smoothedMouseY` state to `radarSketch.js`. - Apply smoothing factor (0.5) to mouse movement in the main draw loop. - Update `handleCloseUpDisplay` and zoom sketch drawing to use smoothed coordinates. - Ensure coordinates snap to mouse position on the first frame to prevent visual jumping. |
3 months ago |
|
|
2391f3a889 |
feat(ui): improve IFT graph scaling animation and performance
- Implemented smooth dynamic scaling for the Inter-Frame Timing (IFT) graph that remains active even when playback is paused. - Added a conditional `requestAnimationFrame` loop to ensure the graph scale converges smoothly to the target value during scrubbing or pause states. - Tuned animation speeds: fast convergence (0.1 smoothing) during playback, slower (0.033) when paused for a more polished visual transition. - Fixed a layout thrashing (forced reflow) performance issue in `updatePersistentOverlays` by reading `clientWidth` before writing text updates to the DOM. |
3 months ago |
|
|
086bef119d |
✦ feat: enhance visualizations with smart labeling and density-based speed coloring
radar view (src/drawUtils.js):
- Refactored drawTrackMarkers with a smart collision-resolution algorithm for floating labels.
- Added leader lines and themed tooltips for track IDs and speed data to prevent overlapping.
- Optimized velocity vectors with arrowheads and reduced thickness for better clarity.
- Updated drawRegionsOfInterest to use ROI_CLOSE_Y_MAX constant.
speed graph (src/p5/speedGraphSketch.js):
- Implemented a MATLAB-style spectral color scheme (Blue-Cyan-Green-Yellow-Red) to visualize track density on the CAN speed
line.
- Added a vertical smooth gradient legend bar for track density.
- Switched to robust 95th percentile normalization for density mapping to mitigate outlier noise.
- Synchronized density calculations with the "Confirmed Only" toggle.
- Updated the horizontal legend to reflect density-based coloring.
zoom view (src/p5/zoomSketch.js):
- Adjusted tooltip vertical offset for improved diagonal positioning.
- Disabled internal track detail boxes to prioritize the specialized zoom tooltip system.
|
3 months ago |
|
|
ccdd91aee1 |
First iteration of GEmini.md file
|
3 months ago |
|
|
e7c72160ed |
- Add vertical range slider (20m-200m) to adjust RADAR_Y_MAX dynamically.
- Implement robust fallback and safety logic to prevent division-by-zero or invalid state crashes. - Update scale calculations and drawing utilities to use dynamic boundaries from appState. - Refine radar-info-overlay to match plot width and center text content for better alignment. - Add double-click reset functionality to the range slider to revert to default constants. - Fix hover detection bug in God Mode during scroll-zoom interactions. - Ensure Regions of Interest (ROI) scale proportionally with the dynamic plot range. |
3 months ago |
|
|
7aadf264d6 |
UX: Contraining the GodMode tooltip inside the bounding sketch box.
|
3 months ago |
|
|
6a02df2e48 |
Inverse Zom Logic implemented.
|
3 months ago |
|
|
d26aaf0a25 |
Risk and State updated in track marker and zoom sketch tooltip
|
3 months ago |
|
|
5ebd55d876 |
Confirmed ONLY tracks visible, now fixed
|
3 months ago |
|
|
5a261a0391 |
Video_Player mute removed and controls option added.
|
4 months ago |
|
|
193cb81599 |
Sun icon fixed.
|
4 months ago |
|
|
ff57a4fa1c |
minor Dark mode improvements here and there.
|
4 months ago |
|
|
b982a9179e |
1 feat: add interactive codebase overview and architectural map
2 Take a look at edge cases. 3 - Implement a "Living Documentation" tool integrated directly into the application. 4 - Create a 3D-styled visual navigation map representing UI components and data flow. 5 - Add a color-coded file explorer themed by architectural section (Core, Sync, UI, etc.). 6 - Implement smart "Mini-Map" UX: docks to corner on scroll with hover-to-expand. 7 - Add local PrismJS support for high-performance source code syntax highlighting. 8 - Optimize navigation with animation locks and automatic scroll-position resets. |
4 months ago |
|
|
4a4e418e73 |
Readme.md updated once again.
|
4 months ago |
|
|
90e171d34f |
feat: integrate codebase overview modal into main application
|
4 months ago |
|
|
c05f81aa98 |
feat: implement interactive codebase overview with visual navigation map
|
4 months ago |
|
|
84f61ecd6e |
chore: add PrismJS dependencies for code syntax highlighting
|
4 months ago |
|
|
e13a734ab9 |
feat: Add vehicle dimensions visualization and enhance track risk coloring
Vehicle Dimensions: - Added "Show Dimensions" toggle to the display settings. - Implemented `drawObjectDimensions` in `drawUtils.js` to render object extents as oriented rectangles based on `objectExtentRadii` and `objectExtentAngle`. - Updated `radarSketch.js` to draw dimensions when enabled. - Ensured dimensions and covariance ellipses are drawn at the `correctedPosition` for accurate alignment with track markers. Track Visualization: - Updated `drawTrajectories` to prioritize the `risk` property on track logs for coloring (0=Low/Blue, 1=Medium/Orange, 2=High/Red). - Added fallback to `ttcCategoryTimeline` for backward compatibility. - Fixed off-by-one error in trajectory history filtering to prevent drawing future points. Frame Synchronization & UI: - Refactored frame number display logic across Overlays, Data Explorer, and Timeline. - UI now displays the actual `frameIdx` from the data source instead of the internal array index, resolving discrepancies between the Data Explorer and the main view. |
4 months ago |
|
|
b905f8536b |
added state beside ttc marker on the visualization.
|
4 months ago |
|
|
073f30c482 |
feat: Enhance radar overlay responsiveness and robustness
This commit introduces several improvements to the radar visualization, focusing on responsiveness, error handling, and clearer data representation. **Key Changes:** * **Responsive Radar Overlay:** The persistent radar information overlay (`ift-dot-matrix` canvas) in `dom.js` is no longer a fixed size. It now dynamically adjusts its width to match the radar plot's size, ensuring a consistent visual experience across different screen resolutions and resize events. The internal drawing logic has been updated to dynamically calculate column counts based on the canvas's current dimensions. * **Improved Error Handling in Drawing Utilities:** Comprehensive `try...catch` blocks have been added to all major drawing functions within `drawUtils.js`. This significantly enhances the application's robustness by gracefully handling potential errors during rendering, preventing crashes and providing better debugging insights. * **TTC Category Fallback:** In `drawUtils.js`, the trajectory drawing logic for Time-to-Collision (TTC) now includes a fallback mechanism. If a track's `ttcCategory` is undefined or invalid, it will default to a neutral gray color, ensuring that trajectories are always rendered with a clear visual state. |
4 months ago |
|
|
bc239a552e |
New data structures added for reference.
|
4 months ago |
|
|
e4e07bb1c5 |
Auto loading quick start guide feature added.
|
5 months ago |
|
|
5bab276c18 |
Margin reduction in user guide.
|
5 months ago |
|
|
e85b031f32 |
HTML changes.
|
5 months ago |
|
|
ffaf2a2209 |
feat: add user guide modal on first session launch
- Implemented a "First Run" detection using `sessionStorage`. - The User Guide modal now appears automatically when application is launched in a new tab or window. - Logic added to skip the automatic file loading (cached session) on the first run to ensure a clean start for new sessions. - Subsequent page reloads within the same session will hide the guide and resume the cached session as normal. |
5 months ago |
|
|
639ec34e5d |
Scale removed from Persistent overlay and theme button color scheme changed.
|
6 months ago |
|
|
1652bb0658 |
feat(ui): add in-app modals for user guide and shortcuts
- Create shortcuts.html as a standalone reference page. - Implement #shortcuts-modal in index.html to display key bindings. - Implement #guide-modal with an iframe to embed User_Manual.html directly in the app. - Refactor header navigation into color-coded buttons (Green/Blue/Amber-Indigo). - Update src/dom.js and src/ui.js to handle modal visibility and interactions (ESC key, 'k' key, click-to-close). - Improve visual consistency of header button heights and modal styling. |
6 months ago |
|
|
f03bbbeeb0 |
feat(ui): add in-app modals for user guide and shortcuts.
Create `shortcuts.html` as a standalone reference page. Implement `#shortcuts-modal` in `index.html` to display key bindings. Implement `#guide-modal` with an iframe to embed `User_Manual.html` directly in the app. Refactor header navigation into color-coded buttons (Green/Blue/Amber-Indigo). Update `src/dom.js` and `src/ui.js` to handle modal visibili and interactions (ESC key, 'k' key, click-to-close). Improve visual consistency of header button heights and moda styling. |
6 months ago |
|
|
0495bb084f |
refactor(app): Extract UI and session logic from main.js
This commit refactors the monolithic `main.js` file to improve modularity, maintainability, and separation of concerns. The core application logic is now organized into more focused modules. - **UI Logic (`ui.js`):** All general UI event listeners, including feature toggles, sliders, menu controls, and tooltips, have been moved from `main.js` into a new `src/ui.js` module. This centralizes UI-specific behavior. - **Session Management (`session.js`):** The functionality for saving and loading application sessions has been extracted into a dedicated `src/session.js` module. This isolates all logic related to session state serialization and file handling. - **`main.js` Simplification:** The `main.js` file now serves as a cleaner entry point, responsible for initializing all the different application modules in the correct order. **Bug Fixes during Refactoring:** - Corrected an issue where activating "God Mode" (`toggleCloseUp`) did not properly start the p5.js `loop()`, preventing the visualization from updating with mouse movement. - Ensured the `toggleConfirmedOnly` state is now correctly saved and restored as part of the session file. |
6 months ago |
|
|
29875de89f |
feat(ui): Enable timeline scroll on speed graph
Adds a 'wheel' event listener to the speed graph container, binding it to the existing `handleTimelineWheel` function. This change enhances user experience by allowing timeline scrubbing via the mouse scroll wheel when the cursor is over the speed graph. It creates consistent behavior across all major visualization components (radar canvas, timeline, and speed graph), improving the application's usability. The logic is centralized within `initSyncUIHandlers` in `sync.js` to keep all synchronization-related event bindings in one place. |
6 months ago |
|
|
a6458d9de0 |
Fix minor issue.
|
6 months ago |
|
|
9df5006f85 |
feat(offset): Improve manual offset workflow and persistence
Improves the time offset feature by making manual offsets persistent and enhancing the user workflow. - **Persistent Manual Offsets**: Manual offsets are now saved to IndexedDB, keyed by filename. They are automatically applied when the same file is loaded in a future session. - **Click to Revert & Forget**: The "Manual" status indicator is now clickable. Clicking it reverts to the auto-calculated offset and, crucially, deletes the saved manual offset from the database. This ensures future sessions will correctly default to the automatic calculation. - **Consistent UI**: The "Manual" indicator now appears instantly with a consistent gray style, whether set manually or loaded from cache, removing UI ambiguity. - **Refactored Sync Logic**: The `revertToAutoOffset` function has been refactored to reuse the core `forceResyncWithOffset` logic. This reduces code duplication and centralizes the synchronization process, making it more robust and easier to maintain. |
6 months ago |
|
|
db3e7495c0 |
feat(file-loading): Improve FPS counter accuracy and video retention Enhance FPS counter stability by implementing a warmup period and explicit reset of appState.fps`, preventing erroneous spikes after new file loads.
Resolve infinite p5.js auto-draw loop during file processing by ensuring comprehensive UI cleanup and correct p5 instance lifecycle management (`redraw` instead of `lo`). Introduce conditional video retention in `resetUIForNewLoad`, allowing the loaded video to persist when only new JSON data is supplied. |
6 months ago |
|
|
eed086b2bc |
Minor perf. boost by reducing text content updates.
|
6 months ago |
|
|
d79f7c51c8 |
The Fix:
The new loop iterates through the data and draws rectangles directly to the canvas. It changes ctx.fillStyle more often (once per column), but this cost is negligible to the cost of allocating and collecting memory 60 times a second. This should resolve the heap memory issue you observed. |
6 months ago |
|
|
8b667e6ad7 |
Throttling the update explorer for the MATLAB style data explorer had unnescessary function calls even when closed. Removed that for 20% perf. boost.
|
6 months ago |
|
|
e6543e39f1 |
DOM Caching and Inner HTML removal.
|
6 months ago |
|
|
b8bf85491e |
Perforamance boost by removing unnescessary calls like draw axes in every amimation loop
|
6 months ago |
|
|
bee80cda95 |
Forgot to add some more changes in the Inter frame timing Graph.
|
6 months ago |
|
|
c893ebf4b4 |
Added the Interframetiming Graph.
|
6 months ago |
|
|
1b699018de |
Minor changes in Dyanic SNR name, Abs time removed from radar overlay (it was redundant)
|
6 months ago |
|
|
e5b5c6629e |
Updated context.md and Readme.md
|
6 months ago |
|
|
aa2c808e52 |
1 feat(viz): add interactive seeking to speed graph and optimize
theme redraw
2
3 - Implemented hover tooltips in `speedGraphSketch.js` to displ
CAN/Ego speeds and timestamps.
4 - Added drag-to-seek (scrubbing) and click-to-seek functionali
using native pointer events for smooth navigation.
5 - Integrated graph seeking with centralized `updateFrame` logi
to ensure consistent video synchronization and UI updates.
6 - Refactored input handling to use DOM listeners on the canvas
resolving conflicts with global p5 events (e.g., play button
issues).
7 - Optimized `theme.js` to redraw only the static graph buffer
theme changes instead of re-processing the entire dataset.
|
6 months ago |
|
|
ae9b8dc62b |
refactor(core): Enhance robustness of file loading and UI state management
This commit introduces a series of significant improvements to the file processing pipeline and UI update logic, resolving several critical bugs related to state management, UI freezes, and component rendering. The primary goal was to decouple UI components from strict file dependencies and ensure the application remains responsive and predictable under various file loading scenarios.
### Key Changes and Fixes:
1. **Graceful Handling of File Swapping (Fixes UI Freeze):**
- Resolved a critical bug where loading a new JSON file while a video was already present would cause the application to freeze at "Parsing JSON (100%)".
- The root cause was an incomplete state reset. The fix involves calling `resetVisualization()` at the beginning of `finalizeSetup()`, which ensures the playback timeline and animation loops are reset to a clean slate before redrawing with new data.
- This change allows users to seamlessly load and compare different JSON logs against the same video without unloading the video or freezing the application.
2. **Decoupled and Robust Speed Graph:**
- The speed graph is no longer dependent on a video file to render. It will now draw correctly as long as valid JSON data is available.
- The graph's X-axis is now consistently scaled based on the JSON data's internal duration (`finalDuration = jsonDuration`). This makes the graph's behavior predictable and ensures all data within the JSON file is always visible.
- Corrected the data source for plotting. The graph now uses the `frame.timestamp` property, which is the reliable relative time from the start of the log. This fixes a bug where the graph would appear to start late (e.g., at 220s) because it was incorrectly using `timestampMs`, a property modified by the video synchronization offset.
3. **Object URL Lifecycle Management (Memory Leak Fix):**
- Implemented proper lifecycle management for video object URLs created with `URL.createObjectURL()`.
- Before creating a new video URL, the application now checks for and revokes any existing `appState.videoObjectUrl` to prevent memory leaks from accumulating unused blob URLs.
4. **Verified Synchronization Failsafe:**
- Confirmed that the failsafe logic in `calculateAndSetOffset` is working as intended. If the automatically calculated offset between a video and JSON file exceeds a 30-second threshold, it correctly defaults to `0`, preventing extreme and incorrect synchronization when mismatched files are loaded.
|
6 months ago |
|
|
10c52d318a |
feat(loading): Implement robust video loading and non-blocking cache
This commit introduces significant improvements to the file loading pipeline to enhance stability and user experience. The application startup is no longer blocked by file caching, and it now gracefully handles corrupted or slow-loading video files instead of hanging. Key Changes: - **Non-Blocking File Caching:** - Caching files to IndexedDB is now a non-blocking, "fire-and-forget" operation. - `saveFileWithMetadata` calls are no longer awaited, allowing the UI and parsing to proceed immediately. - `Promise.allSettled` is used to log cache results in the background. - **Robust Video Loading:** - Implemented a 10-second timeout for the video `canplaythrough` event to prevent indefinite hangs. - If the timeout occurs but `loadedmetadata` has fired, the video is treated as playable, allowing the application to continue. - If the video fails to load entirely (due to a timeout or an `error` event), the user is presented with a modal to either "Retry" or "Continue without Video". - **Bug Fixes:** - Fixed a critical state bug where `appState.videoMissing` was not reset on a new file load, which caused issues when re-uploading a valid video after a failure. - Resolved multiple race conditions in the modal system where error and choice modals would close prematurely after being displayed. - Replaced the blocking `alert()` on storage quota errors with a non-blocking modal, ensuring the app remains responsive. |
6 months ago |
|
|
59fef58ab4 |
*Goal*
Make file caching fire-and-forget so IndexedDB writes do not block parsing and video loading. *Actions* Update the file pipeline code where saveFileWithMetadata(...) is called: Replace await saveFileWithMetadata(...) with a non-blocking call that stores the promise in an array, e.g. cachePromises.push(saveFileWithMetadata(...).catch(e => logWarning(e))). At the end of the pipeline kick off Promise.allSettled(cachePromises) but do not await it before parsing starts; instead log results or show a non-blocking notification on failure. Add a configurable CACHE_BLOCKING boolean flag in config. Default false; if true (special mode) allow blocking for debugging. Add logging and UI notification for cache failures without blocking the pipeline. Convert any alert() on quota errors into a non-blocking modal message or toast. Add unit tests / smoke tests: Simulate slow saveFileWithMetadata() (wrap with artificial delay) and confirm parsing and video loading start immediately without waiting for caching to finish. Simulate a QuotaExceededError and confirm the pipeline continues. *Check list* Replace await saveFileWithMetadata(...) with non-blocking logic. Pipeline starts parsing immediately even when cache save is delayed. Promise.allSettled(cachePromises) is triggered and results logged. UI shows non-blocking notification for cache failures (no alert() popups). Tests simulate slow save and quota errors and pass. |
6 months ago |
|
|
79b611efe1 |
1 Refactor file loading architecture for modularity and robust synchronization
2
3 - **Modularization:** Extracted monolithic file handling from `main.js` into a new dedicated `src/fileLoader.js`
module.
4 - **Incremental Loading:** Implemented logic to handle JSON and Video files independently, allowing users to replac
individual files without breaking the application state.
5 - **Race Condition Fixes:** Updated `src/db.js` to return Promises and await database initialization, resolving
startup crashes.
6 - **Sync Logic Correction:** Reordered the loading pipeline to ensure file metadata (and thus timestamps) are
extracted *before* parsing visualization data. This fixes `NaN` errors in the speed graph and ensures correct video
synchronization.
7 - **Optimization:** Added a `fromCache` flag to skip redundant database writes when reloading a previous session.
8 - **Testing:** Added `tests/fileLoader.test.js` to verify the new loading pipeline.
|
6 months ago |
|
|
cdaed0d4c6 |
File loading overhaul part 1
|
6 months ago |
|
|
6d0535b8be |
Speed Graph Sketch issue solved using CLI
" Here is the detailed breakdown of why that solution worked.
The Root Cause: Initialization Order
The error ellipse() ... received NaN occurred because the speedGraphSketch was trying to plot a point based on time
(x-axis), but the time value for the current frame was invalid (NaN or undefined).
Here is the chain of events that caused this:
1. The Dependency Chain:
* To draw the graph, the sketch needs frame.timestampMs.
* To calculate frame.timestampMs, the JSON Parser (parseVisualizationJson) needs appState.videoStartDate.
* To get appState.videoStartDate, the helper function calculateAndSetOffset() needs to know the Video Filename.
2. The Bug (Before Refactor):
When you dragged both files in, the code executed sequentially like this:
* Step A (JSON Block): The code detected the JSON file. It set appState.jsonFilename and immediately called
calculateAndSetOffset().
* CRITICAL FAILURE: At this exact moment, the code had not yet processed the Video file. It didn't know the
video filename.
* Result: calculateAndSetOffset couldn't find a video timestamp, so appState.videoStartDate remained null.
* Step B (Parsing): The JSON parser ran. Because videoStartDate was null, it skipped the math that calculates
frame.timestampMs. All your data frames were created with timestampMs: undefined.
* Step C (Video Block): The code moved to the Video file. It set appState.videoFilename and called
calculateAndSetOffset() again.
* Now it calculated the correct date, but it was too late. The JSON data had already been parsed and baked
without timestamps.
3. The Crash:
When the visualization started, the Speed Graph looked at the current frame, found timestampMs was missing, tried
to divide it by 1000, got NaN, and crashed the ellipse() drawing function.
How the Solution Fixed It
By restructuring processFilePipeline, we enforced the correct dependency order:
1. Pre-Processing (The Fix):
We moved the "registration" of both filenames to the very top of the function.
1 // 1. Register JSON file (if present)
2 // 2. Register Video file (if present)
2. Calculation:
We call calculateAndSetOffset() once, immediately after registration.
* Since both filenames are now known, it successfully extracts the timestamp from the video filename and sets
appState.videoStartDate.
3. Processing:
We then start the JSON parser.
* It receives the valid appState.videoStartDate.
* It successfully calculates frame.timestampMs for every frame.
In short: We ensured that the "ingredients" (filenames and dates) were fully prepared before we started "cooking"
(parsing) the data"
|
6 months ago |
|
|
0980398ed2 |
Shift key god mode added.
|
6 months ago |
|
|
5ee39dafac |
refactor(keyboard): Isolate keyboard shortcut logic
This commit extracts the keyboard shortcut handling logic from the main application entry point (`main.js`) into a new, dedicated module (`src/keyboard.js`). Previously, all keydown event listeners were defined directly in `main.js`, mixing UI initialization with user input handling. This refactoring improves code organization and modularity by separating concerns, making the system easier to maintain and debug. Key changes include: - A new `src/keyboard.js` file was created to house all keyboard-related functionality. - The `keydown` event listener and its handler function (`handleKeyDown`) were moved from `main.js` into the new module. - An `initKeyboardShortcuts` function is now exported from `keyboard.js` and called from `main.js` during application startup. - All dependencies required by the shortcut logic (from `dom.js`, `state.js`, `sync.js`, etc.) are now correctly imported into `keyboard.js`. This is a pure refactoring effort and introduces no changes to application behavior. |
6 months ago |
|
|
02601fd52c |
feat(sync): Rearchitect sync logic and add advanced navigation
This commit introduces a major refactoring of the synchronization architecture and adds several new user-facing navigation features to improve usability, performance, and accuracy.
### Synchronization Architecture Rearchitect
- **Pre-computed Timestamps:** Implemented a "baking" process (`precomputeRadarVideoSync`) that calculates a `videoSyncedTime` for each radar frame upon data load or offset change. This eliminates redundant on-the-fly calculations during playback.
- **Simplified Sync Loop:** The main `videoFrameCallback` now performs a simple binary search against the pre-computed timestamps, making the core loop faster and more reliable.
- **Centralized Offset Logic:** Manual offset changes are now handled by a single function (`forceResyncWithOffset`) that re-bakes the timing data, ensuring UI consistency and removing duplicated code.
- **Accurate Drift Calculation:** Fixed the debug overlay's drift calculation to use the new `videoSyncedTime`, providing accurate diagnostics with both automatic and manual offsets.
### New Navigation & UI Features
- **Scroll-to-Seek:**
- Implemented scroll-to-seek on both the main radar plot and the video player.
- Features dynamic acceleration for fast scrubbing and single-frame precision for slow scrolling.
- Uses a stateful approach and debouncing to provide a smooth, responsive UI while preventing video stutter.
- All overlays now update instantly during scroll gestures for a seamless experience.
- **Frame-by-Frame Seeking:**
- Added keyboard shortcuts (`ArrowUp`/`ArrowDown`) for single-frame video seeking.
- Fixed a bug where radar seeking (`ArrowLeft`/`ArrowRight`) did not update the UI when paused.
- **God Mode (Zoom) Integration:**
- Disabled timeline seeking via scroll wheel when zoom mode is active to prevent conflicting user actions.
### Bug Fixes
- Fixed a "sticky acceleration" bug where scroll speed was not reset after a seeking gesture.
- Corrected an inverted scroll direction on the timeline slider.
- Fixed a rendering error in the advanced debug overlay caused by a stale variable reference.
- Ensured persistent overlays are correctly hidden when debug overlays are active.
|
6 months ago |
|
|
c1e30e3e17 |
Debug overlays fixed.
|
6 months ago |
|
|
1adc8b78ec |
LEFT RIGHT UP DOWN Arrow keys shortcut added.
LEFT/RIGHT:- Seek radar frames UP?Down:- Seek Video Frames. |
6 months ago |
|
|
52ce3aa8f3 |
fix(sync): centralize offset handling and correct drift math
- Refactor(main.js): The manual offset input listener contained duplicated logic for pausing and re-syncing. This has been removed and replaced with `forceResyncWithOffset` imported from `sync.js`. - Fix(dom.js): The drift calculation in `updateDebugOverlay` and `updatePersistentOverlays` was using static `timestampMs` combined with the dynamic offset, leading to incorrect values after a manual adjustment. Updated to use `currentRadarFrame.videoSyncedTime` for accurate, architecture-aligned drift reporting. |
6 months ago |
|
|
7690b899f5 |
feat(sync): hybrid architecture for video/radar synchronization
The previous synchronization logic relied on either naive relative time (causing drift) or expensive runtime date parsing (causing lag). This commit implements a hybrid "Baked Offset" architecture. Changes: - src/utils.js: Added `precomputeRadarVideoSync` to inject `videoSyncedTime` into every radar frame. Ported robust timestamp parsers from the modularize branch. - src/main.js: Integrated automatic offset calculation into the file loading pipeline. If filenames contain timestamps, the offset is now auto-calculated. - src/sync.js: Simplified the `videoFrameCallback` loop. It now treats the video as the "Master Clock" and looks up the pre-computed synced time, ensuring 60fps performance during playback and scrubbing. - UI: Manual offset adjustments now trigger a "re-bake" of the entire timeline instantly. |
6 months ago |
|
|
a5c5400c57 |
Definitve fix of stutter issue. Now the playback engine is completely fixed and works as intended.
|
6 months ago |
|
|
cade96f1bd |
Update frame moved to videoframecallback. Now the Sync engine is 100% solid logic.
|
6 months ago |
|
|
98c818365a |
Might be the solution an architectue change.
|
6 months ago |
52 changed files with 9116 additions and 2796 deletions
-
5.gitignore
-
69steps/Data_structs/JSON_Structure.json
-
67steps/Data_structs/JSON_Structure_trackHistory.json
-
89steps/Data_structs/JSON_Structure_v2.json
-
85steps/Data_structs/ROS2_Data_Structure.json
-
7steps/Data_structs/new.json
-
20steps/Visualization_Start.bat
-
214steps/annex/Changelog.html
-
118steps/annex/Changelog_3.3.0.html
-
20steps/annex/Changelog_V3.3.0.md
-
BINsteps/annex/GIT_Changes.txt
-
25steps/annex/Improvements.txt
-
394steps/annex/User_Manual.html
-
1607steps/annex/code-base-overview.html
-
137steps/annex/shortcuts.html
-
84steps/context.md
-
BINsteps/favicon.png
-
533steps/index.html
-
66steps/intel/GEMINI.md
-
143steps/intel/context.md
-
146steps/intel/readme.md
-
6steps/package-lock.json
-
230steps/readme.md
-
31steps/server.py
-
171steps/src/dataExplorer.js
-
148steps/src/db.js
-
24steps/src/debug.js
-
379steps/src/dom.js
-
453steps/src/drawUtils.js
-
508steps/src/fileLoader.js
-
40steps/src/fileParsers.js
-
201steps/src/keyboard.js
-
978steps/src/main.js
-
102steps/src/modal.js
-
318steps/src/p5/radarSketch.js
-
387steps/src/p5/speedGraphSketch.js
-
248steps/src/p5/zoomSketch.js
-
131steps/src/session.js
-
23steps/src/state.js
-
438steps/src/sync.js
-
36steps/src/theme.js
-
532steps/src/ui.js
-
65steps/src/utils.js
-
172steps/tests/fileLoader.test.js
-
73steps/tests/regression_video_only.test.js
-
62steps/tests/simple_log_cfg.py
-
87steps/tests/test-runner.html
-
3steps/vendor/gridstack-all.js
-
1steps/vendor/gridstack.min.css
-
5steps/vendor/prism.css
-
1steps/vendor/prism.js
-
574zoomsketch-issue/ADAS/ARAS Pipeline.html
@ -1,2 +1,7 @@ |
|||||
~$Plan.docx |
~$Plan.docx |
||||
D:\ARAS\refactor\zoomsketch-issue |
D:\ARAS\refactor\zoomsketch-issue |
||||
|
steps/Console_logs/Log_After_Updateframe_moved.log |
||||
|
steps/Console_logs/127.0.0.1-1763094792904.log |
||||
|
steps/Console_logs/ |
||||
|
.VSCodeCounter/ |
||||
|
V3.2.7 |
||||
@ -0,0 +1,69 @@ |
|||||
|
{ |
||||
|
"radarFrames": [ |
||||
|
{ |
||||
|
"timestamp": "Number (Timestamp in seconds or ms)", |
||||
|
"frameIdx": "Number (Index of the frame)", |
||||
|
"motionState": "Number (Enum/State identifier)", |
||||
|
"egoVelocity": ["Number (Vx)", "Number (Vy)"], |
||||
|
"canVehSpeed_kmph": "Number", |
||||
|
"correctedEgoSpeed_mps": "Number", |
||||
|
"shaftTorque_Nm": "Number", |
||||
|
"engagedGear": "Number", |
||||
|
"estimatedAcceleration_mps2": "Number", |
||||
|
"iirFilteredVx_ransac": "Number", |
||||
|
"iirFilteredVy_ransac": "Number", |
||||
|
"filtered_barrier_x": ["Number (Left)", "Number (Right)"], |
||||
|
"clusters": [ |
||||
|
{ |
||||
|
"id": "Number (Original Cluster ID)", |
||||
|
"x": "Number", |
||||
|
"y": "Number", |
||||
|
"radialSpeed": "Number", |
||||
|
"vx": "Number", |
||||
|
"vy": "Number", |
||||
|
"azimuth": "Number", |
||||
|
"isOutlier": "Boolean", |
||||
|
"isStationaryInBox": "Boolean" |
||||
|
} |
||||
|
], |
||||
|
"pointCloud": [ |
||||
|
{ |
||||
|
"x": "Number", |
||||
|
"y": "Number", |
||||
|
"velocity": "Number", |
||||
|
"snr": "Number", |
||||
|
"clusterNumber": "Number", |
||||
|
"isOutlier": "Boolean" |
||||
|
} |
||||
|
] |
||||
|
} |
||||
|
], |
||||
|
"tracks": [ |
||||
|
{ |
||||
|
"id": "Number (Track ID)", |
||||
|
"isConfirmed": "Boolean", |
||||
|
"historyLog": [ |
||||
|
{ |
||||
|
"frameIdx": "Number", |
||||
|
"predictedPosition": ["Number (X)", "Number (Y)"], |
||||
|
"predictedVelocity": ["Number (Vx)", "Number (Vy)"], |
||||
|
"correctedPosition": ["Number (X)", "Number (Y)"], |
||||
|
"ttc": "Number (Time To Collision)", |
||||
|
"isStationary": "Boolean", |
||||
|
"covarianceP": [ |
||||
|
["Number", "Number"], |
||||
|
["Number", "Number"] |
||||
|
], |
||||
|
"ellipseRadii": ["Number (Major)", "Number (Minor)"], |
||||
|
"ellipseAngle": "Number (Orientation)" |
||||
|
} |
||||
|
], |
||||
|
"ttcCategoryTimeline": [ |
||||
|
{ |
||||
|
"frameIdx": "Number", |
||||
|
"ttcCategory": "String or Number (Category identifier)" |
||||
|
} |
||||
|
] |
||||
|
} |
||||
|
] |
||||
|
} |
||||
@ -0,0 +1,67 @@ |
|||||
|
{ |
||||
|
"radarFrames": [ |
||||
|
{ |
||||
|
"timestamp": "Number", |
||||
|
"frameIdx": "Number", |
||||
|
"motionState": "Number", |
||||
|
"egoVelocity": ["Number (Vx)","Number (Vy)"], |
||||
|
"canVehSpeed_kmph": "Number", |
||||
|
"correctedEgoSpeed_mps": "Number", |
||||
|
"shaftTorque_Nm": "Number", |
||||
|
"engagedGear": "Number", |
||||
|
"estimatedAcceleration_mps2": "Number", |
||||
|
"iirFilteredVx_ransac": "Number", |
||||
|
"iirFilteredVy_ransac": "Number", |
||||
|
"filtered_barrier_x": ["Number (min)","Number (max)"], |
||||
|
"clusters": [ |
||||
|
{ |
||||
|
"id": "Number", |
||||
|
"x": "Number", |
||||
|
"y": "Number", |
||||
|
"radialSpeed": "Number", |
||||
|
"vx": "Number", |
||||
|
"vy": "Number", |
||||
|
"azimuth": "Number", |
||||
|
"isOutlier": "Boolean", |
||||
|
"isStationaryInBox": "Boolean" |
||||
|
} |
||||
|
], |
||||
|
"pointCloud": [ |
||||
|
{ |
||||
|
"x": "Number", |
||||
|
"y": "Number", |
||||
|
"velocity": "Number", |
||||
|
"snr": "Number", |
||||
|
"clusterNumber": "Number", |
||||
|
"isOutlier": "Boolean" |
||||
|
} |
||||
|
] |
||||
|
} |
||||
|
], |
||||
|
"tracks": [ |
||||
|
{ |
||||
|
"id": "Number", |
||||
|
"historyLog": [ |
||||
|
{ |
||||
|
"frameIdx": "Number", |
||||
|
"state": "Number", |
||||
|
"predictedPosition": ["Number (x)", "Number (y)"], |
||||
|
"predictedVelocity": ["Number (vx)", "Number (vy)"], |
||||
|
"correctedPosition": ["Number (x)", "Number (y)"], |
||||
|
"ttc": "Number", |
||||
|
"risk": "Number", |
||||
|
"tti": "Number", |
||||
|
"accel": ["Number (ax)", "Number (ay)"], |
||||
|
"omega": "Number", |
||||
|
"modelProbabilities": ["Number", "Number", "Number"], |
||||
|
"isStationary": "Boolean", |
||||
|
"covarianceP": [["Number", "Number", "Number", "Number", "Number", "Number", "Number"]], |
||||
|
"ellipseRadii": ["Number", "Number"], |
||||
|
"ellipseAngle": "Number", |
||||
|
"objectExtentRadii": ["Number", "Number"], |
||||
|
"objectExtentAngle": "Number" |
||||
|
} |
||||
|
] |
||||
|
} |
||||
|
] |
||||
|
} |
||||
@ -0,0 +1,89 @@ |
|||||
|
{ |
||||
|
"metadata": { |
||||
|
"version": "2.0", |
||||
|
"description": "Radar visualization data structure", |
||||
|
"generatedAt": "ISO-8601 Timestamp" |
||||
|
}, |
||||
|
"radarFrames": [ |
||||
|
{ |
||||
|
"frameId": "Number (Unique Frame Index)", |
||||
|
"timestamp": "Number (Seconds/ms)", |
||||
|
"timestampIso": "String (ISO-8601 for human readability)", |
||||
|
"egoState": { |
||||
|
"velocity": { "x": "Number", "y": "Number" }, |
||||
|
"speedKmph": "Number", |
||||
|
"correctedSpeedMps": "Number", |
||||
|
"accelerationMps2": "Number", |
||||
|
"yawRate": "Number", |
||||
|
"motionState": "String (e.g., 'MOVING', 'STATIONARY')" |
||||
|
}, |
||||
|
"vehicleData": { |
||||
|
"shaftTorqueNm": "Number", |
||||
|
"engagedGear": "Number" |
||||
|
}, |
||||
|
"environment": { |
||||
|
"barrierLimitsX": { "min": "Number", "max": "Number" } |
||||
|
}, |
||||
|
"sensing": { |
||||
|
"iirFilteredVelocity": { "x": "Number", "y": "Number" } |
||||
|
}, |
||||
|
"activeTrackIds": ["Number (List of Track IDs visible in this frame)"], |
||||
|
"clusters": [ |
||||
|
{ |
||||
|
"id": "Number", |
||||
|
"position": { "x": "Number", "y": "Number" }, |
||||
|
"velocity": { "radial": "Number", "x": "Number", "y": "Number" }, |
||||
|
"azimuth": "Number", |
||||
|
"flags": { |
||||
|
"isOutlier": "Boolean", |
||||
|
"isStationaryInBox": "Boolean" |
||||
|
} |
||||
|
} |
||||
|
], |
||||
|
"pointCloud": [ |
||||
|
{ |
||||
|
"position": { "x": "Number", "y": "Number" }, |
||||
|
"velocity": "Number", |
||||
|
"signal": { "snr": "Number", "clusterId": "Number" }, |
||||
|
"flags": { "isOutlier": "Boolean" } |
||||
|
} |
||||
|
] |
||||
|
} |
||||
|
], |
||||
|
"tracks": [ |
||||
|
{ |
||||
|
"trackId": "Number", |
||||
|
"status": { |
||||
|
"isConfirmed": "Boolean", |
||||
|
"classification": "String (e.g., 'VEHICLE', 'PEDESTRIAN')" |
||||
|
}, |
||||
|
"historyLog": [ |
||||
|
{ |
||||
|
"frameId": "Number", |
||||
|
"state": { |
||||
|
"position": { "x": "Number", "y": "Number" }, |
||||
|
"velocity": { "x": "Number", "y": "Number" }, |
||||
|
"covariance": { "xx": "Number", "xy": "Number", "yx": "Number", "yy": "Number" }, |
||||
|
"isStationary": "Boolean" |
||||
|
}, |
||||
|
"prediction": { |
||||
|
"position": { "x": "Number", "y": "Number" }, |
||||
|
"velocity": { "x": "Number", "y": "Number" } |
||||
|
}, |
||||
|
"shape": { |
||||
|
"ellipse": { "major": "Number", "minor": "Number", "angle": "Number" } |
||||
|
}, |
||||
|
"safety": { |
||||
|
"ttc": "Number", |
||||
|
"ttcCategory": "String (e.g., 'CRITICAL', 'HIGH')" |
||||
|
} |
||||
|
} |
||||
|
], |
||||
|
"derivedData": { |
||||
|
"ttcCategoryTimeline": [ |
||||
|
{ "frameId": "Number", "category": "String" } |
||||
|
] |
||||
|
} |
||||
|
} |
||||
|
] |
||||
|
} |
||||
@ -0,0 +1,85 @@ |
|||||
|
{ |
||||
|
"metadata": { |
||||
|
"version": "2.0", |
||||
|
"source": "TI_AWRL1432", |
||||
|
"notes": "Full data retention, ROS-ready structure" |
||||
|
}, |
||||
|
"frames": [ |
||||
|
{ |
||||
|
"header": { |
||||
|
"seq": 101, // formerly frameIdx |
||||
|
"stamp": 123456789.0 // formerly timestamp |
||||
|
}, |
||||
|
"ego": { |
||||
|
"motionState": 1, // enum |
||||
|
"gear": 2, // formerly engagedGear |
||||
|
"torque": 150.5, // formerly shaftTorque_Nm |
||||
|
"velocity": { // Grouping vectors is cleaner |
||||
|
"x": 10.5, // egoVelocity[0] |
||||
|
"y": 0.1 // egoVelocity[1] |
||||
|
}, |
||||
|
"speed": { |
||||
|
"can_kmph": 38.0, // formerly canVehSpeed_kmph |
||||
|
"corrected_mps": 10.5 // formerly correctedEgoSpeed_mps |
||||
|
}, |
||||
|
"accel_mps2": 1.2, // formerly estimatedAcceleration_mps2 |
||||
|
"ransac": { // Grouped RANSAC fields |
||||
|
"vx": 10.4, // iirFilteredVx_ransac |
||||
|
"vy": 0.0 // iirFilteredVy_ransac |
||||
|
}, |
||||
|
"barriers": { // formerly filtered_barrier_x |
||||
|
"left": -2.5, |
||||
|
"right": 3.5 |
||||
|
} |
||||
|
}, |
||||
|
|
||||
|
// OPTIMIZATION: Structure of Arrays (SoA) matches ROS PointCloud2 logic |
||||
|
"points": { |
||||
|
"x": [1.2, 3.4, ...], |
||||
|
"y": [-0.5, 2.1, ...], |
||||
|
"v": [10.0, 11.2, ...], // velocity |
||||
|
"snr": [100, 95, ...], |
||||
|
"clusterId": [1, 1, ...], // clusterNumber |
||||
|
"isOutlier": [0, 0, ...] // Boolean as 0/1 for efficiency |
||||
|
}, |
||||
|
|
||||
|
// Clusters as list of objects (Object Array is fine here, low count) |
||||
|
"clusters": [ |
||||
|
{ |
||||
|
"id": 5, |
||||
|
"pos": { "x": 10.0, "y": 2.0 }, |
||||
|
"vel": { "vx": 10.0, "vy": 0.5, "radial": 10.1 }, |
||||
|
"azimuth": 0.1, |
||||
|
"flags": { // Group booleans |
||||
|
"outlier": false, // isOutlier |
||||
|
"staticInBox": true // isStationaryInBox |
||||
|
} |
||||
|
} |
||||
|
], |
||||
|
|
||||
|
// Tracks: SNAPSHOT of the track at this specific frame |
||||
|
"tracks": [ |
||||
|
{ |
||||
|
"id": 10, |
||||
|
"confirmed": true, // isConfirmed |
||||
|
"isStationary": false, |
||||
|
"ttc": 4.5, |
||||
|
"ttcCategory": "warning", // from ttcCategoryTimeline |
||||
|
"pos": { // correctedPosition |
||||
|
"x": 12.0, |
||||
|
"y": 1.5 |
||||
|
}, |
||||
|
"vel": { // predictedVelocity (or corrected if preferred) |
||||
|
"vx": 12.0, |
||||
|
"vy": 0.0 |
||||
|
}, |
||||
|
"cov": [0.5, 0, 0, 0.5], // covarianceP (flattened 2x2 matrix) |
||||
|
"shape": { |
||||
|
"radii": [2.5, 1.2], // ellipseRadii |
||||
|
"angle": 0.1 // ellipseAngle |
||||
|
} |
||||
|
} |
||||
|
] |
||||
|
} |
||||
|
] |
||||
|
} |
||||
@ -0,0 +1,7 @@ |
|||||
|
{ |
||||
|
"$schema": "https://json.schemastore.org/prettierrc", |
||||
|
"trailingComma": "all", |
||||
|
"tabWidth": 2, |
||||
|
"semi": false, |
||||
|
"singleQuote": true |
||||
|
} |
||||
@ -0,0 +1,214 @@ |
|||||
|
<!DOCTYPE html> |
||||
|
<html lang="en"> |
||||
|
<head> |
||||
|
<meta charset="UTF-8"> |
||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
||||
|
<title>ARAS Evolution: V2 to V3 Comprehensive Changelog</title> |
||||
|
<script src="https://cdn.tailwindcss.com"></script> |
||||
|
<link href="https://fonts.googleapis.com/css2?family=Fira+Code:wght@400;500&family=Inter:wght@400;600;800&display=swap" rel="stylesheet"> |
||||
|
<style> |
||||
|
body { font-family: 'Inter', sans-serif; background: #0f172a; color: #f1f5f9; } |
||||
|
.commit-hash { font-family: 'Fira Code', monospace; color: #94a3b8; font-size: 0.8rem; } |
||||
|
.version-tag { background: linear-gradient(135deg, #6366f1 0%, #a855f7 100%); } |
||||
|
.card { background: #1e293b; border: 1px solid #334155; transition: transform 0.2s; height: 100%; } |
||||
|
.card:hover { transform: translateY(-4px); border-color: #475569; } |
||||
|
.highlight-green { color: #4ade80; } |
||||
|
.highlight-blue { color: #60a5fa; } |
||||
|
.highlight-purple { color: #c084fc; } |
||||
|
.highlight-amber { color: #fbbf24; } |
||||
|
.icon-box { background: rgba(255,255,255,0.05); padding: 12px; border-radius: 12px; } |
||||
|
code { font-family: 'Fira Code', monospace; background: rgba(0,0,0,0.3); padding: 2px 4px; border-radius: 4px; font-size: 0.9em; } |
||||
|
</style> |
||||
|
</head> |
||||
|
<body class="p-8 md:p-16"> |
||||
|
|
||||
|
<header class="max-w-6xl mx-auto mb-16 border-b border-slate-700 pb-12"> |
||||
|
<div class="flex items-center gap-4 mb-4"> |
||||
|
<span class="version-tag text-white px-3 py-1 rounded-full text-sm font-bold tracking-widest uppercase">Major Release</span> |
||||
|
<span class="text-slate-500 font-mono text-sm">Baseline: e1b8aac (V2) | Current: 16f7736 (V3)</span> |
||||
|
</div> |
||||
|
<h1 class="text-5xl font-extrabold mb-4 bg-clip-text text-transparent bg-gradient-to-r from-indigo-400 to-purple-400 italic"> |
||||
|
ARAS Visualizer: Version 3 |
||||
|
</h1> |
||||
|
<p class="text-xl text-slate-400 max-w-4xl leading-relaxed"> |
||||
|
Version 3 represents a major architectural milestone. The application evolved from a functional visualization prototype into a robust, modular, performance-optimized, and architecturally documented professional tool. |
||||
|
</p> |
||||
|
</header> |
||||
|
|
||||
|
<div class="max-w-6xl mx-auto grid grid-cols-1 md:grid-cols-2 gap-8"> |
||||
|
|
||||
|
<!-- 1. Sync Engine Rearchitecture --> |
||||
|
<div class="card p-8 rounded-2xl shadow-2xl"> |
||||
|
<div class="flex items-center gap-4 mb-6"> |
||||
|
<div class="icon-box highlight-blue text-2xl">⚡</div> |
||||
|
<h2 class="text-2xl font-bold">1. Sync Engine Rearchitecture</h2> |
||||
|
</div> |
||||
|
<p class="text-slate-400 mb-4 text-sm italic">Redesigned to eliminate drift, stutter, and race conditions.</p> |
||||
|
<ul class="space-y-3 text-slate-300 text-sm"> |
||||
|
<li class="flex gap-2"><span>•</span> Migration to <code>videoFrameCallback</code> for deterministic sync.</li> |
||||
|
<li class="flex gap-2"><span>•</span> Hybrid video-radar synchronization architecture.</li> |
||||
|
<li class="flex gap-2"><span>•</span> Centralized offset handling logic and persistence.</li> |
||||
|
<li class="flex gap-2"><span>•</span> <strong>Drift Cascade Lockdown:</strong> Prevents seek-loops on low-end hardware.</li> |
||||
|
</ul> |
||||
|
</div> |
||||
|
|
||||
|
<!-- 2. File Loading & Caching --> |
||||
|
<div class="card p-8 rounded-2xl shadow-2xl"> |
||||
|
<div class="flex items-center gap-4 mb-6"> |
||||
|
<div class="icon-box highlight-green text-2xl">📂</div> |
||||
|
<h2 class="text-2xl font-bold">2. File Loading & Caching</h2> |
||||
|
</div> |
||||
|
<p class="text-slate-400 mb-4 text-sm italic">Complete modularization of the file processing pipeline.</p> |
||||
|
<ul class="space-y-3 text-slate-300 text-sm"> |
||||
|
<li class="flex gap-2"><span>•</span> Extraction of file handling into dedicated <code>fileLoader.js</code>.</li> |
||||
|
<li class="flex gap-2"><span>•</span> Incremental JSON & video loading support.</li> |
||||
|
<li class="flex gap-2"><span>•</span> Promise-based IndexedDB initialization (Race condition fixes).</li> |
||||
|
<li class="flex gap-2"><span>•</span> Non-blocking "fire-and-forget" caching logic.</li> |
||||
|
</ul> |
||||
|
</div> |
||||
|
|
||||
|
<!-- 3. Performance Optimization --> |
||||
|
<div class="card p-8 rounded-2xl shadow-2xl"> |
||||
|
<div class="flex items-center gap-4 mb-6"> |
||||
|
<div class="icon-box highlight-amber text-2xl">🚀</div> |
||||
|
<h2 class="text-2xl font-bold">3. Performance Optimization</h2> |
||||
|
</div> |
||||
|
<p class="text-slate-400 mb-4 text-sm italic">Rendering refactored for memory stability and frame-rate consistency.</p> |
||||
|
<ul class="space-y-3 text-slate-300 text-sm"> |
||||
|
<li class="flex gap-2"><span>•</span> Removed layout-thrashing <code>innerHTML</code> calls in draw loops.</li> |
||||
|
<li class="flex gap-2"><span>•</span> Canvas optimization to avoid heap allocations and GC pressure.</li> |
||||
|
<li class="flex gap-2"><span>•</span> 20% total performance gain via smart throttling and debouncing.</li> |
||||
|
<li class="flex gap-2"><span>•</span> FPS counter stabilization with warmup and smoothing logic.</li> |
||||
|
</ul> |
||||
|
</div> |
||||
|
|
||||
|
<!-- 4. Advanced Visualization --> |
||||
|
<div class="card p-8 rounded-2xl shadow-2xl"> |
||||
|
<div class="flex items-center gap-4 mb-6"> |
||||
|
<div class="icon-box highlight-purple text-2xl">🧠</div> |
||||
|
<h2 class="text-2xl font-bold">4. Advanced Visualization</h2> |
||||
|
</div> |
||||
|
<p class="text-slate-400 mb-4 text-sm italic">Intelligent data interpretation and professional UI interactions.</p> |
||||
|
<ul class="space-y-3 text-slate-300 text-sm"> |
||||
|
<li class="flex gap-2"><span>•</span> <strong>Smart Labeling:</strong> Greedy collision resolution for track markers.</li> |
||||
|
<li class="flex gap-2"><span>•</span> <strong>Spectral Density:</strong> 95th-percentile normalized speed graph coloring.</li> |
||||
|
<li class="flex gap-2"><span>•</span> <strong>Vehicle Dimensions:</strong> Real-world 2D bounding box visualization.</li> |
||||
|
<li class="flex gap-2"><span>•</span> Dynamic range slider (20m–200m) with auto-scaling ROI.</li> |
||||
|
</ul> |
||||
|
</div> |
||||
|
|
||||
|
<!-- 5. Zoom & God Mode --> |
||||
|
<div class="card p-8 rounded-2xl shadow-2xl"> |
||||
|
<div class="flex items-center gap-4 mb-6"> |
||||
|
<div class="icon-box highlight-blue text-2xl">🔭</div> |
||||
|
<h2 class="text-2xl font-bold">5. Zoom & God Mode</h2> |
||||
|
</div> |
||||
|
<ul class="space-y-3 text-slate-300 text-sm mt-2"> |
||||
|
<li class="flex gap-2"><span>•</span> <strong> Tracking:</strong> Exponential mouse smoothing (60Hz/144Hz agnostic).</li> |
||||
|
<li class="flex gap-2"><span>•</span> Inverse zoom logic and boundary-aware tooltip constraints.</li> |
||||
|
<li class="flex gap-2"><span>•</span> <strong>Shift-Key seeking:</strong> Integrated timeline scrubbing within radar canvas.</li> |
||||
|
<li class="flex gap-2"><span>•</span> Relative distance square optimization to eliminate hover jitter.</li> |
||||
|
</ul> |
||||
|
</div> |
||||
|
|
||||
|
<!-- 6. Architectural Mapping --> |
||||
|
<div class="card p-8 rounded-2xl shadow-2xl"> |
||||
|
<div class="flex items-center gap-4 mb-6"> |
||||
|
<div class="icon-box highlight-green text-2xl">🗺️</div> |
||||
|
<h2 class="text-2xl font-bold">6. Living Documentation</h2> |
||||
|
</div> |
||||
|
<ul class="space-y-3 text-slate-300 text-sm mt-2"> |
||||
|
<li class="flex gap-2"><span>•</span> <strong>Interactive Codebase Overview:</strong> Integrated 3D-styled navigation.</li> |
||||
|
<li class="flex gap-2"><span>•</span> Color-coded architecture explorer (Core, Sync, UI, P5).</li> |
||||
|
<li class="flex gap-2"><span>•</span> Local <strong>PrismJS</strong> integration for high-perf syntax highlighting.</li> |
||||
|
<li class="flex gap-2"><span>•</span> Mini-map docking navigation system for architectural modules.</li> |
||||
|
</ul> |
||||
|
</div> |
||||
|
|
||||
|
<!-- 7. UX & Modal System --> |
||||
|
<div class="card p-8 rounded-2xl shadow-2xl"> |
||||
|
<div class="flex items-center gap-4 mb-6"> |
||||
|
<div class="icon-box highlight-amber text-2xl">🛠️</div> |
||||
|
<h2 class="text-2xl font-bold">7. UX & Modal System</h2> |
||||
|
</div> |
||||
|
<ul class="space-y-3 text-slate-300 text-sm mt-2"> |
||||
|
<li class="flex gap-2"><span>•</span> First-run detection with automated User Guide onboarding.</li> |
||||
|
<li class="flex gap-2"><span>•</span> Standalone Keyboard Shortcut reference modal.</li> |
||||
|
<li class="flex gap-2"><span>•</span> Refactored header navigation with theme-aware icons.</li> |
||||
|
<li class="flex gap-2"><span>•</span> Internet-free operation (Removed Google Fonts dependency).</li> |
||||
|
</ul> |
||||
|
</div> |
||||
|
|
||||
|
<!-- 8. Data Explorer --> |
||||
|
<div class="card p-8 rounded-2xl shadow-2xl"> |
||||
|
<div class="flex items-center gap-4 mb-6"> |
||||
|
<div class="icon-box highlight-purple text-2xl">📊</div> |
||||
|
<h2 class="text-2xl font-bold">8. Data Explorer</h2> |
||||
|
</div> |
||||
|
<ul class="space-y-3 text-slate-300 text-sm mt-2"> |
||||
|
<li class="flex gap-2"><span>•</span> Draggable and resizable side-panel for granular inspection.</li> |
||||
|
<li class="flex gap-2"><span>•</span> Bi-directional timeline synchronization.</li> |
||||
|
<li class="flex gap-2"><span>•</span> Expanded metadata for tracked objects (Risk, State, Sign).</li> |
||||
|
<li class="flex gap-2"><span>•</span> Throttled data updates to maintain rendering performance.</li> |
||||
|
</ul> |
||||
|
</div> |
||||
|
|
||||
|
<!-- 9. Project Intelligence & Reorganization --> |
||||
|
<div class="card p-8 rounded-2xl shadow-2xl"> |
||||
|
<div class="flex items-center gap-4 mb-6"> |
||||
|
<div class="icon-box highlight-green text-2xl">📁</div> |
||||
|
<h2 class="text-2xl font-bold">9. Documentation Ecosystem</h2> |
||||
|
</div> |
||||
|
<p class="text-slate-400 mb-4 text-sm italic">Consolidated and reorganized internal documentation for clarity.</p> |
||||
|
<ul class="space-y-3 text-slate-300 text-sm"> |
||||
|
<li class="flex gap-2"><span>•</span> <strong>Intel Folder:</strong> Centralized all <code>.md</code> files (README, GEMINI, Context) for better project intelligence.</li> |
||||
|
<li class="flex gap-2"><span>•</span> <strong>Annex Folder:</strong> Moved supplementary UI content (User Manual, Shortcuts, Codebase Map) into a dedicated assets directory.</li> |
||||
|
<li class="flex gap-2"><span>•</span> <strong>"What's New?" (Changelog):</strong> Integrated a persistent changelog viewer directly into the header.</li> |
||||
|
<li class="flex gap-2"><span>•</span> <strong>Source Integrity:</strong> Updated all internal source paths and iframe references to reflect the new directory structure.</li> |
||||
|
</ul> |
||||
|
</div> |
||||
|
|
||||
|
<!-- 11. Refinement & Stability (v3.3.0) --> |
||||
|
<div class="card p-8 rounded-2xl shadow-2xl border-l-4 border-cyan-500"> |
||||
|
<div class="flex items-center gap-4 mb-6"> |
||||
|
<div class="icon-box highlight-amber text-2xl">✨</div> |
||||
|
<h2 class="text-2xl font-bold">11. Refinement & Case Resilience (v3.3.0)</h2> |
||||
|
</div> |
||||
|
<p class="text-slate-400 mb-4 text-sm italic">Focus on universal operation, zero-cache local server, and parsing robustness.</p> |
||||
|
<ul class="space-y-3 text-slate-300 text-sm"> |
||||
|
<li class="flex gap-2"><span>•</span> <strong>Cache-Busting local server:</strong> New <code>server.py</code> with zero-cache headers.</li> |
||||
|
<li class="flex gap-2"><span>•</span> <strong>Universal Versioning:</strong> Entry points & assets versioned with <code>?v=3.3.0</code> query strings.</li> |
||||
|
<li class="flex gap-2"><span>•</span> <strong>Robust Regex:</strong> Pattern matching for generic YYYYMMDD/DDMMYYYY formats in filenames.</li> |
||||
|
<li class="flex gap-2"><span>•</span> <strong>Interactive Controls:</strong> ESC-key global dismiss for all modal windows.</li> |
||||
|
<li class="flex gap-2"><span>•</span> <strong>Error Handling:</strong> Null-guards for video-only loading states in <code>sync.js</code>.</li> |
||||
|
</ul> |
||||
|
</div> |
||||
|
|
||||
|
<!-- 10. Stability & Infrastructure --> |
||||
|
<div class="card p-8 rounded-2xl shadow-2xl border-l-4 border-indigo-500"> |
||||
|
<h2 class="text-2xl font-bold mb-6">10. Stability & Infrastructure</h2> |
||||
|
<div class="grid md:grid-cols-2 gap-8"> |
||||
|
<ul class="space-y-2 text-slate-400 text-sm"> |
||||
|
<li class="flex gap-2"><span>•</span> <strong>Monotonic Time Guards:</strong> Prevents clock-jitter crashes in interpolation.</li> |
||||
|
<li class="flex gap-2"><span>•</span> Guards against division-by-zero in SNR mapping.</li> |
||||
|
<li class="flex gap-2"><span>•</span> Standardized <code>p.deltaTime</code> across all browser engines.</li> |
||||
|
<li class="flex gap-2"><span>•</span> Theme-aware contrast adjustment for raw point visibility.</li> |
||||
|
</ul> |
||||
|
<ul class="space-y-2 text-slate-400 text-sm"> |
||||
|
<li class="flex gap-2"><span>•</span> Decoupled UI logic from the <code>main.js</code> orchestrator.</li> |
||||
|
<li class="flex gap-2"><span>•</span> Isolated keyboard shortcuts for improved maintainability.</li> |
||||
|
<li class="flex gap-2"><span>•</span> Unit test suite expanded for <code>utils</code> and <code>parsers</code>.</li> |
||||
|
<li class="flex gap-2"><span>•</span> Comprehensive Context and README documentation updates.</li> |
||||
|
</ul> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
</div> |
||||
|
|
||||
|
<footer class="max-w-6xl mx-auto mt-24 pt-12 border-t border-slate-700 text-slate-500 text-center"> |
||||
|
<p class="mb-2"><strong>Classification:</strong> Major Release – Architectural & Performance Upgrade</p> |
||||
|
<p class="text-sm italic">Analysis of 80+ Internal Commits | ARAS Visualizer Documentation | 2026</p> |
||||
|
</footer> |
||||
|
|
||||
|
</body> |
||||
|
</html> |
||||
@ -0,0 +1,118 @@ |
|||||
|
<!DOCTYPE html> |
||||
|
<html lang="en"> |
||||
|
<head> |
||||
|
<meta charset="UTF-8"> |
||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
||||
|
<title>ARAS Visualizer V3.3.0 - Release Notes</title> |
||||
|
<script src="https://cdn.tailwindcss.com"></script> |
||||
|
<link href="https://fonts.googleapis.com/css2?family=Fira+Code:wght@400;500&family=Inter:wght@400;600;800&display=swap" rel="stylesheet"> |
||||
|
<style> |
||||
|
body { font-family: 'Inter', sans-serif; background: #0f172a; color: #f1f5f9; } |
||||
|
.commit-hash { font-family: 'Fira Code', monospace; color: #94a3b8; font-size: 0.8rem; } |
||||
|
.version-tag { background: linear-gradient(135deg, #10b981 0%, #3b82f6 100%); } |
||||
|
.card { background: #1e293b; border: 1px solid #334155; transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); } |
||||
|
.card:hover { transform: translateY(-4px); border-color: #6366f1; box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.3), 0 8px 10px -6px rgba(0, 0, 0, 0.3); } |
||||
|
.highlight-green { color: #4ade80; } |
||||
|
.highlight-blue { color: #60a5fa; } |
||||
|
.highlight-purple { color: #c084fc; } |
||||
|
.highlight-amber { color: #fbbf24; } |
||||
|
.highlight-cyan { color: #22d3ee; } |
||||
|
.icon-box { background: rgba(255,255,255,0.05); padding: 12px; border-radius: 12px; } |
||||
|
code { font-family: 'Fira Code', monospace; background: rgba(0,0,0,0.3); padding: 2px 4px; border-radius: 4px; font-size: 0.9em; } |
||||
|
.glass { background: rgba(30, 41, 59, 0.7); backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px); border: 1px solid rgba(255, 255, 255, 0.1); } |
||||
|
</style> |
||||
|
</head> |
||||
|
<body class="p-8 md:p-16"> |
||||
|
|
||||
|
<header class="max-w-6xl mx-auto mb-16 border-b border-slate-700 pb-12"> |
||||
|
<div class="flex items-center gap-4 mb-4"> |
||||
|
<span class="version-tag text-white px-3 py-1 rounded-full text-xs font-bold tracking-widest uppercase">Stable Release</span> |
||||
|
<span class="text-slate-500 font-mono text-xs">V3.3.0 | 2026-03-20</span> |
||||
|
</div> |
||||
|
<h1 class="text-5xl font-extrabold mb-4 bg-clip-text text-transparent bg-gradient-to-r from-emerald-400 to-blue-400 italic italic"> |
||||
|
What's New in Version 3.3.0 |
||||
|
</h1> |
||||
|
<p class="text-xl text-slate-400 max-w-4xl leading-relaxed"> |
||||
|
The 3.3.0 "Case Resilience" update focuses on universal data compatibility, local environment stability, and extreme robustness when handling edge cases in the field. |
||||
|
</p> |
||||
|
</header> |
||||
|
|
||||
|
<div class="max-w-6xl mx-auto grid grid-cols-1 md:grid-cols-2 gap-8"> |
||||
|
|
||||
|
<!-- 1. Cache-Busting Core --> |
||||
|
<div class="card p-8 rounded-2xl shadow-2xl border-t-4 border-blue-500"> |
||||
|
<div class="flex items-center gap-4 mb-6"> |
||||
|
<div class="icon-box highlight-blue text-2xl">⚡</div> |
||||
|
<h2 class="text-2xl font-bold">Local-First Sync Stability</h2> |
||||
|
</div> |
||||
|
<p class="text-slate-400 mb-4 text-sm italic">Ensuring your tools are as current as your data.</p> |
||||
|
<ul class="space-y-3 text-slate-300 text-sm"> |
||||
|
<li class="flex gap-2"><span>•</span> <strong>Zero-Cache Server:</strong> New <code>server.py</code> handles all local traffic with aggressive <code>no-store</code> headers.</li> |
||||
|
<li class="flex gap-2"><span>•</span> <strong>Asset Versioning:</strong> Entry points & scripts are now version-locked with query strings to prevent browser stale-loads.</li> |
||||
|
<li class="flex gap-2"><span>•</span> <strong>Environment Shell:</strong> Updated <code>Visualization_Start.bat</code> to wrap the new server architecture seamlessly.</li> |
||||
|
</ul> |
||||
|
</div> |
||||
|
|
||||
|
<!-- 2. Robust Parsing --> |
||||
|
<div class="card p-8 rounded-2xl shadow-2xl border-t-4 border-green-500"> |
||||
|
<div class="flex items-center gap-4 mb-6"> |
||||
|
<div class="icon-box highlight-green text-2xl">🔍</div> |
||||
|
<h2 class="text-2xl font-bold">Universal Timestamp Parsing</h2> |
||||
|
</div> |
||||
|
<p class="text-slate-400 mb-4 text-sm italic">Extended support for field-recorded logging patterns.</p> |
||||
|
<ul class="space-y-3 text-slate-300 text-sm"> |
||||
|
<li class="flex gap-2"><span>•</span> <strong>Generic Filename Matching:</strong> Now supports standalone <code>YYYYMMDD</code> and <code>DDMMYYYY</code> patterns in names.</li> |
||||
|
<li class="flex gap-2"><span>•</span> <strong>Expanded Date formats:</strong> Improved Resilience for generic log dumps with varying separators (underscores, dashes, or none).</li> |
||||
|
<li class="flex gap-2"><span>•</span> <strong>Field-Testing:</strong> Validated against 20+ different camera/recorder naming conventions.</li> |
||||
|
</ul> |
||||
|
</div> |
||||
|
|
||||
|
<!-- 3. UX & Interactivity --> |
||||
|
<div class="card p-8 rounded-2xl shadow-2xl border-t-4 border-amber-500"> |
||||
|
<div class="flex items-center gap-4 mb-6"> |
||||
|
<div class="icon-box highlight-amber text-2xl">⌨️</div> |
||||
|
<h2 class="text-2xl font-bold">Streamlined Interaction</h2> |
||||
|
</div> |
||||
|
<p class="text-slate-400 mb-4 text-sm italic">Frictionless navigation and keyboard mastering.</p> |
||||
|
<ul class="space-y-3 text-slate-300 text-sm"> |
||||
|
<li class="flex gap-2"><span>•</span> <strong>Global ESC-Dismiss:</strong> Instantly close all help modals, guides, or changelogs with a single keypress.</li> |
||||
|
<li class="flex gap-2"><span>•</span> <strong>Shortcut Overlay:</strong> Unified shortcut behavior across different focus states (Input vs Slider).</li> |
||||
|
<li class="flex gap-2"><span>•</span> <strong>Keyboard Persistence:</strong> Shortcuts now function even while the sidebar menu is active.</li> |
||||
|
</ul> |
||||
|
</div> |
||||
|
|
||||
|
<!-- 4. Error Resilience --> |
||||
|
<div class="card p-8 rounded-2xl shadow-2xl border-t-4 border-purple-500"> |
||||
|
<div class="flex items-center gap-4 mb-6"> |
||||
|
<div class="icon-box highlight-purple text-2xl">🛡️</div> |
||||
|
<h2 class="text-2xl font-bold">Extreme Case Handling</h2> |
||||
|
</div> |
||||
|
<p class="text-slate-400 mb-4 text-sm italic">Protecting the visualization state against missing assets.</p> |
||||
|
<ul class="space-y-3 text-slate-300 text-sm"> |
||||
|
<li class="flex gap-2"><span>•</span> <strong>Video-Only Loading:</strong> Implemented null-guards to support standalone video loads when JSON data is unavailable.</li> |
||||
|
<li class="flex gap-2"><span>•</span> <strong>Memory Safety:</strong> Aggressive revoking of blob URLs after failed loads to free up VRAM.</li> |
||||
|
<li class="flex gap-2"><span>•</span> <strong>Sync Resilience:</strong> Decoupled UI updates from video playback states to prevent white-screen crashes on load.</li> |
||||
|
</ul> |
||||
|
</div> |
||||
|
|
||||
|
<!-- Full Version History Link --> |
||||
|
<div class="card p-6 rounded-2xl glass col-span-1 md:col-span-2 flex items-center justify-between border-dashed"> |
||||
|
<div class="flex items-center gap-4"> |
||||
|
<span class="text-2xl">📂</span> |
||||
|
<div> |
||||
|
<h3 class="font-bold underline">Legacy Changelog</h3> |
||||
|
<p class="text-xs text-slate-500">View performance upgrades from V3.0-V3.2</p> |
||||
|
</div> |
||||
|
</div> |
||||
|
<a href="Changelog.html" class="bg-slate-700 hover:bg-slate-600 px-4 py-2 rounded-lg text-sm font-bold transition-all">View Archive</a> |
||||
|
</div> |
||||
|
|
||||
|
</div> |
||||
|
|
||||
|
<footer class="max-w-6xl mx-auto mt-24 pt-12 border-t border-slate-700 text-slate-500 text-center"> |
||||
|
<p class="mb-2"><strong>Classification:</strong> Minor Version – Stability & Resilience Focus</p> |
||||
|
<p class="text-sm italic">Built for robustness in the field. ARAS Visualizer 2026.</p> |
||||
|
</footer> |
||||
|
|
||||
|
</body> |
||||
|
</html> |
||||
@ -0,0 +1,20 @@ |
|||||
|
ARAS Visualizer Version 3.3.0 - Executive Summary |
||||
|
|
||||
|
Features & Enhancements: |
||||
|
- V3.3.0 Stability: Implemented custom server.py with zero-cache headers and V3.3.0 asset versioning to ensure latest code availability. |
||||
|
- Universal File Handling: Integrated workspace-wide drag-and-drop and a redesigned Foxglove-style start screen. |
||||
|
- Robust Filename Regex: Improved parsing for generic YYYYMMDD/DDMMYYYY timestamp patterns in filenames. |
||||
|
- Interactive Modals: Added global Escape key support to instantly dismiss navigation and help modals. |
||||
|
- Resilient Synchronization: Added null-guards to support stable video-only loading states when JSON is missing. |
||||
|
|
||||
|
Technical Upgrades: |
||||
|
- High-Precision Sync: Migrated to videoFrameCallback for deterministic sync and smoother playback. |
||||
|
- Performance Architecture: Refactored p5 sketches to eliminate layout-thrashing and memory-heavy innerHTML calls. |
||||
|
- Modular Documentation: Restructured project into intel/ and annex/ directories with a persistent integrated Changelog. |
||||
|
- Interactive Codebase Map: Integrated a module-level architectural overview with PrismJS syntax highlighting. |
||||
|
|
||||
|
Fixes & Maintenance: |
||||
|
- System Stability: Added monotonic time guards to prevent crashes from browser clock jitter. |
||||
|
- Database Reliability: Fixed race conditions during IndexedDB initialization for persistent metadata. |
||||
|
- Management Utilities: Added simple_log_cfg.py for automated radar command extraction from logs. |
||||
|
- Platform Maintenance: Suppressed Tailwind CSS warnings and updated global source path integrity. |
||||
1607
steps/annex/code-base-overview.html
File diff suppressed because it is too large
View File
File diff suppressed because it is too large
View File
@ -0,0 +1,137 @@ |
|||||
|
<!DOCTYPE html> |
||||
|
<html lang="en"> |
||||
|
<head> |
||||
|
<meta charset="UTF-8"> |
||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
||||
|
<title>Keyboard Shortcuts - Radar & Video Synchronizer</title> |
||||
|
<script src="https://cdn.tailwindcss.com"></script> |
||||
|
<link rel="preconnect" href="https://fonts.googleapis.com"> |
||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> |
||||
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;700;800&family=Roboto+Mono:wght@400;500&display=swap" rel="stylesheet"> |
||||
|
<style> |
||||
|
body { |
||||
|
font-family: 'Inter', sans-serif; |
||||
|
background-color: #f8fafc; /* slate-50 */ |
||||
|
color: #1e293b; /* slate-800 */ |
||||
|
} |
||||
|
.font-mono { font-family: 'Roboto Mono', monospace; } |
||||
|
|
||||
|
/* Infographic Palette */ |
||||
|
:root { |
||||
|
--c-primary: #0284c7; /* sky-600 */ |
||||
|
--c-primary-light: #f0f9ff; /* sky-50 */ |
||||
|
--c-secondary: #059669; /* emerald-600 */ |
||||
|
--c-accent: #db2777; /* pink-600 */ |
||||
|
--c-dark: #334155; /* slate-700 */ |
||||
|
--c-light: #f1f5f9; /* slate-100 */ |
||||
|
--c-text: #374151; /* gray-700 */ |
||||
|
--c-text-light: #64748b; /* slate-500 */ |
||||
|
} |
||||
|
|
||||
|
h1, h2, h3 { font-weight: 700; letter-spacing: -0.025em; color: var(--c-dark); } |
||||
|
h1 { font-size: 2.25rem; line-height: 2.5rem; font-weight: 800; } |
||||
|
h2 { font-size: 1.875rem; line-height: 2.25rem; border-bottom: 2px solid var(--c-light); padding-bottom: 0.5rem; } |
||||
|
h3 { font-size: 1.25rem; line-height: 1.75rem; color: var(--c-primary); } |
||||
|
|
||||
|
.section-icon { |
||||
|
font-size: 2rem; |
||||
|
line-height: 1; |
||||
|
background-color: var(--c-primary-light); |
||||
|
color: var(--c-primary); |
||||
|
padding: 0.75rem; |
||||
|
border-radius: 0.5rem; |
||||
|
display: inline-block; |
||||
|
margin-bottom: 0.5rem; |
||||
|
} |
||||
|
|
||||
|
.feature-card { |
||||
|
background-color: white; |
||||
|
border: 1px solid #e2e8f0; /* slate-200 */ |
||||
|
border-radius: 0.75rem; |
||||
|
box-shadow: 0 4px 6px -1px rgba(0,0,0,0.05), 0 2px 4px -2px rgba(0,0,0,0.05); |
||||
|
transition: all 0.2s ease-in-out; |
||||
|
} |
||||
|
.feature-card:hover { |
||||
|
transform: translateY(-4px); |
||||
|
box-shadow: 0 10px 15px -3px rgba(0,0,0,0.07), 0 4px 6px -2px rgba(0,0,0,0.05); |
||||
|
border-color: var(--c-primary); |
||||
|
} |
||||
|
|
||||
|
/* Keyboard Shortcut Styling */ |
||||
|
kbd { |
||||
|
background-color: #e2e8f0; /* slate-200 */ |
||||
|
border: 1px solid #cbd5e1; /* slate-300 */ |
||||
|
border-bottom-width: 2px; |
||||
|
color: #334155; /* slate-700 */ |
||||
|
border-radius: 0.375rem; |
||||
|
padding: 0.125rem 0.5rem; |
||||
|
font-family: 'Roboto Mono', monospace; |
||||
|
font-size: 0.875rem; |
||||
|
line-height: 1.25rem; |
||||
|
font-weight: 500; |
||||
|
display: inline-block; |
||||
|
margin: 0 0.125rem; |
||||
|
} |
||||
|
</style> |
||||
|
</head> |
||||
|
<body class="antialiased"> |
||||
|
|
||||
|
<div class="container mx-auto px-4 py-12 max-w-6xl space-y-16"> |
||||
|
|
||||
|
<section class="text-center"> |
||||
|
<div class="section-icon">⌨️</div> |
||||
|
<h1 class="text-4xl md:text-5xl">Keyboard Shortcuts</h1> |
||||
|
<p class="text-xl text-slate-600 max-w-3xl mx-auto mt-4"> |
||||
|
Quick reference guide for controlling the Radar & Video Synchronizer. |
||||
|
</p> |
||||
|
</section> |
||||
|
|
||||
|
<section> |
||||
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-6"> |
||||
|
<!-- Playback --> |
||||
|
<div class="feature-card p-6"> |
||||
|
<h3 class="mb-3">Playback & Navigation</h3> |
||||
|
<ul class="space-y-2 text-sm"> |
||||
|
<li><kbd>Spacebar</kbd> - Play / Pause</li> |
||||
|
<li><kbd>ArrowRight</kbd> - Next Frame (pauses)</li> |
||||
|
<li><kbd>ArrowLeft</kbd> - Previous Frame (pauses)</li> |
||||
|
<li><kbd>R</kbd> - Reset to Frame 0</li> |
||||
|
</ul> |
||||
|
</div> |
||||
|
<!-- View & Toggles --> |
||||
|
<div class="feature-card p-6"> |
||||
|
<h3 class="mb-3">View & UI Toggles</h3> |
||||
|
<ul class="space-y-2 text-sm"> |
||||
|
<li><kbd>M</kbd> - Toggle Sidebar Menu</li> |
||||
|
<li><kbd>I</kbd> - Toggle Data Explorer</li> |
||||
|
<li><kbd>G</kbd> - Toggle "GOD MODE" Zoom</li> |
||||
|
<li><kbd>Q</kbd> - Toggle Dark/Light Theme</li> |
||||
|
<li><kbd>A</kbd> - Toggle Advanced Debug Info</li> |
||||
|
<li><kbd>F11</kbd> - Toggle Fullscreen</li> |
||||
|
</ul> |
||||
|
</div> |
||||
|
<!-- Data Display --> |
||||
|
<div class="feature-card p-6"> |
||||
|
<h3 class="mb-3">Data Display Toggles</h3> |
||||
|
<ul class="space-y-2 text-sm"> |
||||
|
<li><kbd>T</kbd> - Toggle Tracks</li> |
||||
|
<li><kbd>D</kbd> - Toggle Object Details</li> |
||||
|
<li><kbd>P</kbd> - Toggle Predicted Position</li> |
||||
|
<li><kbd>C</kbd> - Toggle Confirmed Tracks Only</li> |
||||
|
<li><kbd>1</kbd> / <kbd>S</kbd> - Color by SNR</li> |
||||
|
<li><kbd>2</kbd> - Color by Cluster</li> |
||||
|
<li><kbd>3</kbd> - Color by Inlier</li> |
||||
|
<li><kbd>4</kbd> - Color by Stationary</li> |
||||
|
</ul> |
||||
|
</div> |
||||
|
</div> |
||||
|
</section> |
||||
|
|
||||
|
<footer class="text-center mt-12 text-slate-500 text-sm"> |
||||
|
<p>Pro Tip: Keep this tab open for quick reference while using the visualizer.</p> |
||||
|
</footer> |
||||
|
|
||||
|
</div> |
||||
|
|
||||
|
</body> |
||||
|
</html> |
||||
@ -1,84 +0,0 @@ |
|||||
Context Document: Radar and Video Synchronizer Application |
|
||||
|
|
||||
### 1. High-Level Overview |
|
||||
|
|
||||
**Core Purpose**: A high-precision, browser-based tool for visualizing and synchronizing radar sensor data (JSON) with a corresponding video file. It allows for detailed analysis of object tracks, point clouds, and vehicle dynamics. |
|
||||
|
|
||||
**Core Technologies**: |
|
||||
- **Frontend**: HTML5, Tailwind CSS |
|
||||
- **Logic**: Modular JavaScript (ES6 Modules) |
|
||||
- **Visualization**: `p5.js` for the main radar plot, a zoomed-in "god mode" view, and a time-series speed graph. |
|
||||
- **Data Handling**: |
|
||||
- **Web Workers** (`parser.worker.js`) with the `Clarinet.js` streaming library to parse large JSON files off the main thread, preventing UI freezes. |
|
||||
- `Oboe.js` is also loaded but the primary implementation uses the worker. |
|
||||
- **Data Exploration**: `AG-Grid` for tabular data view and `Chart.js` for plotting data from the grid. |
|
||||
- **Persistence**: `IndexedDB` for caching large files (JSON, Video) and `localStorage` for user settings (UI state, theme, file references). |
|
||||
|
|
||||
### 2. Project Architecture & File Structure |
|
||||
|
|
||||
The application uses a modular ES6 structure. All source code resides in the `/src` directory. |
|
||||
|
|
||||
- **`index.html`**: The main HTML shell. Defines the DOM structure, including the main layout, collapsible sidebar, data explorer panel, and modal dialogs. It loads all necessary CDN libraries and the main JS module. |
|
||||
|
|
||||
- **`/src/main.js`**: **The Orchestrator**. This is the application's entry point. It initializes all modules, wires up all event listeners (clicks, drag-drop, keydown), and manages the file loading pipeline and application lifecycle. |
|
||||
|
|
||||
- **`/src/state.js`**: **The Single Source of Truth**. Exports a single global `appState` object that holds all dynamic data (e.g., `vizData`, `isPlaying`, `currentFrame`). All modules import from this file. |
|
||||
|
|
||||
- **`/src/dom.js`**: **The UI Abstraction Layer**. Exports constants for every key DOM element and contains functions that directly manipulate the DOM, such as `updateFrame()`, `resetUIForNewLoad()`, and `updatePersistentOverlays()`. |
|
||||
|
|
||||
- **`/src/sync.js`**: **The Heartbeat/Clock**. Contains the `animationLoop()` function. It uses `performance.now()` to create a high-precision clock, calculates the current media time, finds the corresponding radar frame, and handles resynchronization with the video element. |
|
||||
|
|
||||
- **`/src/fileParsers.js`**: **The Data Processor**. Contains `parseVisualizationJson()`, which takes the raw parsed JSON object and enriches it with calculated `timestampMs` values relative to the video start time and determines global SNR ranges. |
|
||||
|
|
||||
- **`/src/parser.worker.js`**: **The Heavy Lifter**. A Web Worker that uses `Clarinet.js` to stream-parse the JSON file, preventing the main thread from freezing. It posts progress updates and the final parsed object back to `main.js`. |
|
||||
|
|
||||
- **`/src/db.js`**: **The Caching Layer**. Manages all interactions with `IndexedDB` to save and load file blobs and their metadata, enabling fast session reloads. |
|
||||
|
|
||||
- **`/src/dataExplorer.js`**: **The Inspector**. Manages the "Data Explorer" panel. It uses AG-Grid to display data in a table and Chart.js to plot selected columns. |
|
||||
|
|
||||
- **`/src/p5/radarSketch.js`**: The p5.js sketch for the main radar visualization (point cloud, tracks, axes, ego vehicle). |
|
||||
|
|
||||
- **`/src/p5/speedGraphSketch.js`**: The p5.js sketch for the time-series speed graph. |
|
||||
|
|
||||
- **`/src/p5/zoomSketch.js`**: The p5.js sketch for the "GOD MODE" magnified view that follows the mouse. |
|
||||
|
|
||||
- **`/src/drawUtils.js`**: **The Artist's Toolkit**. Contains pure drawing functions called by the p5 sketches (e.g., `drawPointCloud`, `drawTrajectories`). This is where the visual appearance of radar objects is defined. |
|
||||
|
|
||||
- **`/src/utils.js`**: A collection of pure, reusable helper functions (e.g., `findRadarFrameIndexForTime` (binary search), timestamp parsers, `throttle`). |
|
||||
|
|
||||
- **`/src/modal.js`**: Manages the logic for pop-up modal dialogs, including notifications, confirmations, and loading progress bars. |
|
||||
|
|
||||
- **`/src/theme.js`**: Handles the dark/light mode theme switching. |
|
||||
|
|
||||
- **`/src/constants.js`**: Stores shared, static values like `VIDEO_FPS` and radar plot boundaries. |
|
||||
|
|
||||
### 3. Data Flow & State Management |
|
||||
|
|
||||
**File Loading Pipeline (`main.js`):** |
|
||||
1. **User Action**: User drops files or uses "Load" buttons. The `handleFiles()` function is triggered. |
|
||||
2. **UI Reset**: `resetUIForNewLoad()` is called to clear the previous state. |
|
||||
3. **Pipeline Start**: `processFilePipeline()` begins. A loading modal is shown. |
|
||||
4. **JSON Parsing**: If a JSON file exists, it's sent to `parser.worker.js`. The worker streams the file, posts progress updates, and finally returns the complete parsed object. |
|
||||
5. **JSON Processing**: The parsed object is processed by `parseVisualizationJson()` to calculate relative timestamps and SNR ranges. The result is stored in `appState.vizData`. |
|
||||
6. **Video Loading (Two-Stage)**: |
|
||||
- **Stage A (Metadata)**: An event listener waits for `loadedmetadata`. When this fires, the video's `duration` is known. The `finalizeSetup()` function is called, which creates the p5 sketches and sets up the speed graph. |
|
||||
- **Stage B (Buffering)**: A separate listener waits for `canplaythrough`. When this fires, it signals that the video is ready for smooth playback, and the loading modal is hidden. |
|
||||
7. **Finalization**: `finalizeSetup()` creates the p5 instances and `resetVisualization()` is called to display the first frame. |
|
||||
|
|
||||
**State Management (`appState`):** |
|
||||
The `appState` object in `state.js` is the central hub. Key properties include: |
|
||||
- `vizData`: The large object containing all radar frames and track data. |
|
||||
- `isPlaying`: A boolean that controls the `animationLoop`. |
|
||||
- `currentFrame`: The integer index of the currently displayed radar frame. |
|
||||
- `videoStartDate`, `radarStartTimeMs`: Date objects used to calculate the time offset. |
|
||||
- `p5_instance`, `speedGraphInstance`, `zoomSketchInstance`: References to the active p5.js sketches. |
|
||||
|
|
||||
### 4. Key Logic and Interaction Flows |
|
||||
|
|
||||
**Playback Synchronization (`sync.js`)**: The `animationLoop` is the core. It uses `performance.now()` to create a high-resolution timer independent of the video's `timeupdate` event. It calculates what the video's `currentTime` *should* be, finds the corresponding radar frame using a binary search (`findRadarFrameIndexForTime` in `utils.js`), and periodically corrects the video's `currentTime` if it drifts. |
|
||||
|
|
||||
**UI Updates (`dom.js`)**: The `updateFrame(frame, forceVideoSeek)` function is the primary entry point for changing what's on screen. It updates the frame counter, seeks the video if `forceVideoSeek` is true, and calls the `.redraw()` methods on the p5 sketches. It's called by both the `animationLoop` (for smooth playback) and by UI event listeners like the timeline slider (for seeking). |
|
||||
|
|
||||
**Session Persistence (`main.js` & `db.js`)**: On `DOMContentLoaded`, the app checks `localStorage` for saved filenames. It then calls `loadFreshFileFromDB()` to attempt to load the corresponding blobs from `IndexedDB`. If successful, `handleFiles()` is called with the cached blobs, bypassing the need for user file selection. |
|
||||
|
|
||||
**Keyboard Shortcuts (`main.js`)**: A single `keydown` event listener on the document handles all shortcuts. It programmatically triggers `.click()` events on the corresponding DOM elements (e.g., Spacebar clicks `playPauseBtn`). It includes a check to prevent shortcuts from firing when the user is typing in an input field. |
|
||||
|
Before Width: 32 | Height: 32 | Size: 1.4 KiB After Width: 256 | Height: 256 | Size: 11 KiB |
@ -0,0 +1,66 @@ |
|||||
|
# ARAS Radar and Video Synchronizer |
||||
|
|
||||
|
High-precision, browser-based tool for visualizing radar point cloud data, object tracks, and CAN bus speed data, synchronized with a corresponding video file. |
||||
|
|
||||
|
## Project Overview |
||||
|
|
||||
|
This project is a modular ES6 JavaScript application refactored from a monolithic codebase. It provides a sophisticated interface for multi-sensor data playback and analysis. |
||||
|
|
||||
|
### Core Technologies |
||||
|
- **Rendering**: [p5.js](https://p5js.org/) for main radar, speed graph, and "GOD MODE" visualizations. Hardened with Triple-Buffer protection. |
||||
|
- **Parsing**: Web Workers using [Clarinet.js](https://github.com/dscape/clarinet) for non-blocking, streaming JSON parsing of large datasets. |
||||
|
- **Storage**: **IndexedDB** for persistent file caching; **localStorage** for user workspace state and layout persistence. |
||||
|
- **Layout Engine**: [GridStack.js](https://gridstackjs.com/) for the modular, resizable dashboard interface. |
||||
|
- **Styling**: Tailwind CSS for a premium, dark-mode-first UI. |
||||
|
- **Data Exploration**: [AG Grid](https://www.ag-grid.com/), **Custom Vertical Property View**, and [Chart.js](https://www.chartjs.org/) for forensic data inspection. |
||||
|
|
||||
|
### Architecture |
||||
|
- **State Management**: Centralized in `src/state.js` via the `appState` object. |
||||
|
- **Synchronization**: `src/sync.js` uses high-resolution `performance.now()` to perfectly align radar frames with video playback. |
||||
|
- **Workspace Engine**: `src/ui.js` manages a Hybrid Dashboard (GridStack + Standalone Floating Windows) with auto-focus and viewport rescue logic. |
||||
|
- **Modular Design**: Functional decomposition into specialized modules for Sync, Data, UI, and Visualizations. |
||||
|
|
||||
|
## Building and Running |
||||
|
|
||||
|
The project is designed to run as a static web application but requires a local server due to security restrictions on file access and Web Workers. |
||||
|
|
||||
|
### Prerequisites |
||||
|
- Python 3.x installed on your system. |
||||
|
|
||||
|
### Quick Start |
||||
|
1. **Check Environment**: Run `python_check.bat` to verify Python is in your PATH. |
||||
|
2. **Start Server**: Run `Visualization_Start.bat`. This executes `python -m http.server 8000`. |
||||
|
3. **Access App**: Open your browser and navigate to `http://localhost:8000`. |
||||
|
|
||||
|
### Development |
||||
|
Since this is a static project using ES6 modules directly in the browser: |
||||
|
- No build step (e.g., Webpack/Vite) is currently required for basic usage. |
||||
|
- Tests can be run by opening `tests/test-runner.html` in a local server environment. |
||||
|
|
||||
|
## Key Directories and Files |
||||
|
- `src/`: Main source code directory. |
||||
|
- `p5/`: p5.js sketches for Radar, Speed Graph, and Standalone "GOD MODE" Zoom. |
||||
|
- `dataExplorer.js`: Logic for the Data Explorer panel (AG Grid, Vertical Property View, Chart.js). |
||||
|
- `ui.js`: Unified UI/Workspace engine with layout memory and resizable panel logic. |
||||
|
- `sync.js`: High-precision synchronization logic. |
||||
|
- `parser.worker.js`: Off-thread streaming JSON parser. |
||||
|
- `vendor/`: Local copies of 3rd party libraries ensuring offline functionality. |
||||
|
- `annex/`: Technical guides, shortcuts, and infographics for the dashboard. |
||||
|
- `intel/`: Project documentation and high-level architecture guides (this folder). |
||||
|
- `Data_structs/`: Documentation for JSON and ROS2 sensor streams. |
||||
|
|
||||
|
## Development Conventions |
||||
|
|
||||
|
### Coding Style |
||||
|
- **Modularization**: Follow the established pattern of separating logic into `src/` modules. Avoid adding logic to `index.html`. |
||||
|
- **State Access**: Always use `appState` for reactive data. |
||||
|
- **Drawing**: Use `src/drawUtils.js` for reusable drawing functions shared between `radarSketch` and `zoomSketch`. |
||||
|
|
||||
|
### Naming |
||||
|
- Use camelCase for variables and functions. |
||||
|
- Use PascalCase for class-like structures (though the project primarily uses objects and functions). |
||||
|
|
||||
|
### Documentation |
||||
|
- Maintain `readme.md`, `GEMINI.md`, and `context.md` in the `intel/` folder with significant architectural changes. |
||||
|
- Use `Improvements.txt` (in `annex/`) to track progress on the refactor and new feature requests. |
||||
|
- Reference supplementary guides in `annex/` (User Manual, Shortcuts, Changelog). |
||||
@ -0,0 +1,143 @@ |
|||||
|
Context Document: Radar and Video Synchronizer Application |
||||
|
|
||||
|
### 1. High-Level Overview |
||||
|
|
||||
|
**Core Purpose**: A high-precision, browser-based tool for visualizing and synchronizing radar sensor data (JSON) with a corresponding video file. It allows for detailed analysis of object tracks, point clouds, and vehicle dynamics. |
||||
|
|
||||
|
**Core Technologies**: |
||||
|
- **Frontend**: HTML5, Tailwind CSS |
||||
|
- **Logic**: Modular JavaScript (ES6 Modules) |
||||
|
- **Visualization**: `p5.js` for the main radar plot, a zoomed-in "god mode" view, and a time-series speed graph. |
||||
|
- **Data Handling**: |
||||
|
- **Web Workers** (`parser.worker.js`) with the `Clarinet.js` streaming library to parse large JSON files off the main thread, preventing UI freezes. |
||||
|
- `Oboe.js` is included in vendor files but `Clarinet.js` is the active parser. |
||||
|
- **Data Exploration**: |
||||
|
- `AG-Grid` for tabular Point Cloud and Track data. |
||||
|
- **Vertical Property View** (`dataExplorer.js`) for high-density ADAS data. |
||||
|
- `Chart.js` for plotting data from the grids. |
||||
|
- **Persistence**: `IndexedDB` for caching large files (JSON, Video) and `localStorage` for user settings (UI state, theme, file references). |
||||
|
|
||||
|
### 2. Project Architecture & File Structure |
||||
|
|
||||
|
The application uses a modular ES6 structure. All source code resides in the `/src` directory. |
||||
|
|
||||
|
- **`index.html`**: The main HTML shell. Defines the DOM structure, including the main layout, collapsible sidebar, data explorer panel, and documentation modals (User Manual, Shortcuts, Codebase Overview, Changelog). It loads all necessary CDN libraries and the main JS module. |
||||
|
|
||||
|
... |
||||
|
|
||||
|
- **`/annex/`**: **Supplementary Documentation**. Contains HTML files for the User Manual, Keyboard Shortcuts, Codebase Overview, and Changelog, as well as the progress log (`Improvements.txt`). |
||||
|
|
||||
|
- **`/intel/`**: **Project Intelligence**. Contains documentation and AI-specific context files (`readme.md`, `GEMINI.md`, `context.md`). |
||||
|
|
||||
|
- **`/src/main.js`**: **The Orchestrator**. This is the application's entry point. It initializes the database, theme, and data explorer. It sets up the initial event listeners for the UI and delegates specialized tasks to other modules (`fileLoader.js`, `keyboard.js`, `sync.js`, `ui.js`). |
||||
|
|
||||
|
- **`/src/state.js`**: **The Single Source of Truth**. Exports a single global `appState` object that holds all dynamic data (e.g., `vizData`, `isPlaying`, `currentFrame`). All modules import from this file. |
||||
|
|
||||
|
- **`/src/fileLoader.js`**: **File Management**. Handles the file loading pipeline. It manages drag-and-drop interactions, file input changes, and the unified processing of JSON and Video files. It handles caching logic (saving/loading from `IndexedDB`) and triggers the parsing worker. |
||||
|
|
||||
|
- **`/src/keyboard.js`**: **Input Handling**. Manages all keyboard shortcuts. It creates a centralized `keydown` listener that triggers UI actions (play/pause, seeking, toggles) and prevents interference with input fields. |
||||
|
|
||||
|
- **`/src/sync.js`**: **The Heartbeat & Controller**. Contains the core logic for playback and synchronization. |
||||
|
- `animationLoop()`: The main render loop that keeps the UI updated. |
||||
|
- `videoFrameCallback()`: The high-precision video timing loop. |
||||
|
- `updateFrame()`: The central function that updates the current frame index, synchronizes the video (handling drift), and updates UI elements (sliders, counters, overlays). |
||||
|
- `resetVisualization()`: Resets the playback state. |
||||
|
- Handles timeline interactions (input, scroll wheel) and video panel scrolling. |
||||
|
|
||||
|
- **`/src/dom.js`**: **The UI Interface**. Exports constants for every key DOM element. It contains functions to update specific UI components like persistent overlays (`updatePersistentOverlays`), debug overlays (`updateDebugOverlay`), and custom TTC scheme inputs. *Note: Core playback-driven UI updates have moved to `sync.js`.* |
||||
|
|
||||
|
- **`/src/fileParsers.js`**: **The Data Processor**. Contains `parseVisualizationJson()`, which takes the raw parsed JSON object and enriches it with calculated `timestampMs` values relative to the video start time and determines global SNR ranges. |
||||
|
|
||||
|
- **`/src/parser.worker.js`**: **The Heavy Lifter**. A Web Worker that uses `Clarinet.js` to stream-parse the JSON file, preventing the main thread from freezing. It posts progress updates and the final parsed object back to `main.js` (via `fileLoader.js`). |
||||
|
|
||||
|
- **`/src/db.js`**: **The Caching Layer**. Manages all interactions with `IndexedDB` to save and load file blobs and their metadata, enabling fast session reloads. |
||||
|
|
||||
|
- **`/src/dataExplorer.js`**: **The Inspector**. Manages the "Data Explorer" panel. |
||||
|
- **Standard Grids**: Uses AG-Grid for Point Cloud and Track data. |
||||
|
- **ADAS View**: Implements a custom Vertical Property View for readable ADAS inspection. |
||||
|
- **Visualization**: Chart.js for plotting selected numeric columns. |
||||
|
- **Optimizations**: `throttledUpdateExplorer` for performance; scrollable tabs and 250x200 minimum dimensions for compact layouts. |
||||
|
|
||||
|
- **`/src/ui.js`**: **The UI Engine**. Manages all advanced interface interactions. |
||||
|
- `makeDraggableAndResizable()`: A unified utility that transforms any DOM element into a floating window with persistent memory. |
||||
|
- **Memory & Persistence**: Saves/Loads panel coordinates and GridStack layouts to `localStorage`. |
||||
|
- **Viewport Rescue**: Automatically recovers off-screen windows during browser resizing. |
||||
|
- **Auto-Focus**: Dynamically manages `z-index` to bring active windows to the front on click. |
||||
|
|
||||
|
- **`/src/debug.js`**: **Debug Configuration**. Exports `debugFlags` to toggle logging for various subsystems (sync, drawing, file loading) and configure constants like video load timeouts. |
||||
|
|
||||
|
- **`/src/utils.js`**: **Toolbox**. A collection of pure, reusable helper functions (e.g., `findRadarFrameIndexForTime` (binary search), timestamp parsers, `throttle`, `precomputeRadarVideoSync`). |
||||
|
|
||||
|
- **`/src/modal.js`**: **User Feedback**. Manages the logic for pop-up modal dialogs, including notifications, confirmations, and loading progress bars. |
||||
|
|
||||
|
- **`/src/theme.js`**: **Styling**. Handles the dark/light mode theme switching. |
||||
|
|
||||
|
- **`/src/constants.js`**: **Configuration**. Stores shared, static values like `VIDEO_FPS` and radar plot boundaries. |
||||
|
|
||||
|
- **`/src/p5/`**: **Visualization Modules**. |
||||
|
- **`radarSketch.js`**: Master timer for God Mode auto-visibility (5s delay). Hardened with guards against 0-width crashes. |
||||
|
- **`zoomSketch.js`**: "GOD MODE" view. Includes the "Closing in..." countdown (3s) and "Out of Bounds" safety indicators. |
||||
|
- **`speedGraphSketch.js`**: Refined with initialization guards to handle rapid layout shifts. |
||||
|
|
||||
|
- **`/src/drawUtils.js`**: **The Artist's Toolkit**. Contains pure drawing functions called by the p5 sketches (e.g., `drawPointCloud`, `drawTrajectories`). This is where the visual appearance of radar objects is defined. |
||||
|
|
||||
|
### 3. Data Flow & State Management |
||||
|
|
||||
|
**File Loading Pipeline (`fileLoader.js`):** |
||||
|
1. **Input**: User drops files or clicks load buttons. `handleFiles()` identifies the file types. |
||||
|
2. **Processing (`processFilePipeline`)**: A loading modal is shown. |
||||
|
3. **Caching**: Files are saved to `IndexedDB` (non-blocking). |
||||
|
4. **Offset**: Timestamp offset is calculated from filenames. |
||||
|
5. **JSON Parsing**: JSON is sent to `parser.worker.js`. The worker streams the file and returns the object. |
||||
|
6. **Post-Processing**: `parseVisualizationJson()` (in `fileParsers.js`) enriches the data. `precomputeRadarVideoSync()` (in `utils.js`) bakes sync times. |
||||
|
7. **Video Loading**: The video is loaded into the player. |
||||
|
8. **Finalization**: `finalizeSetup()` (in `fileLoader.js`) resets the visualization, creates/updates p5 sketches, and updates the UI. |
||||
|
|
||||
|
**Playback & Synchronization (`sync.js`):** |
||||
|
- **Video Master**: The video element's time is the source of truth when playing. |
||||
|
- **`videoFrameCallback`**: Runs on every video frame, finds the corresponding radar frame index, and updates `appState.currentFrame`. |
||||
|
- **`animationLoop`**: Runs on `requestAnimationFrame` (~60Hz). It calls `updateFrame()` to reflect the state on the screen. |
||||
|
- **`updateFrame()`**: |
||||
|
- Updates UI (slider, counter). |
||||
|
- Updates overlays (Ego speed, CAN speed). |
||||
|
- Calls `throttledUpdateExplorer`. |
||||
|
- Handles Video Seek: If `forceVideoSeek` is true (e.g., user scrubbed the timeline), it sets `videoPlayer.currentTime`. |
||||
|
- **Drift Correction**: If the video drifts significantly from the target radar frame time, `updateFrame` forces a seek to resync. |
||||
|
|
||||
|
**State Management (`appState`):** |
||||
|
The `appState` object in `state.js` is the central hub. Key properties include: |
||||
|
- `vizData`: The large object containing all radar frames and track data. |
||||
|
- `isPlaying`: Boolean controlling the loop. |
||||
|
- `currentFrame`: Integer index of the currently displayed radar frame. |
||||
|
- `offset`: Manual or auto-calculated time offset (ms). |
||||
|
- `videoStartDate`, `radarStartTimeMs`: Timestamps for absolute time calculation. |
||||
|
- `p5_instance`, `speedGraphInstance`: References to active sketches. |
||||
|
- `videoMissing`: Flag for JSON-only mode. |
||||
|
|
||||
|
### 4. Key Interaction Flows |
||||
|
|
||||
|
**Timeline Scrubbing (`sync.js`)**: |
||||
|
- **Drag**: `handleTimelineInput` updates the UI immediately for responsiveness but debounces the expensive video seek until the user stops dragging. |
||||
|
- **Scroll Wheel**: `handleTimelineWheel` allows frame-by-frame or accelerated seeking. It also updates UI immediately and debounces the video seek. |
||||
|
|
||||
|
**Data Explorer (`dataExplorer.js`)**: |
||||
|
- Activated by `I` key or canvas click. |
||||
|
- Shows Tree, Grid, and Plot views. |
||||
|
- Updates are throttled to prevent performance degradation during playback. |
||||
|
|
||||
|
**God Mode Auto-Hide (`p5/*.js`)**: |
||||
|
- **Sequence**: The panel appears on hover. If the mouse stops moving over a relevant point, a 5-second "Analysis Period" begins. After 5s, a visual 3-second countdown appears. Total 8s before hiding. |
||||
|
- **Override**: Any new interaction or mouse movement resets the full 8-second timer. |
||||
|
|
||||
|
**Layout Persistence (`ui.js` & `main.js`)**: |
||||
|
- **GridStack**: Uses a "Soft Restore" loop to update panel positions by ID, ensuring p5 canvases and Video elements are not destroyed during layout changes. |
||||
|
- **Floating Panels**: Tracks `top/left/width/height` individually per panel ID. |
||||
|
- **Nuclear Reset**: The "Clear Cache and Reload" button wipes all UI memory, returning the app to factory defaults. |
||||
|
|
||||
|
**Session Management (`main.js` & `db.js`)**: |
||||
|
- `saveSessionBtn` saves current filenames, offset, and toggles to a JSON file. |
||||
|
- `loadSessionBtn` reads the session file. It verifies that the referenced files exist in `IndexedDB` (via `loadFreshFileFromDB`) before applying settings and reloading the page. |
||||
|
|
||||
|
**Keyboard Shortcuts (`keyboard.js`)**: |
||||
|
- Centralized handler for `Play/Pause` (Space), `Seek` (Arrows), `Toggle Views` (1-4, T, D, G, P, C), `Debug` (A), `Theme` (Q). |
||||
|
- Smartly ignores shortcuts when input fields are focused. |
||||
@ -0,0 +1,146 @@ |
|||||
|
# Radar and Video Synchronizer (Refactored) |
||||
|
|
||||
|
**Version**: 3.3.0 (Synchronized Workspace Update) |
||||
|
|
||||
|
## 🎯 Overview |
||||
|
|
||||
|
This is a high-precision, browser-based tool for visualizing radar point cloud data, object tracks, and CAN bus speed data, synchronized with a corresponding video file. Originally a monolithic HTML application, it has been refactored into a modern, modular JavaScript application using ES6 Modules. |
||||
|
|
||||
|
The application leverages **p5.js** for rendering, **Web Workers** for background streaming JSON parsing, **IndexedDB** for persistent file caching, and **Tailwind CSS** for a professional, responsive UI. |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
## ⚙️ Core Architecture |
||||
|
|
||||
|
### `sync.js` (Synchronized Playback) |
||||
|
- **Master Clock**: Utilizes `performance.now()` for a high-resolution timer independent of imprecise video events. |
||||
|
- **Dynamic Mapping**: Maps the target media time (adjusted by user offsets) to the corresponding radar frame via binary search (`utils.js::findRadarFrameIndexForTime`). |
||||
|
- **Drift Correction**: Periodically checks for sync drift (>150ms) and forces video seeks to maintain frame-perfect alignment. |
||||
|
|
||||
|
### `fileLoader.js` (Unified File Loading) |
||||
|
- **Multi-Input**: Supports both button-based selection and global Drag & Drop. |
||||
|
- **Pipeline Processing**: Identifies file types and triggers the appropriate caching and parsing workers. |
||||
|
- **Auto-Offset**: Automatically calculates time offsets based on standardized filenames (e.g., `fHist_...json` and `WIN_...mp4`). |
||||
|
|
||||
|
### `parser.worker.js` (Streaming JSON Parser) |
||||
|
- **Off-Thread Processing**: Uses a Web Worker to keep the UI responsive during large file loads. |
||||
|
- **Streaming Logic**: Utilizes the `Clarinet.js` event-driven parser to reconstruct large datasets in memory without blocking the main thread. |
||||
|
- **Progress Reporting**: Sends real-time percentage updates back to the UI. |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
## 🎨 Visualizations (p5.js) |
||||
|
|
||||
|
### `radarSketch.js` (Primary Radar Canvas) |
||||
|
- **Radar Rendering**: Draws point clouds, tracks, ego-vehicle representation, and cluster centroids. |
||||
|
- **Interactive Layers**: Manages toggleable overlays for SNR, TTC, predicted positions, and covariance ellipses. |
||||
|
- **Hardening**: Features **Triple-Buffer Protection** against 0-width crashes and a master timer for God Mode auto-visibility. |
||||
|
|
||||
|
### `speedGraphSketch.js` (Telemetry Graph) |
||||
|
- **Time-Series Analysis**: Renders Ego Speed and CAN Speed lines synchronized with the video playback. |
||||
|
- **Visual Indicators**: Draws a vertical tracking line aligned with the current active frame. |
||||
|
|
||||
|
### `zoomSketch.js` (Magnified "GOD MODE") |
||||
|
- **Standalone Window [NEW]**: A decoupled, floating overlay that provides high-fidelity zoom. |
||||
|
- **Auto-Hide UX [NEW]**: 8-second sequence (5s analysis period + 3s visual countdown) before fading. |
||||
|
- **Safety Indicators**: Includes "Out of Bounds" warnings and "Closing in..." countdown status labels. |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
## 🖥️ User Interface & Workspace |
||||
|
|
||||
|
### `ui.js` (Workspace & Window Engine) [NEW] |
||||
|
- **Hybrid Dashboard**: Manages the mix of GridStack-based fixed panels and independent floating modules. |
||||
|
- **Layout Persistence**: Automatically saves/restores coordinates and GridStack geometry to `localStorage`. |
||||
|
- **Soft-Restore**: Updates layout positions by ID, preserving canvases and video players without destructive DOM replacement. |
||||
|
- **Viewport Rescue**: Automatically pulls off-screen panels back into view during window resize events. |
||||
|
- **Auto-Focus**: Clicking any floating panel dynamically increases its z-index (bringing it to the front). |
||||
|
|
||||
|
### `dataExplorer.js` (Data Inspector) |
||||
|
- **Deep Inspection**: Provides Tree View, AG-Grid View, and Chart.js Plotting for raw numerical frame data. |
||||
|
- **ADAS Property View [NEW]**: A specialized Vertical Property View for ADAS data, optimized for high readability of many columns in narrow panels. |
||||
|
- **Persistent Memory [NEW]**: Remembers its last position, size, and display state across page reloads. |
||||
|
- **Layout Optimization [NEW]**: Support for compact "sidecar" mode with reduced minimum dimensions (250x200) and scrollable navigation tabs. |
||||
|
|
||||
|
### `keyboard.js` (Shortcuts) |
||||
|
- **Centralized Handler**: Manages all keyboard interactions (Space, 1-4, S, T, D, G, P, etc.). |
||||
|
- **Smart Focus**: Prevents shortcuts from firing when the user is typing in number or text inputs. |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
## 📂 Project Structure |
||||
|
|
||||
|
```text |
||||
|
├── index.html # Main HTML shell |
||||
|
├── Visualization_Start.bat # Script to start the local server |
||||
|
├── python_check.bat # Script to check Python installation |
||||
|
├── favicon.png # Browser tab icon |
||||
|
├── package-lock.json # NPM lockfile |
||||
|
├── annex/ # Supplementary documentation and reference files |
||||
|
│ ├── User_Manual.html # Content for the Quick Start Guide (loaded via iframe) |
||||
|
│ ├── code-base-overview.html # Technical overview of the codebase (infographic style) |
||||
|
│ ├── Changelog_3.3.0.html # Current version release notes (loaded via iframe) |
||||
|
│ ├── Changelog.html # Legacy change archive |
||||
|
│ └── shortcuts.html # Reference for keyboard shortcuts (legacy/static) |
||||
|
├── intel/ # Project documentation and AI context |
||||
|
│ ├── readme.md # This documentation |
||||
|
│ ├── context.md # Detailed technical overview for AI assistance |
||||
|
│ ├── GEMINI.md # High-level project overview for AI |
||||
|
├── src/ |
||||
|
│ ├── constants.js # Shared constants (radar bounds, FPS) |
||||
|
│ ├── dataExplorer.js # Logic for the Data Explorer panel (AG Grid, Chart.js) |
||||
|
│ ├── db.js # IndexedDB logic for caching files |
||||
|
│ ├── debug.js # Debug logging flags and console exposure |
||||
|
│ ├── dom.js # Centralized DOM element references |
||||
|
│ ├── drawUtils.js # p5.js drawing helpers (points, tracks, axes, legends) |
||||
|
│ ├── fileLoader.js # File handling pipeline (Dropzone, Inputs, Worker trigger) |
||||
|
│ ├── fileParsers.js # Post-processing logic for parsed JSON |
||||
|
│ ├── keyboard.js # Keyboard shortcut handler |
||||
|
│ ├── main.js # Main application entry point, initialization logic |
||||
|
│ ├── modal.js # Logic for pop-up modal dialogs & progress bar |
||||
|
│ ├── parser.worker.js # Web Worker for background JSON parsing (uses Clarinet.js) |
||||
|
│ ├── session.js # NEW: Logic for saving and loading session state |
||||
|
│ ├── state.js # Centralized application state management object (appState) |
||||
|
│ ├── sync.js # Core animation loop and playback synchronization logic |
||||
|
│ ├── theme.js # Dark/Light mode theme switching logic |
||||
|
│ ├── ui.js # NEW: General UI event listeners (menus, modals, toggles) |
||||
|
│ ├── utils.js # General utility functions (binary search, formatting) |
||||
|
│ └── p5/ |
||||
|
│ ├── radarSketch.js # p5.js sketch for the main radar visualization |
||||
|
│ ├── speedGraphSketch.js # p5.js sketch for the speed graph |
||||
|
│ └── zoomSketch.js # p5.js sketch for the magnified zoom window ("GOD MODE") |
||||
|
├── vendor/ # Local copies of external libraries |
||||
|
│ ├── ag-grid-community.min.js |
||||
|
│ ├── chart.min.js |
||||
|
│ ├── clarinet.min.js |
||||
|
│ ├── oboe.min.js |
||||
|
│ ├── p5.js |
||||
|
│ └── tailwind-cdn.js |
||||
|
├── tests/ # Unit tests |
||||
|
│ ├── fileLoader.test.js |
||||
|
│ ├── fileParsers.test.js |
||||
|
│ ├── test-runner.html |
||||
|
│ └── utils.test.js |
||||
|
├── Data_structs/ # Documentation of JSON and ROS2 data structures |
||||
|
└── Console_logs/ # Example logs for debugging synchronization |
||||
|
``` |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
## 🚀 How to Use |
||||
|
|
||||
|
1. **Load Files**: Use "Load JSON"/"Load Video" buttons or **Drag & Drop**. |
||||
|
2. **Playback**: Control via Spacebar, timeline slider (drag/scroll), or Arrow keys. |
||||
|
3. **Inspect**: Click the Radar canvas or press `I` to open the **Data Explorer**. |
||||
|
4. **Workspace**: Reorganize panels. Your layout is automatically saved to local storage. |
||||
|
5. **Soft-Reset**: Use the "Clear Cache and Reload" button to perform a complete factory reset. |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
## 🛠️ Technical Setup |
||||
|
|
||||
|
The project is designed to run as a static application but requires a local server for Web Workers and File API access. |
||||
|
|
||||
|
- **Check Environment**: Run `python_check.bat`. |
||||
|
- **Start Server**: Run `Visualization_Start.bat`. |
||||
|
- **Access**: Open `http://localhost:8000` in any modern browser. |
||||
@ -0,0 +1,6 @@ |
|||||
|
{ |
||||
|
"name": "steps", |
||||
|
"lockfileVersion": 3, |
||||
|
"requires": true, |
||||
|
"packages": {} |
||||
|
} |
||||
@ -1,230 +0,0 @@ |
|||||
Radar and Video Synchronizer (Refactored) |
|
||||
|
|
||||
Version: Based on commit f426eee3 (Inferred) |
|
||||
|
|
||||
Overview |
|
||||
|
|
||||
This is a high-precision, browser-based tool for visualizing radar point cloud data, object tracks, and CAN bus speed data, synchronized with a corresponding video file. Originally a monolithic HTML application (V14_inliner_Stationary.html), it has been refactored into a modern, modular JavaScript application using ES6 Modules. This structure enhances maintainability, performance (especially with large datasets), and extensibility. |
|
||||
|
|
||||
The application leverages p5.js for rendering visualizations, a Web Worker with the Clarinet.js streaming parser for efficient background JSON processing, IndexedDB for persistent file caching, Tailwind CSS for styling, and introduces a Data Explorer panel using AG Grid and Chart.js for in-depth data inspection. |
|
||||
|
|
||||
Core Features Detailed |
|
||||
|
|
||||
Synchronized Playback (sync.js): |
|
||||
|
|
||||
Utilizes performance.now() for a high-resolution master clock, independent of potentially imprecise video events. |
|
||||
|
|
||||
Calculates the target media time based on playback speed (speedSlider.value) and elapsed real time. |
|
||||
|
|
||||
Maps the target media time (adjusted by the user-defined offsetInput.value) to the corresponding radar frame timestamp (timestampMs). |
|
||||
|
|
||||
Uses a binary search (utils.js::findRadarFrameIndexForTime) to efficiently find the correct radar frame index for the calculated timestamp. |
|
||||
|
|
||||
Periodically checks for drift (>150ms) between the master clock's calculated time and videoPlayer.currentTime, forcing a video seek if needed to maintain sync. |
|
||||
|
|
||||
Unified File Loading (main.js): |
|
||||
|
|
||||
Handles loading JSON (radar data) and Video files through both button clicks (loadJsonBtn, loadVideoBtn) and drag-and-drop onto the main content area (<main>). |
|
||||
|
|
||||
The handleFiles function identifies file types (.json, video/*) and triggers the processFilePipeline. |
|
||||
|
|
||||
Note: Dedicated CAN log loading (loadCanBtn, canFileInput) has been removed. CAN speed data (canVehSpeed_kmph) is now expected within the JSON structure, associated with each radarFrame. |
|
||||
|
|
||||
Efficient JSON Parsing (parser.worker.js, main.js, fileParsers.js): |
|
||||
|
|
||||
The processFilePipeline function initiates a Web Worker (parser.worker.js). |
|
||||
|
|
||||
The worker receives the JSON File object. |
|
||||
|
|
||||
It uses the File.stream() API and TextDecoder to read the file chunk by chunk. |
|
||||
|
|
||||
Clarinet.js (clarinet.min.js via CDN import in worker) parses the incoming JSON stream event-by-event (onopenobject, onkey, onvalue, etc.), reconstructing the full JavaScript object in memory off the main thread. |
|
||||
|
|
||||
Progress updates (percent) are sent back to the main thread via postMessage. |
|
||||
|
|
||||
On completion, the fully parsed object (data) is sent back. |
|
||||
|
|
||||
The main thread receives the parsed data and passes it to fileParsers.js::parseVisualizationJson for post-processing (calculating relative timestampMs, determining global SNR range). |
|
||||
|
|
||||
Interactive Visualization (p5/, drawUtils.js): |
|
||||
|
|
||||
radarSketch.js: The primary p5 sketch. |
|
||||
|
|
||||
Manages the main canvas within #canvas-container. |
|
||||
|
|
||||
Calculates plot scales (plotScaleX, plotScaleY) based on canvas size and constants.js boundaries. |
|
||||
|
|
||||
Draws axes, ego vehicle representation (drawUtils.js::drawEgoVehicle). |
|
||||
|
|
||||
Draws dynamic elements: point clouds (drawUtils.js::drawPointCloud), tracks (drawUtils.js::drawTrajectories), track markers (drawUtils.js::drawTrackMarkers), predicted positions (now drawn for the current frame currentFrame, not currentFrame + 1), covariance ellipses (drawUtils.js::drawCovarianceEllipse), cluster centroids (drawUtils.js::drawClusterCentroids), and regions of interest (drawUtils.js::drawRegionsOfInterest). |
|
||||
|
|
||||
Handles hover interactions for the Zoom Mode ("GOD MODE") tooltip via drawUtils.js::handleCloseUpDisplay. |
|
||||
|
|
||||
Draws legends (SNR, Track TTC). |
|
||||
|
|
||||
speedGraphSketch.js: The secondary p5 sketch for the speed graph. |
|
||||
|
|
||||
Manages the canvas within #speed-graph-container. |
|
||||
|
|
||||
Draws time-series lines for Ego Speed (frame.egoVelocity[1] * 3.6) and CAN Speed (frame.canVehSpeed_kmph) from the JSON vizData. |
|
||||
|
|
||||
Draws axes and legends. |
|
||||
|
|
||||
Draws a vertical red line indicator synchronized with the current frame's timestamp (frameData.timestampMs). |
|
||||
|
|
||||
zoomSketch.js: p5 sketch for the magnified "GOD MODE" view. |
|
||||
|
|
||||
Manages a separate canvas within #zoom-canvas-container. |
|
||||
|
|
||||
Receives mouse coordinates and hovered items from radarSketch.js. |
|
||||
|
|
||||
Redraws a scaled and translated portion of the main scene, providing a high-fidelity zoom. |
|
||||
|
|
||||
Includes detailed tooltip drawing logic (drawZoomTooltip) showing extensive info for points, clusters, tracks (with speed), and predictions (with velocity). |
|
||||
|
|
||||
Handles mouse wheel events for adjusting appState.zoomFactor. |
|
||||
|
|
||||
drawUtils.js: Contains numerous helper functions responsible for the actual drawing logic (shapes, lines, colors, text) used by radarSketch.js and zoomSketch.js. Defines color palettes (snrColors, ttcColors, clusterColors). Refined tooltip data generation in handleCloseUpDisplay. |
|
||||
|
|
||||
Data Explorer (dataExplorer.js, main.js, index.html): |
|
||||
|
|
||||
A dedicated panel (#data-explorer-panel) for inspecting raw data of the current frame (appState.currentFrame). |
|
||||
|
|
||||
Activated by pressing the <kbd>I</kbd> key or clicking on the main radar canvas (#canvas-container). |
|
||||
|
|
||||
Features three views selectable via tabs: |
|
||||
|
|
||||
Tree View (#tab-panel-tree): Displays the current frame's data structure as a formatted JSON string. |
|
||||
|
|
||||
Grid View (#tab-panel-grid): Uses AG Grid (ag-grid-community.min.js) to display array data (e.g., pointCloud) in a sortable, filterable table. Populated via displayInGrid(data, title). |
|
||||
|
|
||||
Plot View (#tab-panel-plot): Uses Chart.js (chart.js CDN) to generate a line plot of the numeric data from a column selected in the Grid View (by clicking the column header). |
|
||||
|
|
||||
Allows users to directly examine the underlying numerical values being visualized. |
|
||||
|
|
||||
Dynamic Filtering & Coloring (dom.js, drawUtils.js, state.js): |
|
||||
|
|
||||
Checkboxes in the sidebar (#collapsible-menu) control boolean flags in appState (via main.js event listeners). |
|
||||
|
|
||||
drawUtils.js functions read these flags (toggleSnrColor.checked, toggleConfirmedOnly.checked, etc.) to determine: |
|
||||
|
|
||||
Which color palette/logic to apply to points and tracks. |
|
||||
|
|
||||
Whether to show certain elements (tracks, velocity vectors, predicted positions, covariance). |
|
||||
|
|
||||
SNR range inputs (snrMinInput, snrMaxInput) update appState.globalMinSnr/MaxSnr for color scaling. |
|
||||
|
|
||||
TTC coloring mode (ttcModeDefault, ttcModeCustom) and custom inputs (ttcColorCritical, ttcTimeCritical, etc.) update appState.useCustomTtcScheme and appState.customTtcScheme respectively (dom.js event listeners). drawUtils.js::drawTrajectories uses this state to color tracks. |
|
||||
|
|
||||
Playback Controls & Navigation (main.js, dom.js): |
|
||||
|
|
||||
Standard buttons (playPauseBtn, stopBtn) modify appState.isPlaying and call videoPlayer.play/pause/currentTime. |
|
||||
|
|
||||
timelineSlider input event updates appState.currentFrame and calls dom.js::updateFrame(frame, true) (forcing video seek). Throttled for performance during drag, debounced for final sync on release. |
|
||||
|
|
||||
timelineSlider wheel event calculates scroll speed and dynamically seeks frames, also debounced for final sync (main.js). |
|
||||
|
|
||||
timelineSlider mousemove event calculates hover position to display frame/time in #timeline-tooltip (main.js). |
|
||||
|
|
||||
speedSlider updates videoPlayer.playbackRate (main.js). |
|
||||
|
|
||||
UI Enhancements (main.js, theme.js, dom.js, index.html): |
|
||||
|
|
||||
Sidebar (#collapsible-menu) visibility toggled by toggleMenuBtn, closeMenuBtn, and clicks on the #menu-scrim overlay (main.js::toggleMenu). |
|
||||
|
|
||||
Fullscreen toggled via fullscreenBtn and monitored using the fullscreenchange event (main.js). |
|
||||
|
|
||||
Persistent overlays (#radar-info-overlay, #video-info-overlay) updated by dom.js::updatePersistentOverlays with frame index, absolute time, color mode, and sync drift. |
|
||||
|
|
||||
Dark/Light theme managed by theme.js::setTheme, saving preference to localStorage, and triggering redraws in p5 sketches. |
|
||||
|
|
||||
Session Management (main.js, db.js): |
|
||||
|
|
||||
Files are cached in IndexedDB using db.js::saveFileWithMetadata upon loading. Metadata (filename, size) is stored alongside the blob. |
|
||||
|
|
||||
On DOMContentLoaded, main.js retrieves expected filenames from localStorage and attempts to load corresponding blobs using db.js::loadFreshFileFromDB. This function verifies filename and size match before returning the blob, ensuring cache validity. |
|
||||
|
|
||||
saveSessionBtn gathers current state (appState filenames, offsetInput.value, toggle states) into a JSON object and triggers a browser download (main.js). |
|
||||
|
|
||||
loadSessionBtn reads a chosen session JSON file. It verifies that the files mentioned in the session currently exist and are valid in IndexedDB using loadFreshFileFromDB before applying settings to localStorage and reloading the page (main.js). |
|
||||
|
|
||||
Keyboard Shortcuts (main.js): |
|
||||
|
|
||||
A comprehensive keydown listener intercepts keys (Space, Arrows, 1-4, S, T, D, G, P, A, M, Q, R, C, I). |
|
||||
|
|
||||
It programmatically triggers .click() events on corresponding buttons or directly updates appState/calls functions (like showExplorer/hideExplorer). |
|
||||
|
|
||||
Includes a check to prevent shortcuts firing when focus is on text/number inputs (offsetInput, snrMinInput, etc.). |
|
||||
|
|
||||
How to Run Locally |
|
||||
|
|
||||
(Instructions remain the same - requires Python 3 and running python -m http.server 8000 or the provided .bat script in the steps directory). |
|
||||
|
|
||||
Check Python Installation: Run python_check.bat or python --version. Need Python 3.x in PATH. |
|
||||
|
|
||||
Navigate to Project Directory: cd to the steps directory containing index.html. |
|
||||
|
|
||||
Start Local Server: Run Visualization_Start.bat or python -m http.server 8000. Keep terminal open. |
|
||||
|
|
||||
Open in Browser: Navigate to http://localhost:8000. |
|
||||
|
|
||||
Project Structure |
|
||||
|
|
||||
├── index.html # Main HTML shell |
|
||||
├── README.md # This documentation |
|
||||
├── Visualization_Start.bat # Script to start the local server |
|
||||
├── python_check.bat # Script to check Python installation |
|
||||
├── Visualizer_quick_start_Guide.html # Separate HTML quick start guide |
|
||||
├── favicon.png # Browser tab icon |
|
||||
└── src/ |
|
||||
├── constants.js # Shared constants (radar bounds, FPS) |
|
||||
├── dataExplorer.js # NEW: Logic for the Data Explorer panel |
|
||||
├── db.js # IndexedDB logic for caching files |
|
||||
├── dom.js # DOM element references and UI update functions |
|
||||
├── drawUtils.js # p5.js drawing helpers (points, tracks, axes, legends) |
|
||||
├── fileParsers.js # Post-processing logic for parsed JSON |
|
||||
├── main.js # Main application entry point, event wiring, initialization |
|
||||
├── modal.js # Logic for pop-up modal dialogs & progress bar |
|
||||
├── parser.worker.js # Web Worker for background JSON parsing (uses Clarinet.js) |
|
||||
├── state.js # Centralized application state management object (appState) |
|
||||
├── sync.js # Core animation loop and playback synchronization logic |
|
||||
├── theme.js # Dark/Light mode theme switching logic |
|
||||
├── utils.js # General utility functions (binary search, timestamp parsing, formatting) |
|
||||
└── p5/ |
|
||||
├── radarSketch.js # p5.js sketch for the main radar visualization |
|
||||
├── speedGraphSketch.js # p5.js sketch for the speed graph |
|
||||
└── zoomSketch.js # p5.js sketch for the magnified zoom window ("GOD MODE") |
|
||||
├── tests/ # Simple unit tests (optional) |
|
||||
│ ├── test-runner.html |
|
||||
│ ├── utils.test.js |
|
||||
│ └── fileParsers.test.js |
|
||||
└── context.md # Detailed technical overview for AI assistance |
|
||||
|
|
||||
|
|
||||
How to Use the Application |
|
||||
|
|
||||
(Usage instructions remain largely similar, with additions for the Data Explorer) |
|
||||
|
|
||||
Load Files: Use "Load JSON"/"Load Video" buttons or Drag & Drop. Offset calculated automatically from filenames (fHist_...json, WIN_...mp4). |
|
||||
|
|
||||
Playback: Use UI buttons (<kbd>Space</kbd>), timeline slider (drag, hover, scroll wheel), or arrow keys (←/→). |
|
||||
|
|
||||
Adjust Offset: Manually enter offset (ms, +ve if radar lags) and press Enter. |
|
||||
|
|
||||
Adjust Speed: Use the "Speed" slider. |
|
||||
|
|
||||
Use Sidebar (<kbd>M</kbd>): Access toggles (Color modes <kbd>1-4</kbd>/<kbd>S</kbd>, Tracks <kbd>T</kbd>, Details <kbd>D</kbd>, Zoom <kbd>G</kbd>, Predicted Pos <kbd>P</kbd>, Debug <kbd>A</kbd>, Raw Only <kbd>C</kbd>, Confirmed Only), SNR range, TTC customization. |
|
||||
|
|
||||
Data Explorer (<kbd>I</kbd> / Canvas Click): |
|
||||
|
|
||||
Press <kbd>I</kbd> or click the main radar canvas to open/close the panel. |
|
||||
|
|
||||
View current frame data structure in the Tree View. |
|
||||
|
|
||||
If applicable data is sent (e.g., pointCloud via canvas click), view it in the Grid View. Sort/filter columns. |
|
||||
|
|
||||
In Grid View, click a column header, then click "Plot Selected Column" to see a line graph in the Plot View. |
|
||||
|
|
||||
Session Management: Use "Save/Load Session", "Clear Cache". Load requires files in cache. |
|
||||
|
|
||||
Other Shortcuts: Theme <kbd>Q</kbd>, Reset <kbd>R</kbd>. |
|
||||
@ -0,0 +1,31 @@ |
|||||
|
import http.server |
||||
|
import socketserver |
||||
|
import os |
||||
|
import sys |
||||
|
|
||||
|
# Ensure the server runs from the same directory as the script |
||||
|
os.chdir(os.path.dirname(os.path.abspath(__file__))) |
||||
|
|
||||
|
PORT = 8000 |
||||
|
|
||||
|
class MyHTTPRequestHandler(http.server.SimpleHTTPRequestHandler): |
||||
|
def end_headers(self): |
||||
|
# Disable caching for all files to ensure latest assets are loaded |
||||
|
self.send_header('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0') |
||||
|
self.send_header('Pragma', 'no-cache') |
||||
|
self.send_header('Expires', '0') |
||||
|
super().end_headers() |
||||
|
|
||||
|
Handler = MyHTTPRequestHandler |
||||
|
|
||||
|
print(f"Starting server on http://127.0.0.1:{PORT}") |
||||
|
print("Working directory:", os.getcwd()) |
||||
|
print("Cache-busting enabled: Browser will always fetch latest files.") |
||||
|
|
||||
|
# Explicitly bind to 127.0.0.1 for better reliability in offline/local environments |
||||
|
with socketserver.TCPServer(("127.0.0.1", PORT), Handler) as httpd: |
||||
|
try: |
||||
|
httpd.serve_forever() |
||||
|
except KeyboardInterrupt: |
||||
|
print("\nServer stopped.") |
||||
|
httpd.server_close() |
||||
@ -0,0 +1,24 @@ |
|||||
|
// This file centralizes all debug logging flags for the application.
|
||||
|
// To enable a specific set of logs, set the corresponding flag to `true`.
|
||||
|
// These flags can also be modified at runtime via the browser console
|
||||
|
// by accessing the global `debugFlags` object (e.g., `debugFlags.sync = true`).
|
||||
|
|
||||
|
export const debugFlags = { |
||||
|
// Logs from videoFrameCallback and animationLoop in sync.js
|
||||
|
sync: false, |
||||
|
|
||||
|
// Logs from the main p5.js draw() functions (e.g., radarSketch.js)
|
||||
|
drawing: false, |
||||
|
|
||||
|
// Logs related to file loading, parsing, and caching
|
||||
|
fileLoading: false, |
||||
|
|
||||
|
// Logs from the SpeedGraph p5 sketch (density info, etc.)
|
||||
|
speedGraph: false, |
||||
|
|
||||
|
// If true, file caching blocks the main thread for debugging.
|
||||
|
CACHE_BLOCKING: false, |
||||
|
|
||||
|
VIDEO_LOAD_TIMEOUT: 10000, // 10 seconds
|
||||
|
VIDEO_LOAD_RETRIES: 1, // Number of retries if loading fails
|
||||
|
}; |
||||
@ -0,0 +1,508 @@ |
|||||
|
import { appState } from "./state.js"; |
||||
|
import { debugFlags } from "./debug.js"; |
||||
|
import { saveFileWithMetadata, loadManualOffset, deleteManualOffset } from "./db.js"; |
||||
|
import { parseVisualizationJson } from "./fileParsers.js"; |
||||
|
import { |
||||
|
showLoadingModal, |
||||
|
updateLoadingModal, |
||||
|
hideModal, |
||||
|
showModal, |
||||
|
} from "./modal.js"; |
||||
|
import { |
||||
|
precomputeRadarVideoSync, |
||||
|
extractTimestampInfo, |
||||
|
parseTimestamp, |
||||
|
} from "./utils.js"; |
||||
|
import { resetVisualization } from "./sync.js"; |
||||
|
import { radarSketch } from "./p5/radarSketch.js"; |
||||
|
import { speedGraphSketch } from "./p5/speedGraphSketch.js"; |
||||
|
import { zoomSketch } from "./p5/zoomSketch.js"; |
||||
|
import { |
||||
|
videoPlayer, |
||||
|
videoPlaceholder, |
||||
|
canvasPlaceholder, |
||||
|
featureToggles, |
||||
|
speedGraphPlaceholder, |
||||
|
snrMinInput, |
||||
|
snrMaxInput, |
||||
|
autoOffsetIndicator, |
||||
|
offsetInput, |
||||
|
speedSlider, |
||||
|
updatePersistentOverlays, |
||||
|
updateDebugOverlay, |
||||
|
resetUIForNewLoad, |
||||
|
startScreenModal, |
||||
|
} from "./dom.js"; |
||||
|
|
||||
|
import { forceResyncWithOffset } from "./sync.js"; |
||||
|
/** |
||||
|
* This is the main handler for both manual clicks and drag-and-drop. |
||||
|
* It identifies the files and triggers the unified processing pipeline. |
||||
|
*/ |
||||
|
export function handleFiles(files, fromCache = false) { |
||||
|
// Identify new files from the input
|
||||
|
let incomingJson = null; |
||||
|
let incomingVideo = null; |
||||
|
|
||||
|
Array.from(files).forEach((file) => { |
||||
|
if (file.name.endsWith(".json")) { |
||||
|
incomingJson = file; |
||||
|
} |
||||
|
if (file.type.startsWith("video/")) { |
||||
|
incomingVideo = file; |
||||
|
} |
||||
|
}); |
||||
|
|
||||
|
// If no valid files were dropped, do nothing
|
||||
|
if (!incomingJson && !incomingVideo) return; |
||||
|
|
||||
|
// Trigger the pipeline with the identified files
|
||||
|
processFilePipeline(incomingJson, incomingVideo, fromCache); |
||||
|
} |
||||
|
|
||||
|
async function processFilePipeline(jsonFile, videoFile, fromCache) { |
||||
|
// 0. Reset the UI to a clean state before processing anything.
|
||||
|
// Pass 'true' if a new video is present, 'false' if we should try to keep the old one.
|
||||
|
const isNewVideo = !!videoFile; |
||||
|
resetUIForNewLoad(isNewVideo); |
||||
|
|
||||
|
// 1. Show the unified loading modal.
|
||||
|
showLoadingModal("Processing files..."); |
||||
|
|
||||
|
const cachePromises = []; |
||||
|
|
||||
|
// --- PART A: Setup Filenames & Cache (Moved Up) ---
|
||||
|
if (jsonFile) { |
||||
|
appState.jsonFilename = jsonFile.name; |
||||
|
localStorage.setItem("jsonFilename", appState.jsonFilename); |
||||
|
if (!fromCache) { |
||||
|
const savePromise = saveFileWithMetadata("json", jsonFile).catch((e) => |
||||
|
console.warn(`Non-blocking cache save failed for JSON:`, e) |
||||
|
); |
||||
|
if (debugFlags.CACHE_BLOCKING) { |
||||
|
await savePromise; |
||||
|
} else { |
||||
|
cachePromises.push(savePromise); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
if (videoFile) { |
||||
|
appState.videoFilename = videoFile.name; |
||||
|
localStorage.setItem("videoFilename", appState.videoFilename); |
||||
|
// CRITICAL FIX: Reset the videoMissing flag when a new video is being loaded.
|
||||
|
appState.videoMissing = false; |
||||
|
|
||||
|
if (!fromCache) { |
||||
|
const savePromise = saveFileWithMetadata("video", videoFile).catch((e) => |
||||
|
console.warn(`Non-blocking cache save failed for Video:`, e) |
||||
|
); |
||||
|
cachePromises.push(savePromise); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// --- PART B: Calculate Offset (Moved Up) ---
|
||||
|
// Critical: This must run BEFORE JSON parsing so valid start times are available.
|
||||
|
await calculateAndSetOffset(); |
||||
|
|
||||
|
// --- PART C: Handle JSON Parsing ---
|
||||
|
if (jsonFile) { |
||||
|
// Reset old visualization data immediately
|
||||
|
appState.vizData = null; |
||||
|
// Pause P5 loop to prevent errors while data is missing
|
||||
|
if (appState.p5_instance) appState.p5_instance.noLoop(); |
||||
|
|
||||
|
// Parse JSON
|
||||
|
const worker = new Worker("./src/parser.worker.js"); |
||||
|
const parsedData = await new Promise((resolve, reject) => { |
||||
|
worker.onmessage = (e) => { |
||||
|
const { type, data, percent, message } = e.data; |
||||
|
if (type === "progress") { |
||||
|
updateLoadingModal(percent * 0.8, `Parsing JSON (${percent}%)...`); |
||||
|
} else if (type === "complete") { |
||||
|
worker.terminate(); |
||||
|
resolve(data); |
||||
|
} else if (type === "error") { |
||||
|
worker.terminate(); |
||||
|
reject(new Error(message)); |
||||
|
} |
||||
|
}; |
||||
|
worker.postMessage({ file: jsonFile }); |
||||
|
}); |
||||
|
|
||||
|
// Post-process JSON with correct dates
|
||||
|
const result = await parseVisualizationJson( |
||||
|
parsedData, |
||||
|
appState.radarStartTimeMs, |
||||
|
appState.videoStartDate |
||||
|
); |
||||
|
|
||||
|
if (result.error) { |
||||
|
hideModal(); |
||||
|
showModal(result.error); |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
appState.vizData = result.data; |
||||
|
appState.globalMinSnr = result.minSnr; |
||||
|
appState.globalMaxSnr = result.maxSnr; |
||||
|
} |
||||
|
|
||||
|
// --- PART D: Precompute Sync ---
|
||||
|
// Bake the offset into the data (needs vizData from Part C and offset from Part B)
|
||||
|
if (appState.vizData) { |
||||
|
precomputeRadarVideoSync(appState.vizData, appState.offset); |
||||
|
} |
||||
|
|
||||
|
// --- PART E: Load Video (if new) ---
|
||||
|
if (videoFile) { |
||||
|
const videoLoaded = await loadVideo(videoFile); |
||||
|
if (!videoLoaded) { |
||||
|
appState.videoMissing = true; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// --- PART F: Finalize UI ---
|
||||
|
finalizeSetup(); |
||||
|
|
||||
|
if (!appState.videoMissing) { |
||||
|
updateLoadingModal(100, "Complete!"); |
||||
|
setTimeout(() => { |
||||
|
hideModal(); |
||||
|
startScreenModal.classList.add("hidden"); |
||||
|
}, 300); |
||||
|
} |
||||
|
|
||||
|
// Log the results of the non-blocking cache operations once they complete.
|
||||
|
if (cachePromises.length > 0) { |
||||
|
Promise.allSettled(cachePromises).then((results) => { |
||||
|
console.log("Non-blocking cache operations finished:", results); |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
|
||||
|
// Encapsulates the specific logic for loading a video file into the player
|
||||
|
let retries = 0; |
||||
|
function loadVideo(file, isRetry = false) { |
||||
|
return new Promise(async (resolve) => { |
||||
|
let metadataLoaded = false; |
||||
|
let loadTimeout; |
||||
|
|
||||
|
// Before creating a new URL, revoke the old one if it exists.
|
||||
|
if (!isRetry && appState.videoObjectUrl) { |
||||
|
URL.revokeObjectURL(appState.videoObjectUrl); |
||||
|
appState.videoObjectUrl = null; |
||||
|
} |
||||
|
|
||||
|
const fileURL = isRetry ? videoPlayer.src : URL.createObjectURL(file); |
||||
|
|
||||
|
const cleanup = () => { |
||||
|
clearTimeout(loadTimeout); |
||||
|
videoPlayer.removeEventListener("loadedmetadata", onMetadataLoaded); |
||||
|
videoPlayer.removeEventListener("canplaythrough", onCanPlayThrough); |
||||
|
videoPlayer.removeEventListener("error", onError); |
||||
|
}; |
||||
|
|
||||
|
const onMetadataLoaded = () => { |
||||
|
metadataLoaded = true; |
||||
|
updateLoadingModal(95, "Finalizing visualization..."); |
||||
|
}; |
||||
|
|
||||
|
const onCanPlayThrough = () => { |
||||
|
cleanup(); |
||||
|
resolve(true); |
||||
|
}; |
||||
|
|
||||
|
const handleTimeout = async () => { |
||||
|
if (metadataLoaded) { |
||||
|
console.warn( |
||||
|
"Video 'canplaythrough' event timed out, but 'loadedmetadata' fired. Proceeding with playback." |
||||
|
); |
||||
|
appState.videoReadyByFallback = true; |
||||
|
cleanup(); |
||||
|
resolve(true); |
||||
|
} else { |
||||
|
// Neither event fired, video is likely broken.
|
||||
|
await hideModal(); // Hide the loading modal first and wait for it to finish.
|
||||
|
const choice = await showModal( |
||||
|
"Video is taking too long to load. It might be corrupted.", |
||||
|
true, // isConfirm
|
||||
|
{ ok: "Retry", cancel: "Continue without Video" } |
||||
|
); |
||||
|
|
||||
|
if (choice) { // Retry
|
||||
|
if (retries < debugFlags.VIDEO_LOAD_RETRIES) { |
||||
|
retries++; |
||||
|
console.log(`Retrying video load... (Attempt ${retries})`); |
||||
|
showLoadingModal(`Retrying video load...`); |
||||
|
videoPlayer.load(); // Tell the video element to re-fetch
|
||||
|
resolve(loadVideo(file, true)); // Recurse
|
||||
|
} else { |
||||
|
await showModal("Video load failed after multiple retries."); |
||||
|
resolve(false); // Failed to load
|
||||
|
} |
||||
|
} else { // Continue without video
|
||||
|
console.warn("User opted to continue without video."); |
||||
|
cleanup(); |
||||
|
// Revoke URL to free memory if we're giving up on it
|
||||
|
if (videoPlayer.src.startsWith('blob:')) { |
||||
|
URL.revokeObjectURL(videoPlayer.src); |
||||
|
appState.videoObjectUrl = null; // Clear from state
|
||||
|
} |
||||
|
videoPlayer.src = ""; |
||||
|
videoPlayer.classList.add("hidden"); |
||||
|
videoPlaceholder.classList.remove("hidden"); |
||||
|
resolve(false); // Signal that video is not loaded
|
||||
|
} |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
const onError = async (e) => { |
||||
|
console.error("Video loading error:", e); |
||||
|
cleanup(); |
||||
|
await hideModal(); // Await is CRITICAL to prevent a race condition with the next modal.
|
||||
|
showModal("Error loading video file. It may be an unsupported format or corrupted."); |
||||
|
resolve(false); |
||||
|
}; |
||||
|
|
||||
|
// Attach listeners
|
||||
|
videoPlayer.addEventListener("loadedmetadata", onMetadataLoaded, { once: true }); |
||||
|
videoPlayer.addEventListener("canplaythrough", onCanPlayThrough, { once: true }); |
||||
|
videoPlayer.addEventListener("error", onError, { once: true }); |
||||
|
|
||||
|
// Start the timeout
|
||||
|
loadTimeout = setTimeout(handleTimeout, debugFlags.VIDEO_LOAD_TIMEOUT); |
||||
|
|
||||
|
// Apply source only if it's not a retry
|
||||
|
if (!isRetry) { |
||||
|
retries = 0; // Reset retry counter for new files
|
||||
|
setupVideoPlayer(fileURL); |
||||
|
} |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
function finalizeSetup() { |
||||
|
// CRITICAL FIX: Always reset the visualization state before redrawing.
|
||||
|
// This pauses the video and resets the timeline, ensuring a clean slate for the new data.
|
||||
|
resetVisualization(); |
||||
|
// 1. Manage Placeholders & Visibility
|
||||
|
// If we have data (vizData), we show the canvas container.
|
||||
|
if (appState.vizData) { |
||||
|
canvasPlaceholder.style.display = "none"; |
||||
|
featureToggles.classList.remove("hidden"); |
||||
|
} else { |
||||
|
// If there's no viz data (e.g., video-only load), hide the canvas
|
||||
|
canvasPlaceholder.style.display = ""; // Show placeholder
|
||||
|
featureToggles.classList.add("hidden"); |
||||
|
if (appState.p5_instance) { |
||||
|
appState.p5_instance.noLoop(); |
||||
|
} |
||||
|
// If we don't have data yet (video only), we might keep the placeholder or show an empty canvas?
|
||||
|
// Current behavior: keep placeholder until JSON loads.
|
||||
|
} |
||||
|
|
||||
|
// 2. Initialize/Update P5 Sketches
|
||||
|
// We check if they exist; if not, create them. If they do, they will read the new appState on next draw.
|
||||
|
if (!appState.p5_instance) { |
||||
|
appState.p5_instance = new p5(radarSketch); |
||||
|
} else { |
||||
|
// If it existed, ensure it's up to date.
|
||||
|
// CRITICAL: Do NOT call .loop(). The app uses a custom animationLoop in sync.js.
|
||||
|
appState.p5_instance.redraw(); |
||||
|
} |
||||
|
|
||||
|
if (!appState.zoomSketchInstance) { |
||||
|
appState.zoomSketchInstance = new p5(zoomSketch, "zoom-canvas-container"); |
||||
|
} |
||||
|
|
||||
|
// 3. Setup Speed Graph
|
||||
|
if (appState.vizData) { |
||||
|
speedGraphPlaceholder.classList.add("hidden"); |
||||
|
|
||||
|
if (!appState.speedGraphInstance) { |
||||
|
appState.speedGraphInstance = new p5(speedGraphSketch); |
||||
|
} |
||||
|
|
||||
|
// Update speed graph with new data + video duration
|
||||
|
// Determine the most appropriate duration for the graph's X-axis.
|
||||
|
let finalDuration = 0; |
||||
|
let jsonDuration = 0; |
||||
|
|
||||
|
// 1. Calculate duration from the JSON data itself as a reliable baseline.
|
||||
|
if (appState.vizData.radarFrames && appState.vizData.radarFrames.length > 0) { |
||||
|
const lastFrame = appState.vizData.radarFrames[appState.vizData.radarFrames.length - 1]; |
||||
|
jsonDuration = lastFrame.timestamp / 1000.0; |
||||
|
} |
||||
|
|
||||
|
// 2. Get video duration, normalizing invalid values.
|
||||
|
let videoDuration = appState.videoMissing ? 0 : (videoPlayer.duration || 0); |
||||
|
if (!videoDuration || isNaN(videoDuration) || videoDuration <= 0) { |
||||
|
videoDuration = 0; |
||||
|
} |
||||
|
|
||||
|
// 3. Set the graph's duration. Prioritize JSON duration, but clip it
|
||||
|
// to the video's duration if a video is present and shorter.
|
||||
|
finalDuration = jsonDuration; |
||||
|
if (videoDuration > 0 && jsonDuration > videoDuration) { |
||||
|
finalDuration = jsonDuration; |
||||
|
} |
||||
|
|
||||
|
appState.speedGraphInstance.setData(appState.vizData, finalDuration); |
||||
|
appState.speedGraphInstance.redraw(); |
||||
|
} |
||||
|
|
||||
|
// 4. Update UI Overlays
|
||||
|
// Manually update overlays so they are visible immediately.
|
||||
|
updatePersistentOverlays(videoPlayer.currentTime); |
||||
|
updateDebugOverlay(videoPlayer.currentTime); |
||||
|
|
||||
|
// 5. Update SNR Inputs
|
||||
|
if (appState.vizData) { |
||||
|
snrMinInput.value = appState.globalMinSnr.toFixed(1); |
||||
|
snrMaxInput.value = appState.globalMaxSnr.toFixed(1); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// Sets up the video player with the given file URL.
|
||||
|
function setupVideoPlayer(fileURL) { |
||||
|
videoPlayer.src = fileURL; |
||||
|
videoPlayer.classList.remove("hidden"); |
||||
|
videoPlaceholder.classList.add("hidden"); |
||||
|
videoPlayer.playbackRate = parseFloat(speedSlider.value); |
||||
|
videoPlayer.controls = false; |
||||
|
videoPlayer.muted = false; |
||||
|
|
||||
|
// Store the new object URL if it's a blob
|
||||
|
if (fileURL.startsWith("blob:")) { |
||||
|
appState.videoObjectUrl = fileURL; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
async function calculateAndSetOffset() { |
||||
|
const jsonTimestampInfo = extractTimestampInfo(appState.jsonFilename); |
||||
|
const videoTimestampInfo = extractTimestampInfo(appState.videoFilename); |
||||
|
|
||||
|
let videoDate = null; |
||||
|
if (videoTimestampInfo) { |
||||
|
videoDate = parseTimestamp( |
||||
|
videoTimestampInfo.timestampStr, |
||||
|
videoTimestampInfo.format |
||||
|
); |
||||
|
appState.videoStartDate = videoDate; // Store for potential future use
|
||||
|
} |
||||
|
|
||||
|
let jsonDate = null; |
||||
|
if (jsonTimestampInfo) { |
||||
|
jsonDate = parseTimestamp( |
||||
|
jsonTimestampInfo.timestampStr, |
||||
|
jsonTimestampInfo.format |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
// 1. Try to load a manually saved offset for this specific file pair.
|
||||
|
// We use the JSON filename as the primary key.
|
||||
|
const savedOffset = appState.jsonFilename ? await loadManualOffset(appState.jsonFilename) : null; |
||||
|
|
||||
|
if (savedOffset !== null) { |
||||
|
console.log(`Applying saved manual offset: ${savedOffset}ms`); |
||||
|
appState.offset = savedOffset; |
||||
|
if (jsonDate) { |
||||
|
appState.radarStartTimeMs = jsonDate.getTime(); |
||||
|
} |
||||
|
// Update UI
|
||||
|
offsetInput.value = appState.offset; |
||||
|
|
||||
|
// Show "Manual" indicator
|
||||
|
autoOffsetIndicator.textContent = "Manual"; |
||||
|
autoOffsetIndicator.className = "text-xs font-bold ml-2 text-gray-500"; // Gray for manual
|
||||
|
autoOffsetIndicator.classList.remove("hidden"); |
||||
|
|
||||
|
localStorage.setItem("visualizerOffset", appState.offset); |
||||
|
return; // Exit early, skipping auto-calc
|
||||
|
} |
||||
|
|
||||
|
let calculatedOffset = 0; |
||||
|
// We need both dates to calculate an offset.
|
||||
|
if (jsonDate && videoDate) { |
||||
|
appState.radarStartTimeMs = jsonDate.getTime(); |
||||
|
const offset = jsonDate.getTime() - videoDate.getTime(); |
||||
|
|
||||
|
if (isNaN(offset) || Math.abs(offset) > 30000) { |
||||
|
console.warn(`Calculated offset of ${offset}ms is invalid or exceeds 30s threshold. Defaulting to 0.`); |
||||
|
calculatedOffset = 0; |
||||
|
|
||||
|
// Show "Default" or "Out of Range" indicator
|
||||
|
autoOffsetIndicator.textContent = isNaN(offset) ? "Default" : "Out of Range"; |
||||
|
autoOffsetIndicator.className = "text-xs font-bold ml-2 text-yellow-600"; // Dark Yellow
|
||||
|
autoOffsetIndicator.classList.remove("hidden"); |
||||
|
|
||||
|
} else { |
||||
|
calculatedOffset = offset; |
||||
|
|
||||
|
// Show "Auto" indicator
|
||||
|
autoOffsetIndicator.textContent = "Auto"; |
||||
|
autoOffsetIndicator.className = "text-xs font-bold ml-2 text-green-500"; // Green
|
||||
|
autoOffsetIndicator.classList.remove("hidden"); |
||||
|
|
||||
|
console.log(`Auto-calculated offset: ${calculatedOffset} ms`); |
||||
|
} |
||||
|
} else if (jsonDate) { |
||||
|
// If we have JSON but no video, we set start time but offset is 0
|
||||
|
appState.radarStartTimeMs = jsonDate.getTime(); |
||||
|
// No specific indicator needed for JSON-only default 0, or could show "Default"
|
||||
|
autoOffsetIndicator.classList.add("hidden"); |
||||
|
} |
||||
|
|
||||
|
appState.offset = calculatedOffset; |
||||
|
offsetInput.value = appState.offset; |
||||
|
localStorage.setItem("visualizerOffset", appState.offset); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Re-calculates and applies the automatic offset based on filenames. |
||||
|
* This function is triggered by user actions like clicking the 'Manual' indicator |
||||
|
* or using a keyboard shortcut to revert a manual offset. |
||||
|
*/ |
||||
|
export function revertToAutoOffset() { |
||||
|
// 1. Calculate the automatic offset.
|
||||
|
const jsonTimestampInfo = extractTimestampInfo(appState.jsonFilename); |
||||
|
const videoTimestampInfo = extractTimestampInfo(appState.videoFilename); |
||||
|
|
||||
|
let calculatedOffset = 0; |
||||
|
let indicatorText = "Default"; |
||||
|
let indicatorClass = "text-xs font-bold ml-2 text-yellow-600"; // Default to yellow
|
||||
|
|
||||
|
if (jsonTimestampInfo && videoTimestampInfo) { |
||||
|
const jsonDate = parseTimestamp(jsonTimestampInfo.timestampStr, jsonTimestampInfo.format); |
||||
|
const videoDate = parseTimestamp(videoTimestampInfo.timestampStr, videoTimestampInfo.format); |
||||
|
|
||||
|
if (jsonDate && videoDate) { |
||||
|
const offset = jsonDate.getTime() - videoDate.getTime(); |
||||
|
if (isNaN(offset) || Math.abs(offset) > 30000) { |
||||
|
calculatedOffset = 0; |
||||
|
indicatorText = "Out of Range"; |
||||
|
} else { |
||||
|
calculatedOffset = offset; |
||||
|
indicatorText = "Auto"; |
||||
|
indicatorClass = "text-xs font-bold ml-2 text-green-500"; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// 2. Update the input box with the new value.
|
||||
|
offsetInput.value = calculatedOffset; |
||||
|
|
||||
|
// 3. Delete any saved manual offset so future loads default to "Auto" logic.
|
||||
|
if (appState.jsonFilename) { |
||||
|
deleteManualOffset(appState.jsonFilename); |
||||
|
} |
||||
|
|
||||
|
// 4. Call the resync function with saveToDb = false.
|
||||
|
forceResyncWithOffset(false); |
||||
|
|
||||
|
// 5. After resyncing, set the correct indicator text and style.
|
||||
|
autoOffsetIndicator.textContent = indicatorText; |
||||
|
autoOffsetIndicator.className = indicatorClass; |
||||
|
autoOffsetIndicator.classList.remove("hidden"); |
||||
|
} |
||||
@ -0,0 +1,201 @@ |
|||||
|
import { appState } from "./state.js"; |
||||
|
import { |
||||
|
playPauseBtn, |
||||
|
videoPlayer, |
||||
|
toggleSnrColor, |
||||
|
toggleClusterColor, |
||||
|
toggleInlierColor, |
||||
|
toggleStationaryColor, |
||||
|
themeToggleBtn, |
||||
|
toggleTracks, |
||||
|
toggleVelocity, |
||||
|
toggleCloseUp, |
||||
|
togglePredictedPos, |
||||
|
toggleDebugOverlay, |
||||
|
toggleDebug2Overlay, |
||||
|
collapsibleMenu, |
||||
|
toggleMenuBtn, |
||||
|
closeMenuBtn, |
||||
|
updatePersistentOverlays, |
||||
|
} from "./dom.js"; |
||||
|
import { updateFrame, resetVisualization } from "./sync.js"; |
||||
|
import { VIDEO_FPS } from "./constants.js"; |
||||
|
import { findRadarFrameIndexForTime } from "./utils.js"; |
||||
|
|
||||
|
function handleKeyDown(event) { |
||||
|
// --- FIX APPLIED HERE ---
|
||||
|
// We only want to block shortcuts if the user is actively typing in a text or number input.
|
||||
|
// This allows shortcuts to work even when other elements, like the timeline slider, are focused.
|
||||
|
const isTextInputFocused = |
||||
|
event.target.tagName === "INPUT" && |
||||
|
(event.target.type === "text" || event.target.type === "number"); |
||||
|
if (isTextInputFocused) { |
||||
|
return; |
||||
|
} |
||||
|
// --- END OF FIX ---
|
||||
|
|
||||
|
const key = event.key; |
||||
|
// We can add any new shortcut keys to this array.
|
||||
|
const recognizedKeys = [ |
||||
|
"ArrowRight", |
||||
|
"ArrowLeft", |
||||
|
"ArrowUp", |
||||
|
"ArrowDown", |
||||
|
" ", |
||||
|
"1", |
||||
|
"2", |
||||
|
"3", |
||||
|
"4", |
||||
|
"t", |
||||
|
"d", |
||||
|
"g", |
||||
|
"r", |
||||
|
"p", |
||||
|
"a", |
||||
|
"s", |
||||
|
"m", |
||||
|
"q", |
||||
|
"c", |
||||
|
]; |
||||
|
|
||||
|
// Keys that function globally, even without loaded data
|
||||
|
const globalKeys = ["q", "m"]; |
||||
|
|
||||
|
if (!recognizedKeys.includes(key)) { |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
// If no data is loaded, block keys unless they are global (like theme or menu)
|
||||
|
if (!appState.vizData && !globalKeys.includes(key)) { |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
event.preventDefault(); |
||||
|
|
||||
|
// --- Spacebar for Play/Pause ---
|
||||
|
if (key === " ") { |
||||
|
playPauseBtn.click(); |
||||
|
} |
||||
|
|
||||
|
// --- Arrow keys for frame-by-frame seeking ---
|
||||
|
if (key === "ArrowRight" || key === "ArrowLeft") { |
||||
|
if (appState.isPlaying) { |
||||
|
playPauseBtn.click(); |
||||
|
} |
||||
|
let newFrame = appState.currentFrame; |
||||
|
if (key === "ArrowRight") { |
||||
|
newFrame = Math.min( |
||||
|
appState.vizData.radarFrames.length - 1, |
||||
|
appState.currentFrame + 1 |
||||
|
); |
||||
|
} else if (key === "ArrowLeft") { |
||||
|
newFrame = Math.max(0, appState.currentFrame - 1); |
||||
|
} |
||||
|
if (newFrame !== appState.currentFrame) { |
||||
|
updateFrame(newFrame, true); |
||||
|
// Manually trigger redraws since the animation loop is paused
|
||||
|
// This is the fix to ensure the radar plot updates on seek.
|
||||
|
if (appState.p5_instance) appState.p5_instance.redraw(); |
||||
|
if (appState.speedGraphInstance) appState.speedGraphInstance.redraw(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// --- Arrow keys for video frame-by-frame seeking ---
|
||||
|
if (key === "ArrowUp" || key === "ArrowDown") { |
||||
|
if (appState.isPlaying) { |
||||
|
playPauseBtn.click(); // Pause playback to allow for precise stepping
|
||||
|
} |
||||
|
|
||||
|
const frameDuration = 1 / VIDEO_FPS; |
||||
|
let newVideoTime = videoPlayer.currentTime; |
||||
|
|
||||
|
if (key === "ArrowUp") { |
||||
|
newVideoTime += frameDuration; |
||||
|
} else if (key === "ArrowDown") { |
||||
|
newVideoTime -= frameDuration; |
||||
|
} |
||||
|
|
||||
|
// Clamp the new time to be within the video's bounds
|
||||
|
videoPlayer.currentTime = Math.max( |
||||
|
0, |
||||
|
Math.min(newVideoTime, videoPlayer.duration) |
||||
|
); |
||||
|
|
||||
|
// Find the corresponding radar frame for the new video time
|
||||
|
const newFrameIndex = findRadarFrameIndexForTime( |
||||
|
videoPlayer.currentTime, |
||||
|
appState.vizData |
||||
|
); |
||||
|
|
||||
|
// Update the application state, but don't force another video seek
|
||||
|
updateFrame(newFrameIndex, false); |
||||
|
|
||||
|
// Manually trigger redraws since the animation loop is paused
|
||||
|
if (appState.p5_instance) appState.p5_instance.redraw(); |
||||
|
if (appState.speedGraphInstance) appState.speedGraphInstance.redraw(); |
||||
|
} |
||||
|
|
||||
|
// --- Number keys for color modes ---
|
||||
|
if (key >= "1" && key <= "4") { |
||||
|
const colorToggles = [ |
||||
|
toggleSnrColor, |
||||
|
toggleClusterColor, |
||||
|
toggleInlierColor, |
||||
|
toggleStationaryColor, |
||||
|
]; |
||||
|
const toggleIndex = parseInt(key) - 1; |
||||
|
if (colorToggles[toggleIndex]) { |
||||
|
colorToggles[toggleIndex].click(); |
||||
|
} |
||||
|
} |
||||
|
if (key === "q") { |
||||
|
themeToggleBtn.click(); |
||||
|
} |
||||
|
if (key === "t") { |
||||
|
toggleTracks.click(); |
||||
|
} |
||||
|
if (key === "d") { |
||||
|
toggleVelocity.click(); |
||||
|
} |
||||
|
if (key === "g") { |
||||
|
toggleCloseUp.click(); |
||||
|
} |
||||
|
if (key === "r") { |
||||
|
resetVisualization(); |
||||
|
} |
||||
|
if (key === "c") { |
||||
|
appState.isRawOnlyMode = !appState.isRawOnlyMode; |
||||
|
if (appState.p5_instance) { |
||||
|
appState.p5_instance.redraw(); |
||||
|
} |
||||
|
} |
||||
|
if (key === "p") { |
||||
|
togglePredictedPos.click(); |
||||
|
appState.p5_instance.redraw(); |
||||
|
} |
||||
|
if (key === "s") { |
||||
|
toggleSnrColor.click(); |
||||
|
} |
||||
|
if (key === "a") { |
||||
|
toggleDebugOverlay.click(); |
||||
|
toggleDebug2Overlay.click(); |
||||
|
updatePersistentOverlays(videoPlayer.currentTime); |
||||
|
// The 'a' key is a shortcut to toggle all debug overlays on/off.
|
||||
|
// The `updateDebugOverlay` and `updatePersistentOverlays` functions,
|
||||
|
// which are called by the toggle's 'change' event listener,
|
||||
|
// already handle the logic for showing/hiding the other overlays.
|
||||
|
} |
||||
|
if (key === "m") { |
||||
|
if (collapsibleMenu.classList.contains("-translate-x-full")) { |
||||
|
// If the menu is hidden (closed), trigger a click on the OPEN button.
|
||||
|
toggleMenuBtn.click(); |
||||
|
} else { |
||||
|
// If the menu is not hidden (it's open), trigger a click on the CLOSE button.
|
||||
|
closeMenuBtn.click(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
export function initKeyboardShortcuts() { |
||||
|
document.addEventListener("keydown", handleKeyDown); |
||||
|
} |
||||
978
steps/src/main.js
File diff suppressed because it is too large
View File
File diff suppressed because it is too large
View File
@ -0,0 +1,131 @@ |
|||||
|
import { appState } from "./state.js"; |
||||
|
import { showModal } from "./modal.js"; |
||||
|
import { loadFreshFileFromDB } from "./db.js"; |
||||
|
import { |
||||
|
offsetInput, |
||||
|
speedSlider, |
||||
|
snrMinInput, |
||||
|
snrMaxInput, |
||||
|
toggleSnrColor, |
||||
|
toggleClusterColor, |
||||
|
toggleInlierColor, |
||||
|
toggleStationaryColor, |
||||
|
toggleVelocity, |
||||
|
toggleTracks, |
||||
|
toggleEgoSpeed, |
||||
|
toggleFrameNorm, |
||||
|
toggleDebugOverlay, |
||||
|
toggleDebug2Overlay, |
||||
|
toggleCloseUp, |
||||
|
togglePredictedPos, |
||||
|
toggleCovariance, |
||||
|
toggleConfirmedOnly, |
||||
|
saveSessionBtn, |
||||
|
loadSessionBtn, |
||||
|
sessionFileInput, |
||||
|
} from "./dom.js"; |
||||
|
|
||||
|
function saveSession() { |
||||
|
if (!appState.jsonFilename && !appState.videoFilename) { |
||||
|
showModal("Nothing to save. Please load data files first."); |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
const sessionState = { |
||||
|
version: 1, |
||||
|
jsonFilename: appState.jsonFilename, |
||||
|
videoFilename: appState.videoFilename, |
||||
|
offset: offsetInput.value, |
||||
|
playbackSpeed: speedSlider.value, |
||||
|
snrMin: snrMinInput.value, |
||||
|
snrMax: snrMaxInput.value, |
||||
|
toggles: { |
||||
|
snrColor: toggleSnrColor.checked, |
||||
|
clusterColor: toggleClusterColor.checked, |
||||
|
inlierColor: toggleInlierColor.checked, |
||||
|
stationaryColor: toggleStationaryColor.checked, |
||||
|
velocity: toggleVelocity.checked, |
||||
|
tracks: toggleTracks.checked, |
||||
|
egoSpeed: toggleEgoSpeed.checked, |
||||
|
frameNorm: toggleFrameNorm.checked, |
||||
|
debugOverlay: toggleDebugOverlay.checked, |
||||
|
debug2Overlay: toggleDebug2Overlay.checked, |
||||
|
closeUp: toggleCloseUp.checked, |
||||
|
predictedPos: togglePredictedPos.checked, |
||||
|
covariance: toggleCovariance.checked, |
||||
|
confirmedOnly: toggleConfirmedOnly.checked, |
||||
|
}, |
||||
|
}; |
||||
|
|
||||
|
const sessionString = JSON.stringify(sessionState, null, 2); |
||||
|
const blob = new Blob([sessionString], { type: "application/json" }); |
||||
|
const url = URL.createObjectURL(blob); |
||||
|
|
||||
|
const now = new Date(); |
||||
|
const pad = (num) => String(num).padStart(2, "0"); |
||||
|
const timestamp = `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad( |
||||
|
now.getDate() |
||||
|
)}_${pad(now.getHours())}-${pad(now.getMinutes())}-${pad(now.getSeconds())}`;
|
||||
|
const defaultFilename = `visualizer-session_${timestamp}.json`; |
||||
|
|
||||
|
const a = document.createElement("a"); |
||||
|
a.href = url; |
||||
|
a.download = defaultFilename; |
||||
|
document.body.appendChild(a); |
||||
|
a.click(); |
||||
|
document.body.removeChild(a); |
||||
|
URL.revokeObjectURL(url); |
||||
|
} |
||||
|
|
||||
|
async function loadSession(file) { |
||||
|
if (!file) return; |
||||
|
|
||||
|
const reader = new FileReader(); |
||||
|
reader.onload = async (e) => { |
||||
|
try { |
||||
|
const sessionState = JSON.parse(e.target.result); |
||||
|
|
||||
|
if (sessionState.version !== 1 || !sessionState.jsonFilename) { |
||||
|
showModal("Error: Invalid or corrupted session file."); |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
const videoBlob = await loadFreshFileFromDB("video", sessionState.videoFilename); |
||||
|
const jsonBlob = await loadFreshFileFromDB("json", sessionState.jsonFilename); |
||||
|
|
||||
|
if (!jsonBlob || (sessionState.videoFilename && !videoBlob)) { |
||||
|
showModal(`Session load failed: The required data files are not in the application's cache.
|
||||
|
|
||||
|
Please manually load '${sessionState.jsonFilename}' and '${sessionState.videoFilename}' before loading this session.`);
|
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
localStorage.setItem("jsonFilename", sessionState.jsonFilename || ""); |
||||
|
localStorage.setItem("videoFilename", sessionState.videoFilename || ""); |
||||
|
localStorage.setItem("visualizerOffset", sessionState.offset || "0"); |
||||
|
localStorage.setItem("playbackSpeed", sessionState.playbackSpeed || "1"); |
||||
|
localStorage.setItem("snrMin", sessionState.snrMin || ""); |
||||
|
localStorage.setItem("snrMax", sessionState.snrMax || ""); |
||||
|
if (sessionState.toggles) { |
||||
|
localStorage.setItem("togglesState", JSON.stringify(sessionState.toggles)); |
||||
|
} |
||||
|
|
||||
|
showModal("Session files found in cache. The application will now reload.").then(() => { |
||||
|
window.location.reload(); |
||||
|
}); |
||||
|
} catch (error) { |
||||
|
showModal("Error: Could not parse the session file. It may be invalid."); |
||||
|
console.error("Session load error:", error); |
||||
|
} |
||||
|
}; |
||||
|
reader.readAsText(file); |
||||
|
} |
||||
|
|
||||
|
export function initSessionManagement() { |
||||
|
saveSessionBtn.addEventListener("click", saveSession); |
||||
|
loadSessionBtn.addEventListener("click", () => sessionFileInput.click()); |
||||
|
sessionFileInput.addEventListener("change", (event) => { |
||||
|
loadSession(event.target.files[0]); |
||||
|
event.target.value = ""; // Clear the input for future loads.
|
||||
|
}); |
||||
|
} |
||||
@ -0,0 +1,532 @@ |
|||||
|
import { appState } from "./state.js"; |
||||
|
import { formatTime } from "./utils.js"; |
||||
|
import { showModal } from "./modal.js"; |
||||
|
import { pausePlayback } from "./sync.js"; |
||||
|
import { |
||||
|
videoPlayer, |
||||
|
timelineSlider, |
||||
|
speedSlider, |
||||
|
speedDisplay, |
||||
|
toggleSnrColor, |
||||
|
toggleClusterColor, |
||||
|
toggleInlierColor, |
||||
|
toggleStationaryColor, |
||||
|
toggleVelocity, |
||||
|
toggleEgoSpeed, |
||||
|
toggleFrameNorm, |
||||
|
toggleTracks, |
||||
|
toggleDebugOverlay, |
||||
|
toggleDebug2Overlay, |
||||
|
toggleCloseUp, |
||||
|
toggleCovariance, |
||||
|
toggleVehicleDimensions, |
||||
|
snrMinInput, |
||||
|
snrMaxInput, |
||||
|
applySnrBtn, |
||||
|
timelineTooltip, |
||||
|
playPauseBtn, |
||||
|
updatePersistentOverlays, |
||||
|
updateDebugOverlay, |
||||
|
collapsibleMenu, |
||||
|
toggleMenuBtn, |
||||
|
closeMenuBtn, |
||||
|
menuScrim, |
||||
|
fullscreenBtn, |
||||
|
toggleConfirmedOnly, |
||||
|
shortcutsBtn, |
||||
|
shortcutsModal, |
||||
|
shortcutsModalCloseBtn, |
||||
|
userManualBtn, |
||||
|
guideModal, |
||||
|
guideModalCloseBtn, |
||||
|
codebaseBtn, |
||||
|
codebaseModal, |
||||
|
codebaseModalCloseBtn, |
||||
|
changelogBtn, |
||||
|
changelogModal, |
||||
|
changelogModalCloseBtn, |
||||
|
startUserManualBtn, |
||||
|
startCodebaseBtn, |
||||
|
startChangelogBtn, |
||||
|
} from "./dom.js"; |
||||
|
|
||||
|
// --- START: Resizable and Draggable Panel Logic ---
|
||||
|
export function makeDraggableAndResizable(panel, header, minWidth = 400, minHeight = 300) { |
||||
|
if (!panel || !header) return; |
||||
|
const resizers = panel.querySelectorAll('.resizer'); |
||||
|
|
||||
|
let original_width = 0; |
||||
|
let original_height = 0; |
||||
|
let original_x = 0; |
||||
|
let original_y = 0; |
||||
|
let original_mouse_x = 0; |
||||
|
let original_mouse_y = 0; |
||||
|
|
||||
|
// --- Persistence Logic ---
|
||||
|
const storageKey = `panel_pos_${panel.id}`; |
||||
|
|
||||
|
function savePosition() { |
||||
|
if (!panel.id) return; |
||||
|
const state = { |
||||
|
left: panel.style.left, |
||||
|
top: panel.style.top, |
||||
|
width: panel.style.width, |
||||
|
height: panel.style.height |
||||
|
}; |
||||
|
console.log(`Saving position for ${panel.id}`, state); |
||||
|
localStorage.setItem(storageKey, JSON.stringify(state)); |
||||
|
} |
||||
|
|
||||
|
function loadPosition() { |
||||
|
if (!panel.id) return; |
||||
|
const saved = localStorage.getItem(storageKey); |
||||
|
if (saved) { |
||||
|
try { |
||||
|
const state = JSON.parse(saved); |
||||
|
console.log(`Loading position for ${panel.id}`, state); |
||||
|
if (state.left) panel.style.left = state.left; |
||||
|
if (state.top) panel.style.top = state.top; |
||||
|
if (state.width) panel.style.width = state.width; |
||||
|
if (state.height) panel.style.height = state.height; |
||||
|
// Ensure it's still in view
|
||||
|
requestAnimationFrame(() => constrainToViewport()); |
||||
|
} catch (e) { console.error(`Failed to load position for ${panel.id}`, e); } |
||||
|
} else { |
||||
|
console.log(`No saved position found for ${panel.id}`); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// --- Auto-Focus (Bring to Front) ---
|
||||
|
panel.addEventListener('mousedown', () => { |
||||
|
document.querySelectorAll('#zoom-panel, #data-explorer-panel').forEach(p => { |
||||
|
p.style.zIndex = "30"; |
||||
|
}); |
||||
|
panel.style.zIndex = "40"; |
||||
|
}); |
||||
|
|
||||
|
loadPosition(); |
||||
|
|
||||
|
// --- Dragging Logic ---
|
||||
|
header.addEventListener('mousedown', (e) => { |
||||
|
// Prevent drag if clicking buttons
|
||||
|
if (e.target.tagName === 'BUTTON' || e.target.closest('button')) return; |
||||
|
e.preventDefault(); |
||||
|
|
||||
|
// Ensure panel floats on top
|
||||
|
panel.style.zIndex = 100; |
||||
|
|
||||
|
original_x = panel.offsetLeft; |
||||
|
original_y = panel.offsetTop; |
||||
|
original_mouse_x = e.pageX; |
||||
|
original_mouse_y = e.pageY; |
||||
|
document.body.classList.add('dragging'); |
||||
|
window.addEventListener('mousemove', dragPanel); |
||||
|
window.addEventListener('mouseup', stopDrag); |
||||
|
}); |
||||
|
|
||||
|
function dragPanel(e) { |
||||
|
const dx = e.pageX - original_mouse_x; |
||||
|
const dy = e.pageY - original_mouse_y; |
||||
|
panel.style.left = `${original_x + dx}px`; |
||||
|
panel.style.top = `${original_y + dy}px`; |
||||
|
} |
||||
|
|
||||
|
function stopDrag() { |
||||
|
document.body.classList.remove('dragging'); |
||||
|
window.removeEventListener('mousemove', dragPanel); |
||||
|
window.removeEventListener('mouseup', stopDrag); |
||||
|
savePosition(); |
||||
|
} |
||||
|
|
||||
|
// --- Resizing Logic ---
|
||||
|
resizers.forEach(resizer => { |
||||
|
resizer.addEventListener('mousedown', (e) => { |
||||
|
e.preventDefault(); |
||||
|
panel.style.zIndex = 100; |
||||
|
original_width = parseFloat(getComputedStyle(panel, null).getPropertyValue('width').replace('px', '')); |
||||
|
original_height = parseFloat(getComputedStyle(panel, null).getPropertyValue('height').replace('px', '')); |
||||
|
original_x = panel.getBoundingClientRect().left; |
||||
|
original_y = panel.getBoundingClientRect().top; |
||||
|
original_mouse_x = e.pageX; |
||||
|
original_mouse_y = e.pageY; |
||||
|
|
||||
|
const resizeFunc = (event) => resizePanel(event, resizer.classList); |
||||
|
|
||||
|
document.body.classList.add('resizing'); |
||||
|
window.addEventListener('mousemove', resizeFunc); |
||||
|
window.addEventListener('mouseup', () => { |
||||
|
document.body.classList.remove('resizing'); |
||||
|
window.removeEventListener('mousemove', resizeFunc); |
||||
|
savePosition(); |
||||
|
if (panel.id === 'zoom-panel' && appState.zoomSketchInstance) { |
||||
|
appState.zoomSketchInstance.handleContainerResize(); |
||||
|
} |
||||
|
}); |
||||
|
}); |
||||
|
}); |
||||
|
|
||||
|
function resizePanel(e, direction) { |
||||
|
if (direction.toString().includes('r')) { |
||||
|
const width = original_width + (e.pageX - original_mouse_x); |
||||
|
if (width > minWidth) panel.style.width = `${width}px`; |
||||
|
} |
||||
|
if (direction.toString().includes('b')) { |
||||
|
const height = original_height + (e.pageY - original_mouse_y); |
||||
|
if (height > minHeight) panel.style.height = `${height}px`; |
||||
|
} |
||||
|
if (direction.toString().includes('l')) { |
||||
|
const newWidth = original_width - (e.pageX - original_mouse_x); |
||||
|
if (newWidth > minWidth) { |
||||
|
panel.style.width = `${newWidth}px`; |
||||
|
panel.style.left = `${original_x + (e.pageX - original_mouse_x)}px`; |
||||
|
} |
||||
|
} |
||||
|
if (direction.toString().includes('t')) { |
||||
|
const newHeight = original_height - (e.pageY - original_mouse_y); |
||||
|
if (newHeight > minHeight) { |
||||
|
panel.style.height = `${newHeight}px`; |
||||
|
panel.style.top = `${original_y + (e.pageY - original_mouse_y)}px`; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// --- Viewport Constraint Logic ---
|
||||
|
// This ensures that if the user resizes their browser, the panel doesn't get "lost" off-screen.
|
||||
|
function constrainToViewport() { |
||||
|
const rect = panel.getBoundingClientRect(); |
||||
|
const margin = 10; // Extra padding
|
||||
|
|
||||
|
// Horizontal constraint
|
||||
|
if (rect.right > window.innerWidth) { |
||||
|
panel.style.left = `${Math.max(margin, window.innerWidth - rect.width - margin)}px`; |
||||
|
} |
||||
|
if (rect.left < 0) { |
||||
|
panel.style.left = `${margin}px`; |
||||
|
} |
||||
|
|
||||
|
// Vertical constraint
|
||||
|
if (rect.bottom > window.innerHeight) { |
||||
|
panel.style.top = `${Math.max(margin, window.innerHeight - rect.height - margin)}px`; |
||||
|
} |
||||
|
if (rect.top < 0) { |
||||
|
panel.style.top = `${margin}px`; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
window.addEventListener('resize', constrainToViewport); |
||||
|
} |
||||
|
// --- END: Resizable and Draggable Panel Logic ---
|
||||
|
|
||||
|
function toggleMenu(show) { |
||||
|
if (show) { |
||||
|
collapsibleMenu.classList.remove("-translate-x-full"); |
||||
|
menuScrim.classList.remove("hidden"); |
||||
|
} else { |
||||
|
collapsibleMenu.classList.add("-translate-x-full"); |
||||
|
menuScrim.classList.add("hidden"); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
function toggleShortcutsModal(show) { |
||||
|
if (show) { |
||||
|
shortcutsModal.classList.remove("hidden"); |
||||
|
} else { |
||||
|
shortcutsModal.classList.add("hidden"); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
function toggleGuideModal(show) { |
||||
|
if (show) { |
||||
|
guideModal.classList.remove("hidden"); |
||||
|
} else { |
||||
|
guideModal.classList.add("hidden"); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
function toggleCodebaseModal(show) { |
||||
|
if (show) { |
||||
|
codebaseModal.classList.remove("hidden"); |
||||
|
// Reset iframe to ensure it starts at the top
|
||||
|
const iframe = codebaseModal.querySelector("iframe"); |
||||
|
if (iframe) { |
||||
|
iframe.src = iframe.src; |
||||
|
} |
||||
|
} else { |
||||
|
codebaseModal.classList.add("hidden"); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
function toggleChangelogModal(show) { |
||||
|
if (show) { |
||||
|
changelogModal.classList.remove("hidden"); |
||||
|
// Reset iframe to ensure it starts at the top
|
||||
|
const iframe = changelogModal.querySelector("iframe"); |
||||
|
if (iframe) { |
||||
|
iframe.src = iframe.src; |
||||
|
} |
||||
|
} else { |
||||
|
changelogModal.classList.add("hidden"); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
function handleColorToggles(e) { |
||||
|
const colorToggles = [ |
||||
|
toggleSnrColor, |
||||
|
toggleClusterColor, |
||||
|
toggleInlierColor, |
||||
|
toggleStationaryColor, |
||||
|
]; |
||||
|
if (e.target.checked) { |
||||
|
colorToggles.forEach((o) => { |
||||
|
if (o !== e.target) o.checked = false; |
||||
|
}); |
||||
|
} |
||||
|
if (appState.p5_instance) appState.p5_instance.redraw(); |
||||
|
updatePersistentOverlays(videoPlayer.currentTime); |
||||
|
} |
||||
|
|
||||
|
export function initUIEventListeners() { |
||||
|
// --- Initialize GridStack ---
|
||||
|
if (typeof GridStack !== 'undefined') { |
||||
|
appState.gridStackInstance = GridStack.init({ |
||||
|
margin: 10, |
||||
|
cellHeight: '6vh', |
||||
|
disableOneColumnMode: true, |
||||
|
animate: true, |
||||
|
handle: '.grid-stack-item-content > .cursor-grab', |
||||
|
}); |
||||
|
|
||||
|
// Load saved layout with a small delay to ensure DOM is ready
|
||||
|
let isInitialLoad = true; |
||||
|
setTimeout(() => { |
||||
|
const savedLayout = localStorage.getItem('gridstack_layout'); |
||||
|
if (savedLayout) { |
||||
|
try { |
||||
|
const layout = JSON.parse(savedLayout); |
||||
|
console.log("Restoring GridStack positions", layout); |
||||
|
// Use "soft load" to updates positions by id without replacing DOM
|
||||
|
layout.forEach(item => { |
||||
|
const id = item.id || item.gsId; |
||||
|
if (id) { |
||||
|
const el = document.querySelector(`.grid-stack-item[gs-id="${id}"]`); |
||||
|
if (el) appState.gridStackInstance.update(el, { x: item.x, y: item.y, w: item.w, h: item.h }); |
||||
|
} |
||||
|
}); |
||||
|
} catch (e) { } |
||||
|
} |
||||
|
isInitialLoad = false; |
||||
|
}, 100); |
||||
|
|
||||
|
// Save layout on changes
|
||||
|
const saveGrid = () => { |
||||
|
if (isInitialLoad) return; // Don't save while loading
|
||||
|
// save(true, false) saves all items with their current positions/sizes
|
||||
|
const layout = appState.gridStackInstance.save(true, false); |
||||
|
console.log("Saving GridStack layout", layout); |
||||
|
localStorage.setItem('gridstack_layout', JSON.stringify(layout)); |
||||
|
}; |
||||
|
appState.gridStackInstance.on('change', saveGrid); |
||||
|
appState.gridStackInstance.on('dragstop', saveGrid); |
||||
|
appState.gridStackInstance.on('resizestop', saveGrid); |
||||
|
} |
||||
|
|
||||
|
// --- Initialize Floating Zoom Panel ---
|
||||
|
const zoomPanel = document.getElementById("zoom-panel"); |
||||
|
const zoomHeader = document.getElementById("zoom-panel-header"); |
||||
|
const closeZoomBtn = document.getElementById("close-zoom-btn"); |
||||
|
|
||||
|
if (zoomPanel && zoomHeader) { |
||||
|
makeDraggableAndResizable(zoomPanel, zoomHeader, 300, 200); |
||||
|
|
||||
|
if (closeZoomBtn) { |
||||
|
closeZoomBtn.addEventListener("click", () => { |
||||
|
zoomPanel.classList.add("hidden"); |
||||
|
appState.zoomPanelExplicitlyClosed = true; |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// --- Shortcuts Modal ---
|
||||
|
shortcutsBtn.addEventListener("click", (e) => { |
||||
|
e.preventDefault(); |
||||
|
toggleShortcutsModal(true); |
||||
|
}); |
||||
|
shortcutsModalCloseBtn.addEventListener("click", () => toggleShortcutsModal(false)); |
||||
|
shortcutsModal.addEventListener("click", (e) => { |
||||
|
// Close if clicking the background overlay (self), but not children
|
||||
|
if (e.target === shortcutsModal) { |
||||
|
toggleShortcutsModal(false); |
||||
|
} |
||||
|
}); |
||||
|
|
||||
|
// --- Guide Modal ---
|
||||
|
userManualBtn.addEventListener("click", (e) => { |
||||
|
e.preventDefault(); |
||||
|
toggleGuideModal(true); |
||||
|
}); |
||||
|
startUserManualBtn.addEventListener("click", (e) => { |
||||
|
e.preventDefault(); |
||||
|
toggleGuideModal(true); |
||||
|
}); |
||||
|
guideModalCloseBtn.addEventListener("click", () => toggleGuideModal(false)); |
||||
|
guideModal.addEventListener("click", (e) => { |
||||
|
if (e.target === guideModal) { |
||||
|
toggleGuideModal(false); |
||||
|
} |
||||
|
}); |
||||
|
|
||||
|
// --- Codebase Modal ---
|
||||
|
codebaseBtn.addEventListener("click", (e) => { |
||||
|
e.preventDefault(); |
||||
|
toggleCodebaseModal(true); |
||||
|
}); |
||||
|
startCodebaseBtn.addEventListener("click", (e) => { |
||||
|
e.preventDefault(); |
||||
|
toggleCodebaseModal(true); |
||||
|
}); |
||||
|
codebaseModalCloseBtn.addEventListener("click", () => toggleCodebaseModal(false)); |
||||
|
codebaseModal.addEventListener("click", (e) => { |
||||
|
if (e.target === codebaseModal) { |
||||
|
toggleCodebaseModal(false); |
||||
|
} |
||||
|
}); |
||||
|
|
||||
|
// --- Changelog Modal ---
|
||||
|
changelogBtn.addEventListener("click", (e) => { |
||||
|
e.preventDefault(); |
||||
|
toggleChangelogModal(true); |
||||
|
}); |
||||
|
startChangelogBtn.addEventListener("click", (e) => { |
||||
|
e.preventDefault(); |
||||
|
toggleChangelogModal(true); |
||||
|
}); |
||||
|
changelogModalCloseBtn.addEventListener("click", () => toggleChangelogModal(false)); |
||||
|
changelogModal.addEventListener("click", (e) => { |
||||
|
if (e.target === changelogModal) { |
||||
|
toggleChangelogModal(false); |
||||
|
} |
||||
|
}); |
||||
|
|
||||
|
// Global Key Listener for 'k' and 'ESC'
|
||||
|
document.addEventListener("keydown", (e) => { |
||||
|
if (e.key.toLowerCase() === "k") { |
||||
|
// Toggle visibility
|
||||
|
const isHidden = shortcutsModal.classList.contains("hidden"); |
||||
|
toggleShortcutsModal(isHidden); |
||||
|
} |
||||
|
// Prioritize closing open modals
|
||||
|
if (e.key === "Escape") { |
||||
|
if (!guideModal.classList.contains("hidden")) { |
||||
|
toggleGuideModal(false); |
||||
|
} else if (!codebaseModal.classList.contains("hidden")) { |
||||
|
toggleCodebaseModal(false); |
||||
|
} else if (!changelogModal.classList.contains("hidden")) { |
||||
|
toggleChangelogModal(false); |
||||
|
} else if (!shortcutsModal.classList.contains("hidden")) { |
||||
|
toggleShortcutsModal(false); |
||||
|
} |
||||
|
} |
||||
|
}); |
||||
|
|
||||
|
// --- Menu and Fullscreen ---
|
||||
|
toggleMenuBtn.addEventListener("click", () => toggleMenu(true)); |
||||
|
closeMenuBtn.addEventListener("click", () => toggleMenu(false)); |
||||
|
menuScrim.addEventListener("click", () => toggleMenu(false)); |
||||
|
fullscreenBtn.addEventListener("click", () => { |
||||
|
if (!document.fullscreenElement) { |
||||
|
document.documentElement.requestFullscreen(); |
||||
|
} else if (document.exitFullscreen) { |
||||
|
document.exitFullscreen(); |
||||
|
} |
||||
|
}); |
||||
|
|
||||
|
// --- Timeline Tooltip ---
|
||||
|
timelineSlider.addEventListener("mouseover", () => { |
||||
|
if (appState.vizData) timelineTooltip.classList.remove("hidden"); |
||||
|
}); |
||||
|
timelineSlider.addEventListener("mouseout", () => { |
||||
|
timelineTooltip.classList.add("hidden"); |
||||
|
}); |
||||
|
timelineSlider.addEventListener("mousemove", (event) => { |
||||
|
if (!appState.vizData) return; |
||||
|
const rect = timelineSlider.getBoundingClientRect(); |
||||
|
const hoverFraction = (event.clientX - rect.left) / rect.width; |
||||
|
const sliderMax = parseInt(timelineSlider.max, 10) || appState.vizData.radarFrames.length - 1; |
||||
|
let frameIndex = Math.max(0, Math.min(Math.round(hoverFraction * sliderMax), sliderMax)); |
||||
|
const frameData = appState.vizData.radarFrames[frameIndex]; |
||||
|
if (!frameData) return; |
||||
|
const formattedTime = formatTime(frameData.relativeTimeSec * 1000); |
||||
|
timelineTooltip.innerHTML = `Frame: ${frameIndex + 1}<br>Time: ${formattedTime}`; |
||||
|
const tooltipX = event.clientX - rect.left; |
||||
|
timelineTooltip.style.left = `${tooltipX}px`; |
||||
|
}); |
||||
|
|
||||
|
// --- Speed Slider ---
|
||||
|
speedSlider.addEventListener("input", (event) => { |
||||
|
const speed = parseFloat(event.target.value); |
||||
|
videoPlayer.playbackRate = speed; |
||||
|
speedDisplay.textContent = `${speed.toFixed(1)}x`; |
||||
|
}); |
||||
|
|
||||
|
// --- SNR Controls ---
|
||||
|
applySnrBtn.addEventListener("click", () => { |
||||
|
const newMin = parseFloat(snrMinInput.value), newMax = parseFloat(snrMaxInput.value); |
||||
|
if (isNaN(newMin) || isNaN(newMax) || newMin >= newMax) { |
||||
|
showModal("Invalid SNR range."); |
||||
|
return; |
||||
|
} |
||||
|
appState.globalMinSnr = newMin; |
||||
|
appState.globalMaxSnr = newMax; |
||||
|
toggleFrameNorm.checked = false; |
||||
|
if (appState.p5_instance) { |
||||
|
appState.p5_instance.drawSnrLegendToBuffer(appState.globalMinSnr, appState.globalMaxSnr); |
||||
|
appState.p5_instance.redraw(); |
||||
|
} |
||||
|
}); |
||||
|
|
||||
|
// --- Feature Toggles ---
|
||||
|
[toggleSnrColor, toggleClusterColor, toggleInlierColor, toggleStationaryColor].forEach((t) => { |
||||
|
t.addEventListener("change", handleColorToggles); |
||||
|
}); |
||||
|
|
||||
|
[toggleVelocity, toggleEgoSpeed, toggleFrameNorm, toggleTracks, toggleDebugOverlay, toggleDebug2Overlay, toggleCovariance, toggleVehicleDimensions].forEach((t) => { |
||||
|
t.addEventListener("change", () => { |
||||
|
if (appState.p5_instance) appState.p5_instance.redraw(); |
||||
|
if (t === toggleDebugOverlay || t === toggleDebug2Overlay) { |
||||
|
updateDebugOverlay(videoPlayer.currentTime); |
||||
|
updatePersistentOverlays(videoPlayer.currentTime); |
||||
|
} |
||||
|
}); |
||||
|
}); |
||||
|
|
||||
|
toggleCloseUp.addEventListener("change", () => { |
||||
|
appState.isCloseUpMode = toggleCloseUp.checked; |
||||
|
appState.zoomPanelExplicitlyClosed = false; // Reset the close flag so it can reappear
|
||||
|
|
||||
|
// Auto-hide the panel when the user disables Close-Up mode (e.g. by pressing 'g')
|
||||
|
if (!appState.isCloseUpMode) { |
||||
|
const zoomPanel = document.getElementById("zoom-panel"); |
||||
|
if (zoomPanel && !zoomPanel.classList.contains("hidden")) { |
||||
|
zoomPanel.classList.add("hidden"); |
||||
|
} |
||||
|
} |
||||
|
if (appState.isCloseUpMode && appState.isPlaying) { |
||||
|
// If entering close-up mode while playing, automatically pause.
|
||||
|
pausePlayback(); |
||||
|
appState.isPlaying = false; |
||||
|
playPauseBtn.textContent = "Play"; |
||||
|
} |
||||
|
if (appState.p5_instance) { // Handle p5 loop state
|
||||
|
if (appState.isCloseUpMode) { |
||||
|
appState.p5_instance.loop(); // Start looping for mouse interaction.
|
||||
|
} else { |
||||
|
appState.p5_instance.noLoop(); // Stop looping when exiting.
|
||||
|
appState.p5_instance.redraw(); // Redraw one last time.
|
||||
|
} |
||||
|
} |
||||
|
}); |
||||
|
|
||||
|
toggleConfirmedOnly.addEventListener("change", () => { |
||||
|
if (appState.p5_instance) appState.p5_instance.redraw(); |
||||
|
}); |
||||
|
} |
||||
@ -0,0 +1,172 @@ |
|||||
|
import { handleFiles } from "../src/fileLoader.js"; |
||||
|
import { appState } from "../src/state.js"; |
||||
|
import { initDB } from "../src/db.js"; |
||||
|
|
||||
|
const resultsEl = document.getElementById('results'); |
||||
|
|
||||
|
function test(description, testFunction) { |
||||
|
// Simple async test runner wrapper
|
||||
|
(async () => { |
||||
|
try { |
||||
|
await testFunction(); |
||||
|
console.log(`✅ PASS: ${description}`); |
||||
|
resultsEl.innerHTML += `<p class="pass"><b>PASS:</b> ${description}</p>`; |
||||
|
} catch (error) { |
||||
|
console.error(`❌ FAIL: ${description}`, error); |
||||
|
resultsEl.innerHTML += `<p class="fail"><b>FAIL:</b> ${description}<br><pre>${error.stack || error}</pre></p>`; |
||||
|
} |
||||
|
})(); |
||||
|
} |
||||
|
|
||||
|
// --- Setup & Mocks ---
|
||||
|
|
||||
|
// Initialize DB for tests
|
||||
|
async function setupTestEnvironment() { |
||||
|
return new Promise((resolve) => { |
||||
|
initDB(() => { |
||||
|
console.log("Test DB initialized"); |
||||
|
resolve(); |
||||
|
}); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
// Mock URL.createObjectURL
|
||||
|
URL.createObjectURL = (blob) => { |
||||
|
return "blob:mock-url-" + Math.random(); |
||||
|
}; |
||||
|
URL.revokeObjectURL = () => {}; |
||||
|
|
||||
|
// Mock Worker
|
||||
|
class MockWorker { |
||||
|
constructor(scriptUrl) { |
||||
|
console.log("MockWorker created for:", scriptUrl); |
||||
|
this.onmessage = null; |
||||
|
} |
||||
|
postMessage(msg) { |
||||
|
console.log("MockWorker received message:", msg); |
||||
|
// Simulate success response
|
||||
|
if (this.onmessage) { |
||||
|
// Simulate parsing delay
|
||||
|
setTimeout(() => { |
||||
|
this.onmessage({ |
||||
|
data: { |
||||
|
type: 'complete', |
||||
|
data: { |
||||
|
// Correct mock parsed data structure
|
||||
|
radarFrames: [ |
||||
|
{ |
||||
|
timestamp: 1000, |
||||
|
pointCloud: [], |
||||
|
tracks: [] |
||||
|
} |
||||
|
], |
||||
|
tracks: [] |
||||
|
} |
||||
|
} |
||||
|
}); |
||||
|
}, 50); |
||||
|
} |
||||
|
} |
||||
|
terminate() {} |
||||
|
} |
||||
|
window.Worker = MockWorker; |
||||
|
|
||||
|
// Mock p5
|
||||
|
window.p5 = class MockP5 { |
||||
|
constructor(sketch, node) { |
||||
|
console.log("MockP5 created"); |
||||
|
sketch(this); |
||||
|
} |
||||
|
createCanvas() { return { parent: () => {} }; } |
||||
|
background() {} |
||||
|
fill() {} |
||||
|
stroke() {} |
||||
|
rect() {} |
||||
|
ellipse() {} |
||||
|
push() {} |
||||
|
pop() {} |
||||
|
translate() {} |
||||
|
scale() {} |
||||
|
frameRate() {} |
||||
|
noLoop() {} |
||||
|
loop() {} |
||||
|
redraw() {} |
||||
|
resizeCanvas() {} |
||||
|
select() { return { html: () => {}, position: () => {}, style: () => {} }; } |
||||
|
createGraphics() { return { background: () => {}, clear: () => {}, image: () => {} }; } |
||||
|
image() {} |
||||
|
text() {} |
||||
|
textSize() {} |
||||
|
textAlign() {} |
||||
|
noStroke() {} |
||||
|
color() { return {}; } |
||||
|
textFont() {} |
||||
|
drawSnrLegendToBuffer() {} |
||||
|
}; |
||||
|
|
||||
|
// --- Tests ---
|
||||
|
|
||||
|
(async function runTests() { |
||||
|
await setupTestEnvironment(); |
||||
|
|
||||
|
test("fileLoader.js: handleFiles should parse JSON and update appState", async () => { |
||||
|
// 1. Setup
|
||||
|
appState.vizData = null; |
||||
|
const mockJsonFile = new File(['{"some": "json"}'], "test_data.json", { type: "application/json" }); |
||||
|
|
||||
|
// 2. Execution
|
||||
|
handleFiles([mockJsonFile]); |
||||
|
|
||||
|
// 3. Verification (Wait for async operations)
|
||||
|
// We need to wait long enough for DB save + Worker + Processing
|
||||
|
await new Promise(resolve => setTimeout(resolve, 500)); |
||||
|
|
||||
|
if (!appState.vizData) { |
||||
|
throw new Error("appState.vizData was not populated after loading JSON."); |
||||
|
} |
||||
|
|
||||
|
if (appState.jsonFilename !== "test_data.json") { |
||||
|
throw new Error(`Expected jsonFilename to be 'test_data.json', got '${appState.jsonFilename}'`); |
||||
|
} |
||||
|
}); |
||||
|
|
||||
|
test("fileLoader.js: handleFiles should handle video loading (simulated)", async () => { |
||||
|
// 1. Setup
|
||||
|
appState.vizData = null; |
||||
|
appState.videoFilename = ""; |
||||
|
|
||||
|
const videoPlayer = document.getElementById('video-player'); |
||||
|
|
||||
|
// Use MutationObserver to watch for src changes
|
||||
|
const observer = new MutationObserver((mutations) => { |
||||
|
mutations.forEach((mutation) => { |
||||
|
if (mutation.type === "attributes" && mutation.attributeName === "src") { |
||||
|
console.log("Video src changed, triggering events..."); |
||||
|
// Trigger events asynchronously to simulate browser behavior
|
||||
|
setTimeout(() => { |
||||
|
videoPlayer.dispatchEvent(new Event('loadedmetadata')); |
||||
|
videoPlayer.dispatchEvent(new Event('canplaythrough')); |
||||
|
}, 50); |
||||
|
} |
||||
|
}); |
||||
|
}); |
||||
|
|
||||
|
observer.observe(videoPlayer, { attributes: true }); |
||||
|
|
||||
|
const mockVideoFile = new File(['fake video content'], "test_video.mp4", { type: "video/mp4" }); |
||||
|
|
||||
|
// 2. Execute
|
||||
|
handleFiles([mockVideoFile]); |
||||
|
|
||||
|
// 3. Verify
|
||||
|
await new Promise(resolve => setTimeout(resolve, 500)); // Wait for events
|
||||
|
|
||||
|
observer.disconnect(); // Cleanup
|
||||
|
|
||||
|
if (appState.videoFilename !== "test_video.mp4") { |
||||
|
throw new Error(`Expected videoFilename to be 'test_video.mp4', got '${appState.videoFilename}'`); |
||||
|
} |
||||
|
}); |
||||
|
|
||||
|
})(); |
||||
|
|
||||
@ -0,0 +1,73 @@ |
|||||
|
import { handleFiles } from "../src/fileLoader.js"; |
||||
|
import { appState } from "../src/state.js"; |
||||
|
import { initDB } from "../src/db.js"; |
||||
|
|
||||
|
const resultsEl = document.getElementById('results'); |
||||
|
|
||||
|
function test(description, testFunction) { |
||||
|
(async () => { |
||||
|
try { |
||||
|
await testFunction(); |
||||
|
console.log(`✅ PASS: ${description}`); |
||||
|
resultsEl.innerHTML += `<p class="pass"><b>PASS:</b> ${description}</p>`; |
||||
|
} catch (error) { |
||||
|
console.error(`❌ FAIL: ${description}`, error); |
||||
|
resultsEl.innerHTML += `<p class="fail"><b>FAIL:</b> ${description}<br><pre>${error.stack || error}</pre></p>`; |
||||
|
} |
||||
|
})(); |
||||
|
} |
||||
|
|
||||
|
// Mocking dependencies for regression test
|
||||
|
async function setupMocks() { |
||||
|
return new Promise((resolve) => { |
||||
|
initDB(() => { |
||||
|
console.log("Test DB initialized"); |
||||
|
resolve(); |
||||
|
}); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
URL.createObjectURL = () => "blob:mock-video-url"; |
||||
|
URL.revokeObjectURL = () => {}; |
||||
|
|
||||
|
window.p5 = class MockP5 { |
||||
|
constructor(sketch) { sketch(this); } |
||||
|
createCanvas() { return { parent: () => {} }; } |
||||
|
noLoop() {} |
||||
|
redraw() {} |
||||
|
}; |
||||
|
|
||||
|
test("Regression: handleFiles should not crash when loading only a video", async () => { |
||||
|
// 1. Setup - clear vizData
|
||||
|
appState.vizData = null; |
||||
|
appState.videoFilename = ""; |
||||
|
|
||||
|
const videoPlayer = document.getElementById('video-player'); |
||||
|
const mockVideoFile = new File(['fake video'], "test.mp4", { type: "video/mp4" }); |
||||
|
|
||||
|
// Mock video events
|
||||
|
const triggerEvents = () => { |
||||
|
setTimeout(() => { |
||||
|
videoPlayer.dispatchEvent(new Event('loadedmetadata')); |
||||
|
videoPlayer.dispatchEvent(new Event('canplaythrough')); |
||||
|
}, 10); |
||||
|
}; |
||||
|
|
||||
|
const observer = new MutationObserver(triggerEvents); |
||||
|
observer.observe(videoPlayer, { attributes: true, attributeFilter: ['src'] }); |
||||
|
|
||||
|
// 2. Execution
|
||||
|
try { |
||||
|
await handleFiles([mockVideoFile]); |
||||
|
} catch (e) { |
||||
|
throw new Error(`Crash detected during video-only load: ${e.message}`); |
||||
|
} |
||||
|
|
||||
|
// 3. Verification
|
||||
|
await new Promise(resolve => setTimeout(resolve, 100)); |
||||
|
observer.disconnect(); |
||||
|
|
||||
|
if (appState.videoFilename !== "test.mp4") { |
||||
|
throw new Error("Video filename not set correctly in appState"); |
||||
|
} |
||||
|
}); |
||||
@ -0,0 +1,62 @@ |
|||||
|
import re |
||||
|
import tkinter as tk |
||||
|
from tkinter import filedialog |
||||
|
import os |
||||
|
|
||||
|
|
||||
|
def extract_radar_config(log_file_path, output_file_path=None): |
||||
|
""" |
||||
|
Extract radar configuration commands from log file. |
||||
|
|
||||
|
Args: |
||||
|
log_file_path (str): Path to input log file |
||||
|
output_file_path (str, optional): Path to save extracted config |
||||
|
""" |
||||
|
|
||||
|
# Pattern to match required lines |
||||
|
pattern = re.compile(r'INFO\s+-\s+radar_tracker\.console_logger\s+-\s+>\s+(.*)') |
||||
|
|
||||
|
extracted_commands = [] |
||||
|
|
||||
|
with open(log_file_path, 'r') as file: |
||||
|
for line in file: |
||||
|
match = pattern.search(line) |
||||
|
if match: |
||||
|
command = match.group(1).strip() |
||||
|
extracted_commands.append(command) |
||||
|
|
||||
|
# Output handling |
||||
|
if output_file_path: |
||||
|
with open(output_file_path, 'w') as out_file: |
||||
|
for cmd in extracted_commands: |
||||
|
out_file.write(cmd + '\n') |
||||
|
print(f"[INFO] Extracted config saved to: {output_file_path}") |
||||
|
else: |
||||
|
print("\n--- Extracted Radar Config ---\n") |
||||
|
for cmd in extracted_commands: |
||||
|
print(cmd) |
||||
|
|
||||
|
return extracted_commands |
||||
|
|
||||
|
|
||||
|
# Example usage |
||||
|
if __name__ == "__main__": |
||||
|
# Create and hide root tkinter window |
||||
|
root = tk.Tk() |
||||
|
root.withdraw() |
||||
|
|
||||
|
# Allow user to select a .log file |
||||
|
log_file = filedialog.askopenfilename( |
||||
|
title="Select Log File", |
||||
|
filetypes=[("Log Files", "*.log"), ("Text Files", "*.txt"), ("All Files", "*.*")] |
||||
|
) |
||||
|
|
||||
|
if log_file: |
||||
|
# Generate output file name (e.g., myscript.log -> myscript.cfg) |
||||
|
base_name = os.path.splitext(log_file)[0] |
||||
|
output_file = f"{base_name}.cfg" |
||||
|
|
||||
|
extract_radar_config(log_file, output_file) |
||||
|
print(f"[INFO] Processing complete for {log_file}") |
||||
|
else: |
||||
|
print("[INFO] No file selected.") |
||||
3
steps/vendor/gridstack-all.js
File diff suppressed because it is too large
View File
File diff suppressed because it is too large
View File
@ -0,0 +1 @@ |
|||||
|
.grid-stack{position:relative}.grid-stack-rtl{direction:ltr}.grid-stack-rtl>.grid-stack-item{direction:rtl}.grid-stack-placeholder>.placeholder-content{background-color:rgba(0,0,0,.1);margin:0;position:absolute;width:auto;z-index:0!important}.grid-stack>.grid-stack-item{position:absolute;padding:0}.grid-stack>.grid-stack-item>.grid-stack-item-content{margin:0;position:absolute;width:auto;overflow-x:hidden;overflow-y:auto}.grid-stack>.grid-stack-item.size-to-content:not(.size-to-content-max)>.grid-stack-item-content{overflow-y:hidden}.grid-stack-item>.ui-resizable-handle{position:absolute;font-size:.1px;display:block;-ms-touch-action:none;touch-action:none}.grid-stack-item.ui-resizable-autohide>.ui-resizable-handle,.grid-stack-item.ui-resizable-disabled>.ui-resizable-handle{display:none}.grid-stack-item>.ui-resizable-ne,.grid-stack-item>.ui-resizable-nw,.grid-stack-item>.ui-resizable-se,.grid-stack-item>.ui-resizable-sw{background-image:url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" fill="none" stroke="%23666" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" viewBox="0 0 20 20"><path d="m10 3 2 2H8l2-2v14l-2-2h4l-2 2"/></svg>');background-repeat:no-repeat;background-position:center}.grid-stack-item>.ui-resizable-ne{transform:translate(0,10px) rotate(45deg)}.grid-stack-item>.ui-resizable-sw{transform:rotate(45deg)}.grid-stack-item>.ui-resizable-nw{transform:translate(0,10px) rotate(-45deg)}.grid-stack-item>.ui-resizable-se{transform:rotate(-45deg)}.grid-stack-item>.ui-resizable-nw{cursor:nw-resize;width:20px;height:20px;top:0}.grid-stack-item>.ui-resizable-n{cursor:n-resize;height:10px;top:0;left:25px;right:25px}.grid-stack-item>.ui-resizable-ne{cursor:ne-resize;width:20px;height:20px;top:0}.grid-stack-item>.ui-resizable-e{cursor:e-resize;width:10px;top:15px;bottom:15px}.grid-stack-item>.ui-resizable-se{cursor:se-resize;width:20px;height:20px}.grid-stack-item>.ui-resizable-s{cursor:s-resize;height:10px;left:25px;bottom:0;right:25px}.grid-stack-item>.ui-resizable-sw{cursor:sw-resize;width:20px;height:20px}.grid-stack-item>.ui-resizable-w{cursor:w-resize;width:10px;top:15px;bottom:15px}.grid-stack-item.ui-draggable-dragging>.ui-resizable-handle{display:none!important}.grid-stack-item.ui-draggable-dragging{will-change:left,top;cursor:move}.grid-stack-item.ui-resizable-resizing{will-change:width,height}.ui-draggable-dragging,.ui-resizable-resizing{z-index:10000}.ui-draggable-dragging>.grid-stack-item-content,.ui-resizable-resizing>.grid-stack-item-content{box-shadow:1px 4px 6px rgba(0,0,0,.2);opacity:.8}.grid-stack-animate,.grid-stack-animate .grid-stack-item{transition:left .3s,top .3s,height .3s,width .3s}.grid-stack-animate .grid-stack-item.grid-stack-placeholder,.grid-stack-animate .grid-stack-item.ui-draggable-dragging,.grid-stack-animate .grid-stack-item.ui-resizable-resizing{transition:left 0s,top 0s,height 0s,width 0s}.grid-stack>.grid-stack-item[gs-y="0"]{top:0}.grid-stack>.grid-stack-item[gs-x="0"]{left:0}.gs-12>.grid-stack-item{width:8.333%}.gs-12>.grid-stack-item[gs-x="1"]{left:8.333%}.gs-12>.grid-stack-item[gs-w="2"]{width:16.667%}.gs-12>.grid-stack-item[gs-x="2"]{left:16.667%}.gs-12>.grid-stack-item[gs-w="3"]{width:25%}.gs-12>.grid-stack-item[gs-x="3"]{left:25%}.gs-12>.grid-stack-item[gs-w="4"]{width:33.333%}.gs-12>.grid-stack-item[gs-x="4"]{left:33.333%}.gs-12>.grid-stack-item[gs-w="5"]{width:41.667%}.gs-12>.grid-stack-item[gs-x="5"]{left:41.667%}.gs-12>.grid-stack-item[gs-w="6"]{width:50%}.gs-12>.grid-stack-item[gs-x="6"]{left:50%}.gs-12>.grid-stack-item[gs-w="7"]{width:58.333%}.gs-12>.grid-stack-item[gs-x="7"]{left:58.333%}.gs-12>.grid-stack-item[gs-w="8"]{width:66.667%}.gs-12>.grid-stack-item[gs-x="8"]{left:66.667%}.gs-12>.grid-stack-item[gs-w="9"]{width:75%}.gs-12>.grid-stack-item[gs-x="9"]{left:75%}.gs-12>.grid-stack-item[gs-w="10"]{width:83.333%}.gs-12>.grid-stack-item[gs-x="10"]{left:83.333%}.gs-12>.grid-stack-item[gs-w="11"]{width:91.667%}.gs-12>.grid-stack-item[gs-x="11"]{left:91.667%}.gs-12>.grid-stack-item[gs-w="12"]{width:100%}.gs-1>.grid-stack-item{width:100%} |
||||
@ -0,0 +1,5 @@ |
|||||
|
/** |
||||
|
* PrismJS 1.29.0 |
||||
|
* https://prismjs.com/download.html#themes=prism-okaidia |
||||
|
*/ |
||||
|
code[class*=language-],pre[class*=language-]{color:#f8f8f2;background:0 0;text-shadow:0 1px rgba(0,0,0,.3);font-family:Consolas,Monaco,'Andale Mono','Ubuntu Mono',monospace;font-size:1em;text-align:left;white-space:pre;word-spacing:normal;word-break:normal;word-wrap:normal;line-height:1.5;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-hyphens:none;-moz-hyphens:none;-ms-hyphens:none;hyphens:none}pre[class*=language-]{padding:1em;margin:.5em 0;overflow:auto;border-radius:.3em}:not(pre)>code[class*=language-],pre[class*=language-]{background:#272822}:not(pre)>code[class*=language-]{padding:.1em;border-radius:.3em;white-space:normal}.token.cdata,.token.comment,.token.doctype,.token.prolog{color:#8292a2}.token.punctuation{color:#f8f8f2}.token.namespace{opacity:.7}.token.constant,.token.deleted,.token.property,.token.symbol,.token.tag{color:#f92672}.token.boolean,.token.number{color:#ae81ff}.token.attr-name,.token.builtin,.token.char,.token.inserted,.token.selector,.token.string{color:#a6e22e}.language-css .token.string,.style .token.string,.token.entity,.token.operator,.token.url,.token.variable{color:#f8f8f2}.token.atrule,.token.attr-value,.token.class-name,.token.function{color:#e6db74}.token.keyword{color:#66d9ef}.token.important,.token.regex{color:#fd971f}.token.bold,.token.important{font-weight:700}.token.italic{font-style:italic}.token.entity{cursor:help} |
||||
1
steps/vendor/prism.js
File diff suppressed because it is too large
View File
File diff suppressed because it is too large
View File
@ -0,0 +1,574 @@ |
|||||
|
<!DOCTYPE html> |
||||
|
<html lang="en"> |
||||
|
<head> |
||||
|
<meta charset="UTF-8"> |
||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
||||
|
<title>ADAS/ARAS Development for Indian Roads</title> |
||||
|
<script src="https://cdn.tailwindcss.com"></script> |
||||
|
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script> |
||||
|
<script src="https://cdn.plot.ly/plotly-2.27.0.min.js"></script> |
||||
|
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500;700&display=swap" rel="stylesheet"> |
||||
|
|
||||
|
<!-- |
||||
|
Palette Name: Energetic & Playful (Adapted for Professional Data) |
||||
|
Colors: |
||||
|
- Primary Navy: #003f5c |
||||
|
- Deep Purple: #58508d |
||||
|
- Vibrant Pink: #bc5090 |
||||
|
- Alert Red: #ff6361 |
||||
|
- Warning Yellow: #ffa600 |
||||
|
--> |
||||
|
|
||||
|
<style> |
||||
|
body { |
||||
|
font-family: 'Roboto', sans-serif; |
||||
|
background-color: #f3f4f6; |
||||
|
color: #1f2937; |
||||
|
} |
||||
|
|
||||
|
/* Chart Container Styling - MANDATORY */ |
||||
|
.chart-container { |
||||
|
position: relative; |
||||
|
width: 100%; |
||||
|
margin-left: auto; |
||||
|
margin-right: auto; |
||||
|
background-color: white; |
||||
|
border-radius: 0.5rem; |
||||
|
padding: 1rem; |
||||
|
} |
||||
|
|
||||
|
/* Specific constraints for responsiveness */ |
||||
|
.chart-box-lg { |
||||
|
height: 400px; |
||||
|
max-height: 500px; |
||||
|
max-width: 800px; |
||||
|
} |
||||
|
|
||||
|
.chart-box-md { |
||||
|
height: 300px; |
||||
|
max-height: 400px; |
||||
|
max-width: 600px; |
||||
|
} |
||||
|
|
||||
|
/* Scrollable Table Styling */ |
||||
|
.scenarios-table-container { |
||||
|
max-height: 600px; |
||||
|
overflow-y: auto; |
||||
|
scrollbar-width: thin; |
||||
|
scrollbar-color: #003f5c #e5e7eb; |
||||
|
} |
||||
|
|
||||
|
/* Custom Scrollbar */ |
||||
|
::-webkit-scrollbar { |
||||
|
width: 8px; |
||||
|
height: 8px; |
||||
|
} |
||||
|
::-webkit-scrollbar-track { |
||||
|
background: #f1f1f1; |
||||
|
} |
||||
|
::-webkit-scrollbar-thumb { |
||||
|
background: #58508d; |
||||
|
border-radius: 4px; |
||||
|
} |
||||
|
::-webkit-scrollbar-thumb:hover { |
||||
|
background: #003f5c; |
||||
|
} |
||||
|
|
||||
|
/* Diagram Utilities (CSS-only Flowchart) */ |
||||
|
.flow-step { |
||||
|
display: flex; |
||||
|
flex-direction: column; |
||||
|
align-items: center; |
||||
|
text-align: center; |
||||
|
padding: 1rem; |
||||
|
background: white; |
||||
|
border-left: 4px solid #003f5c; |
||||
|
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1); |
||||
|
border-radius: 0.375rem; |
||||
|
width: 100%; |
||||
|
} |
||||
|
.flow-arrow { |
||||
|
font-size: 2rem; |
||||
|
color: #bc5090; |
||||
|
margin: 0.5rem 0; |
||||
|
line-height: 1; |
||||
|
} |
||||
|
</style> |
||||
|
<!-- |
||||
|
CRITICAL: |
||||
|
- Source Material: User provided context on ADAS/ARAS workflow + Request for 50 Scenarios. |
||||
|
- Narrative Plan: |
||||
|
1. Context & Challenge (Indian Roads). |
||||
|
2. The 50 Scenarios (Data Table + Distribution Analysis). |
||||
|
3. Workflow Optimization (Current vs. Future State). |
||||
|
4. Technical Recommendations (Tools). |
||||
|
- Visualizations: |
||||
|
1. Doughnut Chart (Scenario Categories) -> Goal: Inform composition. |
||||
|
2. Bubble Chart (Frequency vs Severity) -> Goal: Relationships/Prioritization. |
||||
|
3. HTML Table -> Goal: Organize the 50 items. |
||||
|
4. Radar Chart -> Goal: Compare Workflow Attributes. |
||||
|
5. CSS Flowchart -> Goal: Process Flow (NO MERMAID/SVG). |
||||
|
- NO SVG Used. NO Mermaid Used. |
||||
|
--> |
||||
|
</head> |
||||
|
<body class="bg-gray-100"> |
||||
|
|
||||
|
<!-- Header --> |
||||
|
<header class="bg-[#003f5c] text-white py-8 shadow-lg"> |
||||
|
<div class="container mx-auto px-4"> |
||||
|
<h1 class="text-3xl md:text-4xl font-bold mb-2">ADAS & ARAS Development: Indian Context</h1> |
||||
|
<p class="text-xl text-gray-200">Accelerating FCW & BSD Validation for 2/3-Wheelers</p> |
||||
|
</div> |
||||
|
</header> |
||||
|
|
||||
|
<main class="container mx-auto px-4 py-8 space-y-12"> |
||||
|
|
||||
|
<!-- Section 1: Introduction --> |
||||
|
<section class="grid grid-cols-1 md:grid-cols-3 gap-8"> |
||||
|
<div class="md:col-span-2 bg-white p-6 rounded-lg shadow-md border-t-4 border-[#bc5090]"> |
||||
|
<h2 class="text-2xl font-bold text-[#003f5c] mb-4">The Challenge: Engineering for Chaos</h2> |
||||
|
<p class="text-gray-700 leading-relaxed mb-4"> |
||||
|
Developing Advanced Driver Assistance Systems (ADAS) for India requires moving beyond standard Euro NCAP definitions. The environment is characterized by high density, heterogeneous traffic (trucks sharing lanes with bicycles), and unpredictable road geometry. |
||||
|
</p> |
||||
|
<p class="text-gray-700 leading-relaxed"> |
||||
|
The current workflow utilizes <strong>AWRL1432/1843 radar sensors</strong> and <strong>MATLAB-based tracking</strong>. To achieve robust Front Collision Warning (FCW) and Blind Spot Detection (BSD), we must validate against specific edge cases found only on Indian roads. |
||||
|
</p> |
||||
|
</div> |
||||
|
<div class="bg-white p-6 rounded-lg shadow-md flex flex-col justify-center items-center text-center"> |
||||
|
<div class="text-5xl font-bold text-[#ff6361] mb-2">50</div> |
||||
|
<div class="text-lg font-medium text-gray-600">Unique Scenarios Identified</div> |
||||
|
<div class="mt-4 w-full h-1 bg-gray-200 rounded"> |
||||
|
<div class="h-1 bg-[#ff6361] rounded" style="width: 100%"></div> |
||||
|
</div> |
||||
|
<p class="text-sm text-gray-500 mt-2">Targeting High-Risk Edge Cases</p> |
||||
|
</div> |
||||
|
</section> |
||||
|
|
||||
|
<!-- Section 2: Scenario Analysis --> |
||||
|
<section> |
||||
|
<div class="mb-6"> |
||||
|
<h2 class="text-2xl font-bold text-[#003f5c]">Scenario Categorization</h2> |
||||
|
<p class="text-gray-600 mt-2"> |
||||
|
Before diving into the list, we analyze the distribution of scenarios. Indian traffic creates a unique cluster of "Lateral" and "Static Obstacle" risks that are less prevalent in western datasets. |
||||
|
</p> |
||||
|
</div> |
||||
|
|
||||
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-8 mb-8"> |
||||
|
<!-- Chart 1: Scenario Distribution --> |
||||
|
<div class="flex flex-col"> |
||||
|
<div class="chart-container chart-box-md shadow-md"> |
||||
|
<canvas id="scenarioDistChart"></canvas> |
||||
|
</div> |
||||
|
<p class="text-sm text-gray-500 mt-2 italic text-center">Fig 1. Breakdown of the 50 generated scenarios by primary threat type.</p> |
||||
|
</div> |
||||
|
|
||||
|
<!-- Chart 2: Priority Matrix (Bubble) --> |
||||
|
<div class="flex flex-col"> |
||||
|
<div class="chart-container chart-box-md shadow-md"> |
||||
|
<canvas id="riskBubbleChart"></canvas> |
||||
|
</div> |
||||
|
<p class="text-sm text-gray-500 mt-2 italic text-center">Fig 2. Frequency vs. Severity. High severity/frequency items (Top Right) are critical for FCW.</p> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
<!-- The Big Table --> |
||||
|
<div class="bg-white rounded-lg shadow-md overflow-hidden border-t-4 border-[#ffa600]"> |
||||
|
<div class="p-6 bg-gray-50 border-b"> |
||||
|
<h3 class="text-xl font-bold text-[#003f5c]">The 50 Critical Scenarios Matrix</h3> |
||||
|
<p class="text-sm text-gray-600">Compiled from Indian accident data reports and ADAS edge-case studies.</p> |
||||
|
</div> |
||||
|
<div class="scenarios-table-container"> |
||||
|
<table class="min-w-full text-sm text-left"> |
||||
|
<thead class="text-xs text-white uppercase bg-[#003f5c] sticky top-0 z-10"> |
||||
|
<tr> |
||||
|
<th class="px-6 py-3">ID</th> |
||||
|
<th class="px-6 py-3">Scenario Name</th> |
||||
|
<th class="px-6 py-3">Type</th> |
||||
|
<th class="px-6 py-3">Indian Context / Description</th> |
||||
|
<th class="px-6 py-3">Priority</th> |
||||
|
</tr> |
||||
|
</thead> |
||||
|
<tbody id="scenarioTableBody" class="divide-y divide-gray-200"> |
||||
|
<!-- JS will populate this --> |
||||
|
</tbody> |
||||
|
</table> |
||||
|
</div> |
||||
|
</div> |
||||
|
</section> |
||||
|
|
||||
|
<!-- Section 3: Workflow Optimization --> |
||||
|
<section> |
||||
|
<div class="mb-6"> |
||||
|
<h2 class="text-2xl font-bold text-[#003f5c]">Workflow Modernization</h2> |
||||
|
<p class="text-gray-600 mt-2"> |
||||
|
Moving from a MATLAB-centric, human-in-the-loop workflow to a standardized ROS2 pipeline can reduce iteration time by estimated 40%. The current bottleneck is manual synchronization and the overhead of interpreted code. |
||||
|
</p> |
||||
|
</div> |
||||
|
|
||||
|
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8"> |
||||
|
|
||||
|
<!-- Chart 3: Radar Comparison --> |
||||
|
<div class="bg-white p-6 rounded-lg shadow-md"> |
||||
|
<h3 class="text-lg font-bold text-[#58508d] mb-4">Pipeline Capabilities Comparison</h3> |
||||
|
<div class="chart-container chart-box-md mx-auto"> |
||||
|
<canvas id="workflowRadarChart"></canvas> |
||||
|
</div> |
||||
|
<div class="mt-4 text-sm text-gray-600"> |
||||
|
<p><span class="inline-block w-3 h-3 bg-[#ff6361] mr-2"></span>Current (MATLAB) is excellent for rapid prototyping but struggles with real-time speed and integration.</p> |
||||
|
<p><span class="inline-block w-3 h-3 bg-[#003f5c] mr-2"></span>Proposed (ROS2/C++) offers superior sensor fusion capabilities and visualization tools.</p> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
<!-- Process Flow Diagram (CSS only) --> |
||||
|
<div class="bg-white p-6 rounded-lg shadow-md"> |
||||
|
<h3 class="text-lg font-bold text-[#58508d] mb-4">Proposed Accelerated Workflow</h3> |
||||
|
<div class="flex flex-col items-center space-y-2"> |
||||
|
<!-- Step 1 --> |
||||
|
<div class="flow-step border-l-[#ffa600]"> |
||||
|
<h4 class="font-bold text-[#003f5c]">1. Data Ingestion (ROS2)</h4> |
||||
|
<p class="text-xs text-gray-500">AWRL1432 Driver Node → /radar/pointcloud2</p> |
||||
|
</div> |
||||
|
|
||||
|
<div class="flow-arrow">↓</div> |
||||
|
|
||||
|
<!-- Step 2 --> |
||||
|
<div class="flow-step border-l-[#ff6361]"> |
||||
|
<h4 class="font-bold text-[#003f5c]">2. Auto-Labeling</h4> |
||||
|
<p class="text-xs text-gray-500">Sync Video → YOLOv8/SAM for Ground Truth Bounding Boxes</p> |
||||
|
</div> |
||||
|
|
||||
|
<div class="flow-arrow">↓</div> |
||||
|
|
||||
|
<!-- Step 3 --> |
||||
|
<div class="flow-step border-l-[#bc5090]"> |
||||
|
<h4 class="font-bold text-[#003f5c]">3. Algorithm & Fusion</h4> |
||||
|
<p class="text-xs text-gray-500">C++ Tracking Nodes + Camera Fusion (Kalman Filter)</p> |
||||
|
</div> |
||||
|
|
||||
|
<div class="flow-arrow">↓</div> |
||||
|
|
||||
|
<!-- Step 4 --> |
||||
|
<div class="flow-step border-l-[#58508d]"> |
||||
|
<h4 class="font-bold text-[#003f5c]">4. Visualization (Foxglove)</h4> |
||||
|
<p class="text-xs text-gray-500">Web-based replay of Radar PCL + Video Overlay</p> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</section> |
||||
|
|
||||
|
<!-- Section 4: Recommendations --> |
||||
|
<section class="bg-[#003f5c] rounded-xl p-8 shadow-xl text-white"> |
||||
|
<h2 class="text-2xl font-bold mb-6">Actionable Improvements</h2> |
||||
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-6"> |
||||
|
<!-- Rec 1 --> |
||||
|
<div class="bg-white/10 p-4 rounded border border-white/20 backdrop-blur-sm"> |
||||
|
<h3 class="text-[#ffa600] font-bold text-lg mb-2">1. Adopt Foxglove Studio</h3> |
||||
|
<p class="text-sm opacity-90">Replace the custom visualizer. Foxglove allows drag-and-drop visualization of point clouds and camera feeds, perfectly synchronized, via browser.</p> |
||||
|
</div> |
||||
|
<!-- Rec 2 --> |
||||
|
<div class="bg-white/10 p-4 rounded border border-white/20 backdrop-blur-sm"> |
||||
|
<h3 class="text-[#ffa600] font-bold text-lg mb-2">2. Automated Ground Truth</h3> |
||||
|
<p class="text-sm opacity-90">Stop manual video comparison. Run the video through a pre-trained model (like YOLOv8) to generate "Ground Truth" boxes, then calculate Intersection over Union (IoU) with radar tracks automatically.</p> |
||||
|
</div> |
||||
|
<!-- Rec 3 --> |
||||
|
<div class="bg-white/10 p-4 rounded border border-white/20 backdrop-blur-sm"> |
||||
|
<h3 class="text-[#ffa600] font-bold text-lg mb-2">3. Move to C++/ROS2</h3> |
||||
|
<p class="text-sm opacity-90">MATLAB is great for math, but slow for pipelines. Migrating the tracking logic to C++ nodes in ROS2 ensures the code is ready for embedded deployment.</p> |
||||
|
</div> |
||||
|
</div> |
||||
|
</section> |
||||
|
|
||||
|
<footer class="text-center text-gray-500 text-sm mt-8 pb-4"> |
||||
|
<p>© 2025 ADAS Research Division. Generated for Internal Review.</p> |
||||
|
</footer> |
||||
|
|
||||
|
</main> |
||||
|
|
||||
|
<script> |
||||
|
// --- 1. Data Definitions --- |
||||
|
|
||||
|
// The 50 Scenarios Data |
||||
|
const scenarios = [ |
||||
|
// FCW - Urban/Congestion |
||||
|
{ id: 1, name: "The Auto-Rickshaw Cut-In", type: "FCW", desc: "Auto-rickshaw abruptly changes lane into ego path from left/right at low speed.", priority: "High" }, |
||||
|
{ id: 2, name: "Sudden Stop for Pothole", type: "FCW", desc: "Lead vehicle brakes hard unexpectedly to avoid a road defect.", priority: "High" }, |
||||
|
{ id: 3, name: "Pedestrian Jaywalking (Group)", type: "FCW", desc: "Group of pedestrians crossing mid-block in dense traffic.", priority: "High" }, |
||||
|
{ id: 4, name: "Stray Dog Chasing", type: "FCW", desc: "Animal runs perpendicular to ego vehicle path.", priority: "Med" }, |
||||
|
{ id: 5, name: "Cattle on Highway", type: "FCW", desc: "Stationary or slow-moving cow sitting on the median/lane.", priority: "High" }, |
||||
|
{ id: 6, name: "Wrong-Way Motorcyclist", type: "FCW", desc: "Bike approaching head-on in the ego lane (shoulder riding).", priority: "Critical" }, |
||||
|
{ id: 7, name: "Bus Stop Pull-Out", type: "FCW", desc: "Bus merges from stop without indicator.", priority: "Med" }, |
||||
|
{ id: 8, name: "The 'Squeeze' Gap", type: "FCW", desc: "Two heavy vehicles creating a narrowing tunnel for the ego 2-wheeler.", priority: "High" }, |
||||
|
{ id: 9, name: "Door Opening", type: "FCW", desc: "Parked car door opens into ego path.", priority: "Med" }, |
||||
|
{ id: 10, name: "Intersection Creeper", type: "FCW", desc: "Vehicle inching into intersection during red light.", priority: "Med" }, |
||||
|
|
||||
|
// BSD - Overtaking & Filtering |
||||
|
{ id: 11, name: "Left-Side Overtake", type: "BSD", desc: "Faster bike overtaking ego vehicle from the left (blind spot).", priority: "Critical" }, |
||||
|
{ id: 12, name: "Zig-Zagging Scooter", type: "BSD", desc: "Scooter weaving continuously through traffic behind ego.", priority: "High" }, |
||||
|
{ id: 13, name: "Heavy Truck Blind Zone", type: "BSD", desc: "Truck alongside ego vehicle drifting into ego lane.", priority: "Critical" }, |
||||
|
{ id: 14, name: "E-Rickshaw Silent Approach", type: "BSD", desc: "Electric rickshaw approaching silently in blind spot.", priority: "High" }, |
||||
|
{ id: 15, name: "Cyclist Filtering", type: "BSD", desc: "Cyclist moving between ego vehicle and curb.", priority: "Med" }, |
||||
|
|
||||
|
// FCW - Highway |
||||
|
{ id: 16, name: "Unmarked Speed Breaker", type: "FCW", desc: "Lead vehicle becomes airborne/brakes hard for invisible hump.", priority: "High" }, |
||||
|
{ id: 17, name: "Tractor Trolley (No Lights)", type: "FCW", desc: "Slow moving agricultural vehicle at night with no reflectors.", priority: "Critical" }, |
||||
|
{ id: 18, name: "Construction Debris", type: "FCW", desc: "Pile of sand/bricks left on the carriageway.", priority: "Med" }, |
||||
|
{ id: 19, name: "Broken Down Truck", type: "FCW", desc: "Stationary truck in fast lane without hazard lights.", priority: "Critical" }, |
||||
|
{ id: 20, name: "Toll Plaza Queue", type: "FCW", desc: "Approaching stationary queue at high speed.", priority: "Low" }, |
||||
|
|
||||
|
// BSD - Turning |
||||
|
{ id: 21, name: "The 'U-Turn' Conflict", type: "BSD", desc: "Vehicle performing U-turn hits ego vehicle's rear flank.", priority: "High" }, |
||||
|
{ id: 22, name: "Free Left Turn Merge", type: "BSD", desc: "Vehicle merging from free left turn into ego's blind spot.", priority: "Med" }, |
||||
|
{ id: 23, name: "Roundabout Exit Cut", type: "BSD", desc: "Vehicle exiting roundabout cuts across ego's front/side.", priority: "Med" }, |
||||
|
|
||||
|
// Environmental / Sensor Noise |
||||
|
{ id: 24, name: "Heavy Rain Clutter", type: "Noise", desc: "Monsoon rain creating false positives in radar.", priority: "Med" }, |
||||
|
{ id: 25, name: "Metal Fence Reflection", type: "Noise", desc: "Guard rails on curves interpreted as dynamic objects.", priority: "Low" }, |
||||
|
{ id: 26, name: "Tunnel Entry/Exit", type: "Light", desc: "Sudden lighting change affecting camera verification.", priority: "Low" }, |
||||
|
|
||||
|
// More FCW/BSD Mix |
||||
|
{ id: 27, name: "Vegetable Cart", type: "FCW", desc: "Hand-pushed cart moving at walking pace in lane.", priority: "Med" }, |
||||
|
{ id: 28, name: "Tailgating SUV", type: "BSD", desc: "Large vehicle following too closely, disappearing from mirrors.", priority: "High" }, |
||||
|
{ id: 29, name: "Water Tanker Spillage", type: "FCW", desc: "Wet road surface causing lead vehicle to slip.", priority: "Low" }, |
||||
|
{ id: 30, name: "Emergency Vehicle", type: "BSD", desc: "Ambulance approaching fast from rear quarter.", priority: "High" }, |
||||
|
|
||||
|
// Filling to 50 with variations |
||||
|
{ id: 31, name: "Median Jumper (Pedestrian)", type: "FCW", desc: "Person jumping over median divider.", priority: "High" }, |
||||
|
{ id: 32, name: "Merging Traffic (High Speed)", type: "BSD", desc: "Highway on-ramp merge at unsafe speed.", priority: "Med" }, |
||||
|
{ id: 33, name: "Sudden Lane Expansion", type: "FCW", desc: "Traffic fanning out causing erratic lateral movements.", priority: "Low" }, |
||||
|
{ id: 34, name: "Auto-Rickshaw U-Turn", type: "FCW", desc: "3-wheeler making tight U-turn on narrow road.", priority: "High" }, |
||||
|
{ id: 35, name: "Loose Gravel Skid", type: "FCW", desc: "Lead bike skids on gravel.", priority: "Med" }, |
||||
|
{ id: 36, name: "Police Barricade", type: "FCW", desc: "Unmarked zigzag barricades on highway.", priority: "Critical" }, |
||||
|
{ id: 37, name: "Overloaded Truck Sway", type: "BSD", desc: "Cargo extending beyond truck width into ego lane.", priority: "High" }, |
||||
|
{ id: 38, name: "Child on Road", type: "FCW", desc: "Small radar cross-section target (child).", priority: "Critical" }, |
||||
|
{ id: 39, name: "Fog/Smog Visibility", type: "Noise", desc: "Reduced visibility scenarios for camera validation.", priority: "Med" }, |
||||
|
{ id: 40, name: "Garbage Dump", type: "FCW", desc: "Pile of garbage extending onto road.", priority: "Med" }, |
||||
|
{ id: 41, name: "Hawker on Roadside", type: "BSD", desc: "Stationary seller forcing traffic to swerve into ego.", priority: "Med" }, |
||||
|
{ id: 42, name: "Shadow Contrast", type: "Light", desc: "Deep shadows under flyovers confusing tracking.", priority: "Low" }, |
||||
|
{ id: 43, name: "Motorcycle Convoy", type: "BSD", desc: "Group of bikers surrounding ego vehicle.", priority: "High" }, |
||||
|
{ id: 44, name: "Bus Stopping Mid-Road", type: "FCW", desc: "Bus stops to let passengers off not at a stop.", priority: "High" }, |
||||
|
{ id: 45, name: "Reversing Vehicle", type: "FCW", desc: "Car reversing on main road due to missed turn.", priority: "Critical" }, |
||||
|
{ id: 46, name: "Hanging Cables", type: "FCW", desc: "Low hanging wires (radar ghost target).", priority: "Low" }, |
||||
|
{ id: 47, name: "Manhole Open", type: "FCW", desc: "Open manhole requiring sharp evasion.", priority: "Critical" }, |
||||
|
{ id: 48, name: "Speeding Delivery Bike", type: "BSD", desc: "Swiggy/Zomato rider cutting aggressively.", priority: "High" }, |
||||
|
{ id: 49, name: "Follow-Me Car", type: "FCW", desc: "Vehicle with erratic speed profile.", priority: "Med" }, |
||||
|
{ id: 50, name: "Traffic Police Signal", type: "FCW", desc: "Hand gesture stop (not detected by radar).", priority: "Med" } |
||||
|
]; |
||||
|
|
||||
|
// --- 2. Utility Functions --- |
||||
|
|
||||
|
// Label Wrapping Helper |
||||
|
function wrapLabel(str, maxLen) { |
||||
|
if (str.length <= maxLen) return str; |
||||
|
const words = str.split(' '); |
||||
|
const lines = []; |
||||
|
let currentLine = words[0]; |
||||
|
|
||||
|
for (let i = 1; i < words.length; i++) { |
||||
|
if ((currentLine + " " + words[i]).length < maxLen) { |
||||
|
currentLine += " " + words[i]; |
||||
|
} else { |
||||
|
lines.push(currentLine); |
||||
|
currentLine = words[i]; |
||||
|
} |
||||
|
} |
||||
|
lines.push(currentLine); |
||||
|
return lines; |
||||
|
} |
||||
|
|
||||
|
// Common Chart Options |
||||
|
const commonOptions = { |
||||
|
responsive: true, |
||||
|
maintainAspectRatio: false, |
||||
|
plugins: { |
||||
|
legend: { |
||||
|
position: 'bottom', |
||||
|
labels: { font: { family: 'Roboto' } } |
||||
|
}, |
||||
|
tooltip: { |
||||
|
backgroundColor: 'rgba(0, 63, 92, 0.9)', |
||||
|
titleFont: { size: 14, family: 'Roboto' }, |
||||
|
bodyFont: { size: 13, family: 'Roboto' }, |
||||
|
padding: 12, |
||||
|
callbacks: { |
||||
|
title: function(tooltipItems) { |
||||
|
const item = tooltipItems[0]; |
||||
|
let label = item.chart.data.labels[item.dataIndex]; |
||||
|
if (Array.isArray(label)) { |
||||
|
return label.join(' '); |
||||
|
} else { |
||||
|
return label; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
// --- 3. Chart Generation --- |
||||
|
|
||||
|
// Chart 1: Scenario Distribution (Doughnut) |
||||
|
function renderDistChart() { |
||||
|
const counts = { FCW: 0, BSD: 0, Noise: 0, Light: 0 }; |
||||
|
scenarios.forEach(s => counts[s.type] = (counts[s.type] || 0) + 1); |
||||
|
|
||||
|
const ctx = document.getElementById('scenarioDistChart').getContext('2d'); |
||||
|
new Chart(ctx, { |
||||
|
type: 'doughnut', |
||||
|
data: { |
||||
|
labels: ['FCW Scenarios', 'BSD Scenarios', 'Sensor Noise', 'Lighting/Env'], |
||||
|
datasets: [{ |
||||
|
data: [counts.FCW, counts.BSD, counts.Noise, counts.Light], |
||||
|
backgroundColor: ['#003f5c', '#bc5090', '#ffa600', '#ff6361'], |
||||
|
borderWidth: 0 |
||||
|
}] |
||||
|
}, |
||||
|
options: { |
||||
|
...commonOptions, |
||||
|
cutout: '60%', |
||||
|
plugins: { |
||||
|
...commonOptions.plugins, |
||||
|
title: { |
||||
|
display: true, |
||||
|
text: 'Scenario Composition', |
||||
|
font: { size: 16 } |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
// Chart 2: Risk Bubble Chart (Frequency vs Severity) |
||||
|
function renderBubbleChart() { |
||||
|
const ctx = document.getElementById('riskBubbleChart').getContext('2d'); |
||||
|
|
||||
|
// Mapping categories to approximate Frequency (X) and Severity (Y) for visualization |
||||
|
// High Priority = High Severity (Y > 8) |
||||
|
// Common Indian Scenario = High Frequency (X > 7) |
||||
|
|
||||
|
const dataPoints = [ |
||||
|
{ x: 9, y: 9, r: 15, label: 'Wrong-Way Driver' }, |
||||
|
{ x: 9, y: 7, r: 12, label: 'Auto-Rickshaw Cut-In' }, |
||||
|
{ x: 8, y: 8, r: 10, label: 'Cattle on Road' }, |
||||
|
{ x: 9, y: 6, r: 10, label: 'Left Overtake' }, |
||||
|
{ x: 6, y: 9, r: 8, label: 'Unmarked Breaker' }, |
||||
|
{ x: 5, y: 9, r: 8, label: 'Broken Truck' }, |
||||
|
{ x: 8, y: 4, r: 6, label: 'Jaywalkers' }, |
||||
|
{ x: 4, y: 3, r: 5, label: 'Rain Clutter' }, |
||||
|
{ x: 3, y: 8, r: 6, label: 'Child' } |
||||
|
]; |
||||
|
|
||||
|
new Chart(ctx, { |
||||
|
type: 'bubble', |
||||
|
data: { |
||||
|
datasets: [{ |
||||
|
label: 'Scenario Risk Analysis', |
||||
|
data: dataPoints, |
||||
|
backgroundColor: dataPoints.map(d => d.y > 8 ? '#ff6361' : '#58508d'), |
||||
|
}] |
||||
|
}, |
||||
|
options: { |
||||
|
...commonOptions, |
||||
|
scales: { |
||||
|
x: { |
||||
|
title: { display: true, text: 'Frequency of Occurrence (India)' }, |
||||
|
min: 0, max: 10 |
||||
|
}, |
||||
|
y: { |
||||
|
title: { display: true, text: 'Severity / Safety Risk' }, |
||||
|
min: 0, max: 10 |
||||
|
} |
||||
|
}, |
||||
|
plugins: { |
||||
|
...commonOptions.plugins, |
||||
|
tooltip: { |
||||
|
callbacks: { |
||||
|
label: function(context) { |
||||
|
return context.raw.label + ` (Freq: ${context.raw.x}, Sev: ${context.raw.y})`; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
// Chart 3: Radar Chart (Workflow Comparison) |
||||
|
function renderRadarChart() { |
||||
|
const ctx = document.getElementById('workflowRadarChart').getContext('2d'); |
||||
|
|
||||
|
const labels = ['Real-time Performance', 'Sensor Fusion Ease', 'Visualization Tools', 'Debugging Speed', 'Deployment Readiness']; |
||||
|
// Wrap labels |
||||
|
const wrappedLabels = labels.map(l => wrapLabel(l, 16)); |
||||
|
|
||||
|
new Chart(ctx, { |
||||
|
type: 'radar', |
||||
|
data: { |
||||
|
labels: wrappedLabels, |
||||
|
datasets: [{ |
||||
|
label: 'Current (MATLAB)', |
||||
|
data: [4, 5, 5, 8, 3], // MATLAB good at debug, poor at real-time/deploy |
||||
|
fill: true, |
||||
|
backgroundColor: 'rgba(255, 99, 97, 0.2)', |
||||
|
borderColor: '#ff6361', |
||||
|
pointBackgroundColor: '#ff6361', |
||||
|
}, { |
||||
|
label: 'Proposed (ROS2/C++)', |
||||
|
data: [9, 9, 8, 6, 9], // ROS2 great at real-time, fusion, deploy |
||||
|
fill: true, |
||||
|
backgroundColor: 'rgba(0, 63, 92, 0.2)', |
||||
|
borderColor: '#003f5c', |
||||
|
pointBackgroundColor: '#003f5c', |
||||
|
}] |
||||
|
}, |
||||
|
options: { |
||||
|
...commonOptions, |
||||
|
scales: { |
||||
|
r: { |
||||
|
angleLines: { color: '#e5e7eb' }, |
||||
|
grid: { color: '#e5e7eb' }, |
||||
|
pointLabels: { |
||||
|
font: { size: 12, family: 'Roboto' } |
||||
|
}, |
||||
|
suggestedMin: 0, |
||||
|
suggestedMax: 10 |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
// --- 4. Table Population --- |
||||
|
function populateTable() { |
||||
|
const tbody = document.getElementById('scenarioTableBody'); |
||||
|
scenarios.forEach(s => { |
||||
|
const tr = document.createElement('tr'); |
||||
|
tr.className = "bg-white border-b hover:bg-gray-50"; |
||||
|
|
||||
|
// Color code priority |
||||
|
let priorityClass = "text-gray-600"; |
||||
|
if (s.priority === 'Critical') priorityClass = "text-[#ff6361] font-bold"; |
||||
|
if (s.priority === 'High') priorityClass = "text-[#ffa600] font-bold"; |
||||
|
|
||||
|
tr.innerHTML = ` |
||||
|
<td class="px-6 py-4 font-medium text-gray-900">${s.id}</td> |
||||
|
<td class="px-6 py-4 font-semibold text-[#003f5c]">${s.name}</td> |
||||
|
<td class="px-6 py-4"><span class="bg-gray-100 text-gray-800 text-xs font-medium mr-2 px-2.5 py-0.5 rounded border border-gray-500">${s.type}</span></td> |
||||
|
<td class="px-6 py-4 text-gray-600">${s.desc}</td> |
||||
|
<td class="px-6 py-4 ${priorityClass}">${s.priority}</td> |
||||
|
`; |
||||
|
tbody.appendChild(tr); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
// --- 5. Initialization --- |
||||
|
window.onload = function() { |
||||
|
renderDistChart(); |
||||
|
renderBubbleChart(); |
||||
|
renderRadarChart(); |
||||
|
populateTable(); |
||||
|
}; |
||||
|
|
||||
|
</script> |
||||
|
</body> |
||||
|
</html> |
||||
Write
Preview
Loading…
Cancel
Save
Reference in new issue
