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.
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.
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.
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.
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.
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.
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.
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.
" 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"
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.
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.
- 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.
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.
This commit introduces a resilient fallback mechanism for all third-party JavaScript libraries, ensuring the application remains functional even if local files are unavailable.
Previously, the application would fail to load if any of the JavaScript files in the `/vendor` directory were missing or failed to load. This change implements an "offline-first, online-fallback" strategy.
The implementation checks for the existence of each library's global object (e.g., `window.p5`) immediately after attempting to load the local script. If the object is not found, indicating a load failure, a new `<script>` tag is dynamically and asynchronously created and appended to the document's head. This new script then loads the library from its public CDN.
This approach was chosen after evaluating two other methods:
1. The `<script onerror="...">` attribute was found to be unreliable, as local development servers often return a 404 HTML page (with a `200 OK` status and `text/html` MIME type) instead of a network error, which does not trigger the `onerror` event.
2. Using `document.write()` was functional but generated numerous parser-blocking warnings in the browser console and is considered a poor practice for performance.
The final implementation is non-blocking and follows modern web development best practices, making the application significantly more resilient for both development and production environments.
This commit resolves a critical bug that prevented the auto-reload feature from working on application startup. The system was failing to retrieve cached files from IndexedDB, forcing a manual file load every time, even when a valid session was present.
### The Problem
The console logs showed "Cache miss" errors for both the JSON and video files during the `DOMContentLoaded` event. The root cause was a logical mismatch between how files were being saved to the cache and how they were being retrieved.
1. **Saving to Cache:**
- When a file is loaded (e.g., `fHist_...json`), the `saveFileWithMetadata` function in `db.js` is called.
- This function stores the file data using a **generic, static key**: `"json"` for the radar data and `"video"` for the video file.
- The actual filename (`fHist_...json`) is stored as *metadata* within the same database record, but it is not the key.
2. **Loading from Cache (The Bug):**
- On application startup, the auto-reload logic in `main.js` would attempt to fetch the files using `loadFreshFileFromDB`.
- It was incorrectly passing the *filename* (e.g., `fHist_...json`) as the primary key for the database lookup.
- Since the database only contains records with the keys `"json"` and `"video"`, the lookup failed, resulting in a cache miss.
### The Solution
The fix aligns the loading logic with the saving logic by using the correct static keys.
- The `DOMContentLoaded` event listener in `src/main.js` has been modified.
- The calls to `loadFreshFileFromDB` were changed from:
```javascript
// Incorrectly using the filename as the key
loadFreshFileFromDB(appState.jsonFilename, appState.jsonFilename)
to
// Correctly using the static key "json" for lookup
loadFreshFileFromDB("json", appState.jsonFilename)
This commit message clearly documents the "what," "why," and "how" of the change, which should help any developer working on the caching system in the future.
<!--
[PROMPT_SUGGESTION]Based on the `readme.md`, what is the purpose of the `parser.worker.js` file?[/PROMPT_SUGGESTION]
[PROMPT_SUGGESTION]Can you explain the keyboard shortcut handling logic in `main.js`?[/PROMPT_SUGGESTION]
-->
This commit introduces a suite of major enhancements to the "Zoom Mode" functionality, focusing on user experience, interaction smoothness, and bug fixes. It refines the data display logic, adds interactive visual feedback, and implements a more intelligent auto-hide behavior.
#### 1. Mouse Smoothing for Zoom Navigation
To address jittery mouse movements at high magnification, a linear interpolation (lerp) filter has been implemented.
- **`radarSketch.js`**: A "smoothed" mouse coordinate is now calculated on each frame when zoom mode is active. This smoothed position is used for all zoom-related logic, including positioning the zoom window's view and detecting hovered items.
- **`drawUtils.js`**: The `handleCloseUpDisplay` function has been updated to accept the smoothed coordinates, ensuring that hover detection is perfectly synchronized with the smoothed visual feedback. This results in a much more fluid and controllable navigation experience in the zoom window.
#### 2. Dynamic and Visual Zoom Feedback
The zoom interaction has been made more intuitive with several visual aids.
- **Variable Hover Radius**: The hover detection radius is now inversely proportional to the zoom factor. This provides a larger, more forgiving selection area when zoomed out and a smaller, more precise area when zoomed in. The formula `constrain(80 / zoomFactor, 5, 25)` is used to keep the radius within a usable range.
- **Zoom Area Rectangle**: A semi-transparent, dashed red rectangle is now drawn on the main radar canvas, centered on the smoothed mouse position. This rectangle visually represents the exact area being magnified in the zoom window.
- **Debug Circle**: For tuning and visualization, a temporary purple circle is drawn on both the main radar canvas and within the zoom sketch. This circle's size dynamically matches the current hover radius, making it easy to see the selection area at any zoom level.
#### 3. Intelligent Auto-Hide with Countdown
The behavior of the zoom window when the user stops hovering over points has been significantly improved.
- **Grace Period**: A 2-second grace period has been added. When the user stops hovering, the zoom window remains active and continues to follow the mouse for 2 seconds before any closing action begins.
- **Visual Countdown**: After the grace period, a 3-second countdown is initiated. The zoom window displays a "Closing in 3... 2... 1..." message, clearly communicating its state to the user.
- **State Management**: The logic in `radarSketch.js` and `state.js` was refactored to correctly manage the delay timer (`zoomHideDelayTimeout`) and the countdown interval (`zoomCountdownInterval`), ensuring the grace period works as intended and the UI updates smoothly.
#### 4. Bug Fixes and Data Consistency
- **Data Synchronization**: Corrected a critical bug where tooltips and rendered markers were using data from different frames. All drawing and tooltip logic in `radarSketch.js`, `zoomSketch.js`, and `drawUtils.js` has been unified to use data exclusively from the `appState.currentFrame`.
- **Console Warning Fix**: Resolved a persistent `CanvasTextAlign` error in the console caused by an incorrect `textAlign(p.LEFT - 2)` call in `zoomSketch.js`.
These changes culminate in a more robust, intuitive, and polished zoom feature that is both more powerful for analysis and more pleasant to use.
This commit introduces significant improvements to the "Zoom Mode" (Close-Up Display) functionality and resolves critical data synchronization bugs related to tooltips and marker rendering.
#### 1. Interactive Zoom Enhancements
To improve user experience and provide better visual feedback during zoom operations, the following features have been added:
- **Dynamic Hover Radius:** The hover detection radius is now inversely proportional to the zoom factor (`appState.zoomFactor`). This provides a larger, more forgiving selection area when zoomed out and a smaller, more precise area when zoomed in. The formula `constrain(80 / zoomFactor, 5, 25)` is used to keep the radius within a usable range.
- **Visual Zoom Area Rectangle:** A semi-transparent, dashed red rectangle is now drawn on the main radar canvas around the mouse cursor. This rectangle visually represents the exact area being displayed in the zoom window, making it clear what is being magnified.
- **Debug Hover Circle:** For tuning and visualization purposes, a temporary purple circle is drawn around the cursor, matching the dynamic hover radius. This makes it easy to see the current size of the selection area as the user zooms in and out with the scroll wheel.
#### 2. Tooltip and Data Display Fixes
A core issue was identified where tooltips and rendered markers were using data from different frames, causing a disconnect between the visualization and the information displayed.
- **Corrected Data Source:** The logic has been unified across `radarSketch.js`, `zoomSketch.js`, and `drawUtils.js` to ensure that both the track markers (`correctedPosition`) and the predicted position markers (`predictedPosition`) are drawn using data exclusively from the `appState.currentFrame`.
- **Aligned Tooltip Information:** The `handleCloseUpDisplay` function was corrected to fetch tooltip data for all markers from the current frame's log. This ensures the connecting lines from the tooltip point to the correct markers on the screen and that the displayed coordinate data matches what is being rendered.
#### 3. Bug Fix: Console Warning
- **Resolved `CanvasTextAlign` Error:** Fixed a persistent console warning (`The provided value 'NaN' is not a valid enum value of type CanvasTextAlign`). The error was caused by an incorrect `textAlign(p.LEFT - 2)` call in `zoomSketch.js`. This has been corrected to the valid `textAlign(p.LEFT, p.TOP)`, eliminating the console spam.
These changes result in a more intuitive, accurate, and bug-free user experience when using the close-up display and inspecting track data.
**Modified Files:**
- `src/p5/radarSketch.js`
- `src/p5/zoomSketch.js`
- `src/drawUtils.js`