From 1b95c3166a375ba05fdad9ec9144bc538b9711fe Mon Sep 17 00:00:00 2001 From: DhanashreePetare Date: Fri, 2 Jan 2026 01:35:12 +0530 Subject: [PATCH 1/4] feat: add URL event loading with auto-refresh clean-branch(#448) --- guides/developers/url-event-loading.md | 114 ++++++++++++++++++ .../src/event-display.ts | 109 +++++++++++++++++ .../src/managers/ui-manager/dat-gui-ui.ts | 47 ++++++++ .../src/managers/ui-manager/index.ts | 17 +++ .../phoenix-menu/phoenix-menu-ui.ts | 65 ++++++++++ .../src/managers/ui-manager/phoenix-ui.ts | 6 + 6 files changed, 358 insertions(+) create mode 100644 guides/developers/url-event-loading.md diff --git a/guides/developers/url-event-loading.md b/guides/developers/url-event-loading.md new file mode 100644 index 00000000..39d38191 --- /dev/null +++ b/guides/developers/url-event-loading.md @@ -0,0 +1,114 @@ +# URL Event Loading Feature (#448) + +## Overview + +The URL Event Loading feature enables loading event data directly from a URL with automatic refresh capability when cycling back to the first event. This is useful for live event displays where new events are continuously being generated and published to a server. + +## Features + +- **Load events from URL**: Fetch event data from any HTTP/HTTPS endpoint returning Phoenix JSON format +- **Auto-refresh on loop-back**: Automatically refresh events when cycling back to the first event for live data +- **Manual refresh**: Manually refresh events from the current URL source at any time +- **Live status display**: Visual indicator showing if events are loaded from URL and the current source +- **Error handling**: Graceful error handling with logging to the info logger + +## Usage + +### Dat.GUI Controls + +When dat.GUI menu is enabled, a "Load from URL" folder is automatically added with: + +- **Event URL**: Text input field for the URL of the event data file +- **Load from URL**: Button to load events from the specified URL +- **Refresh from URL**: Button to manually refresh events +- **Status**: Display showing "Live: [URL]" or "Not loaded from URL" + +### Phoenix Menu Controls + +When Phoenix menu is enabled, a "Load from URL" node is automatically added with: + +- **Set URL (prompt)**: Button that prompts for a new URL +- **Load from URL**: Button to load events from the specified URL +- **Refresh from URL**: Button to manually refresh events +- **Status**: Label showing current load status + +### Programmatic API + +```typescript +// Load events from a URL +try { + const eventKeys = await eventDisplay.loadEventsFromURL('https://example.com/events.json'); + console.log('Loaded events:', eventKeys); +} catch (error) { + console.error('Failed to load events:', error); +} + +// Check if currently loaded from URL +if (eventDisplay.isLoadedFromURL()) { + const url = eventDisplay.getEventSourceURL(); + console.log('Currently loading from:', url); +} + +// Manually refresh from the current URL source +try { + await eventDisplay.refreshEventsFromURL(); + console.log('Events refreshed'); +} catch (error) { + console.error('Refresh failed:', error); +} +``` + +## Auto-Refresh Behavior + +The feature automatically detects when the user cycles back to the first event from the last event. When this happens: + +1. The system checks if events were loaded from a URL +2. If yes, it automatically fetches the latest events from that URL +3. The new events replace the old ones +4. An info log message is generated: "Looped back to start - refreshing events from URL" + +This is useful for live event monitoring where you want to refresh the event list after viewing all current events. + +### Example Workflow + +1. User loads 10 events from `https://example.com/events.json` +2. User navigates through events: 1 → 2 → 3 → ... → 10 +3. User clicks next while on event 10, returning to event 1 +4. System automatically refreshes from the URL (e.g., now there are 15 events) +5. User continues viewing the newly loaded events + +## Expected URL Format + +The URL should return a JSON object matching the Phoenix event data format: + +```json +{ + "event_0": { + "RunNumber": 123, + "EventNumber": 1, + "collections": { + "Tracks": [ + // track data... + ], + "Hits": [ + // hit data... + ] + } + }, + "event_1": { + // event data... + } +} +``` + +## Error Handling + +- Invalid URLs result in fetch errors logged to the info logger +- Failed refreshes during auto-refresh are logged but don't interrupt event navigation +- Network errors are captured and displayed to the user via alerts (in UI controls) +- The system gracefully falls back to cached events if refresh fails + +## Related Issues + +- #448: Main feature implementation +- #177: Track extension (merged, affects rk-helper.ts) diff --git a/packages/phoenix-event-display/src/event-display.ts b/packages/phoenix-event-display/src/event-display.ts index 787d65b6..32a0405b 100644 --- a/packages/phoenix-event-display/src/event-display.ts +++ b/packages/phoenix-event-display/src/event-display.ts @@ -47,6 +47,12 @@ export class EventDisplay { private stateManager: StateManager; /** URL manager for managing options given through URL. */ private urlOptionsManager: URLOptionsManager; + /** URL of the currently loaded event source (if loaded from URL). */ + private eventSourceURL: string | null = null; + /** Current event index for detecting loop-back to start. */ + private currentEventIndex: number = 0; + /** Event keys array for navigation tracking. */ + private eventKeys: string[] = []; /** * Create the Phoenix event display and intitialize all the elements. @@ -75,6 +81,8 @@ export class EventDisplay { this.ui.init(configuration); // Set up for the state manager this.getStateManager().setEventDisplay(this); + // Pass this EventDisplay instance to UI for URL loader functionality + this.ui.setEventDisplay(this); // Animate loop const uiLoop = () => { @@ -124,6 +132,10 @@ export class EventDisplay { } const eventKeys = this.configuration.eventDataLoader.getEventsList(eventsData); + // Track event keys for loop-back detection + this.eventKeys = eventKeys; + // Reset index when loading new events + this.currentEventIndex = 0; this.loadEvent(eventKeys[0]); this.onEventsChange.forEach((callback) => callback(eventKeys)); @@ -163,6 +175,29 @@ export class EventDisplay { * @param eventKey String that represents the event in the eventsData object. */ public loadEvent(eventKey: any) { + const newIndex = this.eventKeys.indexOf(eventKey); + + // Detect loop-back to start: if we were at last event and now at first + const loopedBackToStart = + this.currentEventIndex === this.eventKeys.length - 1 && newIndex === 0; + + // If we looped back and loaded from URL, re-fetch to get latest data + if (loopedBackToStart && this.eventSourceURL) { + this.infoLogger.add( + 'Looped back to start - refreshing events from URL', + 'Event', + ); + this.loadEventsFromURL(this.eventSourceURL).catch((error) => { + this.infoLogger.add( + `Failed to refresh from URL: ${error.message}`, + 'Event', + ); + }); + } + + // Update current index + this.currentEventIndex = newIndex; + const event = this.eventsData[eventKey]; if (event) { @@ -194,6 +229,80 @@ export class EventDisplay { return this.infoLogger; } + /** + * Load event data from a URL. Automatically refreshes when looping back to start. + * @param url URL of the event data file (should return Phoenix JSON format). + * @returns Promise that resolves when events are loaded. + */ + public async loadEventsFromURL(url: string): Promise { + try { + this.infoLogger.add(`Loading events from URL: ${url}`, 'Event'); + this.loadingManager.addLoadableItem(`url_events_${url}`); + + const response = await fetch(url, { + method: 'GET', + headers: { + Accept: 'application/json', + }, + // No caching to always get fresh data on refresh + cache: 'no-store', + }); + + if (!response.ok) { + throw new Error( + `HTTP error! status: ${response.status} ${response.statusText}`, + ); + } + + const eventsData = await response.json(); + + // Store URL for auto-refresh on loop-back + this.eventSourceURL = url; + + this.loadingManager.itemLoaded(`url_events_${url}`); + this.infoLogger.add('Events loaded successfully from URL', 'Event'); + + // Parse and load events + return this.parsePhoenixEvents(eventsData); + } catch (error) { + this.loadingManager.itemLoaded(`url_events_${url}`); + const errorMessage = + error instanceof Error ? error.message : 'Unknown error'; + this.infoLogger.add( + `Failed to load events from URL: ${errorMessage}`, + 'Event', + ); + throw error; + } + } + + /** + * Check if events are currently loaded from a URL (for live display). + * @returns True if events are from a URL, false otherwise. + */ + public isLoadedFromURL(): boolean { + return this.eventSourceURL !== null; + } + + /** + * Get the current event source URL if loaded from URL. + * @returns The URL or null if not loaded from URL. + */ + public getEventSourceURL(): string | null { + return this.eventSourceURL; + } + + /** + * Manually refresh events from the current URL source. + * @returns Promise that resolves when refresh completes, or rejects if not loaded from URL. + */ + public async refreshEventsFromURL(): Promise { + if (!this.eventSourceURL) { + throw new Error('No URL source to refresh from'); + } + return this.loadEventsFromURL(this.eventSourceURL); + } + /** * Get the loading manager for managing loadable items. * @returns The loading manager. diff --git a/packages/phoenix-event-display/src/managers/ui-manager/dat-gui-ui.ts b/packages/phoenix-event-display/src/managers/ui-manager/dat-gui-ui.ts index 81af57d8..e86f3f43 100644 --- a/packages/phoenix-event-display/src/managers/ui-manager/dat-gui-ui.ts +++ b/packages/phoenix-event-display/src/managers/ui-manager/dat-gui-ui.ts @@ -458,4 +458,51 @@ export class DatGUIMenuUI implements PhoenixUI { public getEventDataTypeFolder(typeName: string): GUI { return this.eventFolder.__folders[typeName]; } + + /** + * Add event URL loader controls to the menu. + * @param eventDisplay The event display instance for loading events from URL. + */ + public addEventURLLoader(eventDisplay: any): void { + const loaderFolder = this.gui.addFolder('Load from URL'); + + const params = { + url: 'https://example.com/events.json', + loadFromURL: () => { + if (params.url) { + eventDisplay + .loadEventsFromURL(params.url) + .then(() => { + alert('Events loaded successfully from URL'); + }) + .catch((error: Error) => { + alert(`Failed to load: ${error.message}`); + }); + } + }, + refreshURL: () => { + eventDisplay + .refreshEventsFromURL() + .then(() => { + alert('Events refreshed from URL'); + }) + .catch((error: Error) => { + alert(`Refresh failed: ${error.message}`); + }); + }, + liveStatus: () => { + const isLive = eventDisplay.isLoadedFromURL(); + const url = eventDisplay.getEventSourceURL(); + return isLive ? `Live: ${url}` : 'Not loaded from URL'; + }, + }; + + loaderFolder.add(params, 'url').name('Event URL'); + loaderFolder.add(params, 'loadFromURL').name('Load from URL'); + loaderFolder.add(params, 'refreshURL').name('Refresh from URL'); + loaderFolder + .add({ status: params.liveStatus() }, 'status') + .name('Status') + .listen(); + } } diff --git a/packages/phoenix-event-display/src/managers/ui-manager/index.ts b/packages/phoenix-event-display/src/managers/ui-manager/index.ts index ce0d65f8..374e9250 100644 --- a/packages/phoenix-event-display/src/managers/ui-manager/index.ts +++ b/packages/phoenix-event-display/src/managers/ui-manager/index.ts @@ -88,6 +88,9 @@ export class UIManager { /** State manager for managing the event display's state. */ private stateManager: StateManager; + /** Event display instance for URL loading functionality. */ + private eventDisplay: any; + /** * Constructor for the UI manager. * @param three Three manager to perform three.js related operations. @@ -166,6 +169,20 @@ export class UIManager { this.labelsFolderAdded = false; } + /** + * Set the event display instance (for URL loading functionality). + * @param eventDisplay The event display instance. + */ + public setEventDisplay(eventDisplay: any): void { + this.eventDisplay = eventDisplay; + // Add URL loader to all UI menus + this.uiMenus.forEach((menu) => { + if (menu.addEventURLLoader) { + menu.addEventURLLoader(eventDisplay); + } + }); + } + /** * Add geometry (detector geometry) folder to the dat.GUI and Phoenix menu. */ diff --git a/packages/phoenix-event-display/src/managers/ui-manager/phoenix-menu/phoenix-menu-ui.ts b/packages/phoenix-event-display/src/managers/ui-manager/phoenix-menu/phoenix-menu-ui.ts index 1edf3bba..15ef17b1 100644 --- a/packages/phoenix-event-display/src/managers/ui-manager/phoenix-menu/phoenix-menu-ui.ts +++ b/packages/phoenix-event-display/src/managers/ui-manager/phoenix-menu/phoenix-menu-ui.ts @@ -488,4 +488,69 @@ export class PhoenixMenuUI implements PhoenixUI { this.eventFolder.loadStateFromJSON(this.eventFolderState); } } + + /** + * Add event URL loader controls to the Phoenix menu. + * @param eventDisplay The event display instance for loading events from URL. + */ + public addEventURLLoader(eventDisplay: any): void { + const urlLoaderNode = this.phoenixMenuRoot.addChild( + 'Load from URL', + undefined, + 'cloud', + ); + + // URL input field (simulated as label + button since we can't directly input) + let eventURL = 'https://example.com/events.json'; + + urlLoaderNode.addConfig({ + type: 'button', + label: 'Set URL (prompt)', + onClick: () => { + const url = prompt('Enter event data URL:', eventURL); + if (url) { + eventURL = url; + } + }, + }); + + urlLoaderNode.addConfig({ + type: 'button', + label: 'Load from URL', + onClick: () => { + if (eventURL) { + eventDisplay + .loadEventsFromURL(eventURL) + .then(() => { + alert('Events loaded successfully from URL'); + }) + .catch((error: Error) => { + alert(`Failed to load: ${error.message}`); + }); + } + }, + }); + + urlLoaderNode.addConfig({ + type: 'button', + label: 'Refresh from URL', + onClick: () => { + eventDisplay + .refreshEventsFromURL() + .then(() => { + alert('Events refreshed from URL'); + }) + .catch((error: Error) => { + alert(`Refresh failed: ${error.message}`); + }); + }, + }); + + urlLoaderNode.addConfig({ + type: 'label', + label: eventDisplay.isLoadedFromURL() + ? `Live: ${eventDisplay.getEventSourceURL()}` + : 'Not loaded from URL', + }); + } } diff --git a/packages/phoenix-event-display/src/managers/ui-manager/phoenix-ui.ts b/packages/phoenix-event-display/src/managers/ui-manager/phoenix-ui.ts index 7d9f9a56..3ca3e2ae 100644 --- a/packages/phoenix-event-display/src/managers/ui-manager/phoenix-ui.ts +++ b/packages/phoenix-event-display/src/managers/ui-manager/phoenix-ui.ts @@ -73,4 +73,10 @@ export interface PhoenixUI { * @returns Folder of the event data type. */ getEventDataTypeFolder(typeName: string): T | undefined; + + /** + * Add event URL loader controls to the menu. + * @param eventDisplay The event display instance for loading events from URL. + */ + addEventURLLoader?(eventDisplay: any): void; } From 8f9014fd83be327bb54358876576efb17d0ac02d Mon Sep 17 00:00:00 2001 From: DhanashreePetare Date: Wed, 7 Jan 2026 23:28:11 +0530 Subject: [PATCH 2/4] refactor: scope auto-refresh to cycling flow via fileLoader (#448) --- guides/developers/url-event-loading.md | 15 ++++-------- .../src/event-display.ts | 24 ++----------------- .../lib/services/file-loader.service.ts | 9 +++++-- 3 files changed, 13 insertions(+), 35 deletions(-) diff --git a/guides/developers/url-event-loading.md b/guides/developers/url-event-loading.md index 39d38191..8388a1a5 100644 --- a/guides/developers/url-event-loading.md +++ b/guides/developers/url-event-loading.md @@ -2,12 +2,12 @@ ## Overview -The URL Event Loading feature enables loading event data directly from a URL with automatic refresh capability when cycling back to the first event. This is useful for live event displays where new events are continuously being generated and published to a server. +The URL Event Loading feature enables loading event data directly from a URL. Automatic refresh is supported only when using the cycling flow (cycle-events component) and wrapping from the last event back to the first, making it useful for live event displays where new events are continuously being generated. ## Features - **Load events from URL**: Fetch event data from any HTTP/HTTPS endpoint returning Phoenix JSON format -- **Auto-refresh on loop-back**: Automatically refresh events when cycling back to the first event for live data +- **Auto-refresh on loop-back (cycling only)**: Automatically refresh events when the cycling flow wraps from last → first. Manual selection does not trigger refresh. - **Manual refresh**: Manually refresh events from the current URL source at any time - **Live status display**: Visual indicator showing if events are loaded from URL and the current source - **Error handling**: Graceful error handling with logging to the info logger @@ -58,16 +58,9 @@ try { } ``` -## Auto-Refresh Behavior +## Auto-Refresh Behavior (Cycling Flow) -The feature automatically detects when the user cycles back to the first event from the last event. When this happens: - -1. The system checks if events were loaded from a URL -2. If yes, it automatically fetches the latest events from that URL -3. The new events replace the old ones -4. An info log message is generated: "Looped back to start - refreshing events from URL" - -This is useful for live event monitoring where you want to refresh the event list after viewing all current events. +Auto-refresh is performed only by the cycling component. When cycling is active and configured to reload, wrapping from the last event to the first triggers `fileLoader.reloadLastEvents()` which re-fetches the last loaded URL with `cache: 'no-store'` to bypass caches. Manual navigation (including selecting last then first) does not trigger refresh. ### Example Workflow diff --git a/packages/phoenix-event-display/src/event-display.ts b/packages/phoenix-event-display/src/event-display.ts index 32a0405b..106535aa 100644 --- a/packages/phoenix-event-display/src/event-display.ts +++ b/packages/phoenix-event-display/src/event-display.ts @@ -175,28 +175,8 @@ export class EventDisplay { * @param eventKey String that represents the event in the eventsData object. */ public loadEvent(eventKey: any) { - const newIndex = this.eventKeys.indexOf(eventKey); - - // Detect loop-back to start: if we were at last event and now at first - const loopedBackToStart = - this.currentEventIndex === this.eventKeys.length - 1 && newIndex === 0; - - // If we looped back and loaded from URL, re-fetch to get latest data - if (loopedBackToStart && this.eventSourceURL) { - this.infoLogger.add( - 'Looped back to start - refreshing events from URL', - 'Event', - ); - this.loadEventsFromURL(this.eventSourceURL).catch((error) => { - this.infoLogger.add( - `Failed to refresh from URL: ${error.message}`, - 'Event', - ); - }); - } - - // Update current index - this.currentEventIndex = newIndex; + // Update current index for navigation tracking only + this.currentEventIndex = this.eventKeys.indexOf(eventKey); const event = this.eventsData[eventKey]; diff --git a/packages/phoenix-ng/projects/phoenix-ui-components/lib/services/file-loader.service.ts b/packages/phoenix-ng/projects/phoenix-ui-components/lib/services/file-loader.service.ts index 509d862a..d96380e3 100644 --- a/packages/phoenix-ng/projects/phoenix-ui-components/lib/services/file-loader.service.ts +++ b/packages/phoenix-ng/projects/phoenix-ui-components/lib/services/file-loader.service.ts @@ -11,7 +11,7 @@ import { JiveXMLLoader } from 'phoenix-event-display'; }) export class FileLoaderService { private lastEventsURL: string = ''; - private lastEventsOptions: boolean = false; + private lastEventsOptions: any = {}; async unzip(data: ArrayBuffer) { const archive = new JSZip(); @@ -104,7 +104,12 @@ export class FileLoaderService { reloadLastEvents(eventDisplay: EventDisplayService) { if (this.lastEventsURL.length > 0) { - this.loadEvent(this.lastEventsURL, eventDisplay, this.lastEventsOptions); + // Force ignoring caches when reloading for live cycling + const reloadOptions = { + ...(this.lastEventsOptions || {}), + cache: 'no-store', + }; + this.loadEvent(this.lastEventsURL, eventDisplay, reloadOptions); } } } From a54466d36167153af02dea903f3bc07ced80d2df Mon Sep 17 00:00:00 2001 From: DhanashreePetare Date: Fri, 9 Jan 2026 02:03:26 +0530 Subject: [PATCH 3/4] refactor: remove unused URL loading code per reviewer feedback (#448) --- guides/developers/url-event-loading.md | 126 +++++++++--------- .../src/event-display.ts | 89 ------------- .../src/managers/ui-manager/dat-gui-ui.ts | 47 ------- .../src/managers/ui-manager/index.ts | 16 --- .../phoenix-menu/phoenix-menu-ui.ts | 65 --------- .../src/managers/ui-manager/phoenix-ui.ts | 6 - 6 files changed, 61 insertions(+), 288 deletions(-) diff --git a/guides/developers/url-event-loading.md b/guides/developers/url-event-loading.md index 8388a1a5..1d703b2e 100644 --- a/guides/developers/url-event-loading.md +++ b/guides/developers/url-event-loading.md @@ -2,106 +2,102 @@ ## Overview -The URL Event Loading feature enables loading event data directly from a URL. Automatic refresh is supported only when using the cycling flow (cycle-events component) and wrapping from the last event back to the first, making it useful for live event displays where new events are continuously being generated. +The URL Event Loading feature enables loading event data directly from a URL. When using the cycling component with reloading enabled, events automatically refresh when wrapping from the last event back to the first. This is useful for live event displays where new events are continuously being generated. -## Features - -- **Load events from URL**: Fetch event data from any HTTP/HTTPS endpoint returning Phoenix JSON format -- **Auto-refresh on loop-back (cycling only)**: Automatically refresh events when the cycling flow wraps from last → first. Manual selection does not trigger refresh. -- **Manual refresh**: Manually refresh events from the current URL source at any time -- **Live status display**: Visual indicator showing if events are loaded from URL and the current source -- **Error handling**: Graceful error handling with logging to the info logger +## Important Limitations -## Usage +**CORS (Cross-Origin Resource Sharing)**: Due to browser security, loading events from URLs will fail unless the server hosting the event data explicitly allows your Phoenix deployment's origin. For live displays, ensure the server includes appropriate CORS headers. This limitation makes the feature most practical for: +- Same-origin deployments (Phoenix and events on the same server) +- Servers you control where CORS headers can be configured +- Development/testing environments with CORS disabled -### Dat.GUI Controls +## Features -When dat.GUI menu is enabled, a "Load from URL" folder is automatically added with: +- **Load events from URL**: Use the existing file loader to fetch event data from HTTP/HTTPS endpoints +- **Auto-refresh on cycling**: When cycling is active with reloading enabled, automatically refresh events when wrapping from last → first +- **Multiple format support**: Works with JSON, JiveXML, and zipped files (via existing fileLoader) +- **Error handling**: Graceful error handling with logging -- **Event URL**: Text input field for the URL of the event data file -- **Load from URL**: Button to load events from the specified URL -- **Refresh from URL**: Button to manually refresh events -- **Status**: Display showing "Live: [URL]" or "Not loaded from URL" +## Usage -### Phoenix Menu Controls +### Via Cycling Component -When Phoenix menu is enabled, a "Load from URL" node is automatically added with: +The way to use URL loading is through the cycling component in phoenix-ng: -- **Set URL (prompt)**: Button that prompts for a new URL -- **Load from URL**: Button to load events from the specified URL -- **Refresh from URL**: Button to manually refresh events -- **Status**: Label showing current load status +1. Load events from a URL using the file loader (same as loading local files) +2. Enable cycling mode +3. Toggle reload mode (click cycle button additional times) +4. When cycling wraps from last event to first, events automatically refresh with `cache: 'no-store'` -### Programmatic API +### Programmatic API (phoenix-ng) ```typescript -// Load events from a URL -try { - const eventKeys = await eventDisplay.loadEventsFromURL('https://example.com/events.json'); - console.log('Loaded events:', eventKeys); -} catch (error) { - console.error('Failed to load events:', error); -} - -// Check if currently loaded from URL -if (eventDisplay.isLoadedFromURL()) { - const url = eventDisplay.getEventSourceURL(); - console.log('Currently loading from:', url); -} +// Load events from URL using file loader service +fileLoaderService.loadEvent('https://example.com/events.json', eventDisplay); -// Manually refresh from the current URL source -try { - await eventDisplay.refreshEventsFromURL(); - console.log('Events refreshed'); -} catch (error) { - console.error('Refresh failed:', error); -} +// Reload last events (used internally by cycling component) +fileLoaderService.reloadLastEvents(eventDisplay); ``` -## Auto-Refresh Behavior (Cycling Flow) +## Auto-Refresh Behavior (Cycling Flow Only) + +Auto-refresh is triggered only when: +1. The cycling component is active +2. Reload mode is enabled (indicated by cycling UI state) +3. The cycle wraps from the last event to the first event -Auto-refresh is performed only by the cycling component. When cycling is active and configured to reload, wrapping from the last event to the first triggers `fileLoader.reloadLastEvents()` which re-fetches the last loaded URL with `cache: 'no-store'` to bypass caches. Manual navigation (including selecting last then first) does not trigger refresh. +When these conditions are met, `fileLoader.reloadLastEvents()` is called, which re-fetches the last loaded URL with `cache: 'no-store'` to bypass browser caches. + +**Manual navigation does not trigger auto-refresh**. If you manually select the last event then first event, no refresh occurs. ### Example Workflow -1. User loads 10 events from `https://example.com/events.json` -2. User navigates through events: 1 → 2 → 3 → ... → 10 -3. User clicks next while on event 10, returning to event 1 -4. System automatically refreshes from the URL (e.g., now there are 15 events) -5. User continues viewing the newly loaded events +1. User loads events from URL via file loader: `https://example.com/events.json` (10 events) +2. User enables cycling mode in the cycle-events component +3. User enables reload mode (toggle cycle button additional times until reloading = true) +4. Cycling runs: event 1 → 2 → 3 → ... → 10 +5. When cycling wraps (10 → 1), `fileLoader.reloadLastEvents()` is automatically called +6. Fresh events are fetched with `cache: 'no-store'` (e.g., now 15 events available) +7. Cycling continues with the newly loaded events ## Expected URL Format -The URL should return a JSON object matching the Phoenix event data format: +The URL should return event data in a format supported by Phoenix loaders: +**JSON format:** ```json { "event_0": { "RunNumber": 123, "EventNumber": 1, "collections": { - "Tracks": [ - // track data... - ], - "Hits": [ - // hit data... - ] + "Tracks": [ /* track data */ ], + "Hits": [ /* hit data */ ] } }, - "event_1": { - // event data... - } + "event_1": { /* event data */ } } ``` +**JiveXML format:** Standard JiveXML event data format (`.xml` extension) + +**Zipped files:** Any supported format compressed as `.zip` + +## Technical Implementation + +- URLs are loaded through the existing `FileLoaderService.loadEvent()` method +- The last loaded URL is tracked in `fileLoaderService.lastEventsURL` +- On reload, `cache: 'no-store'` is passed to the fetch options to bypass caches +- The cycling component calls `fileLoader.reloadLastEvents()` when wrapping with reload enabled +- All existing format parsing (JSON, JiveXML, zip) works transparently with URLs + ## Error Handling -- Invalid URLs result in fetch errors logged to the info logger -- Failed refreshes during auto-refresh are logged but don't interrupt event navigation -- Network errors are captured and displayed to the user via alerts (in UI controls) -- The system gracefully falls back to cached events if refresh fails +- Invalid URLs result in fetch errors logged to console +- Failed refreshes are logged but don't interrupt cycling +- Network errors are captured and reported +- CORS errors will prevent loading entirely - ensure proper server configuration ## Related Issues -- #448: Main feature implementation -- #177: Track extension (merged, affects rk-helper.ts) +- #448: Main feature implementation (URL loading with auto-refresh in cycling) diff --git a/packages/phoenix-event-display/src/event-display.ts b/packages/phoenix-event-display/src/event-display.ts index 106535aa..787d65b6 100644 --- a/packages/phoenix-event-display/src/event-display.ts +++ b/packages/phoenix-event-display/src/event-display.ts @@ -47,12 +47,6 @@ export class EventDisplay { private stateManager: StateManager; /** URL manager for managing options given through URL. */ private urlOptionsManager: URLOptionsManager; - /** URL of the currently loaded event source (if loaded from URL). */ - private eventSourceURL: string | null = null; - /** Current event index for detecting loop-back to start. */ - private currentEventIndex: number = 0; - /** Event keys array for navigation tracking. */ - private eventKeys: string[] = []; /** * Create the Phoenix event display and intitialize all the elements. @@ -81,8 +75,6 @@ export class EventDisplay { this.ui.init(configuration); // Set up for the state manager this.getStateManager().setEventDisplay(this); - // Pass this EventDisplay instance to UI for URL loader functionality - this.ui.setEventDisplay(this); // Animate loop const uiLoop = () => { @@ -132,10 +124,6 @@ export class EventDisplay { } const eventKeys = this.configuration.eventDataLoader.getEventsList(eventsData); - // Track event keys for loop-back detection - this.eventKeys = eventKeys; - // Reset index when loading new events - this.currentEventIndex = 0; this.loadEvent(eventKeys[0]); this.onEventsChange.forEach((callback) => callback(eventKeys)); @@ -175,9 +163,6 @@ export class EventDisplay { * @param eventKey String that represents the event in the eventsData object. */ public loadEvent(eventKey: any) { - // Update current index for navigation tracking only - this.currentEventIndex = this.eventKeys.indexOf(eventKey); - const event = this.eventsData[eventKey]; if (event) { @@ -209,80 +194,6 @@ export class EventDisplay { return this.infoLogger; } - /** - * Load event data from a URL. Automatically refreshes when looping back to start. - * @param url URL of the event data file (should return Phoenix JSON format). - * @returns Promise that resolves when events are loaded. - */ - public async loadEventsFromURL(url: string): Promise { - try { - this.infoLogger.add(`Loading events from URL: ${url}`, 'Event'); - this.loadingManager.addLoadableItem(`url_events_${url}`); - - const response = await fetch(url, { - method: 'GET', - headers: { - Accept: 'application/json', - }, - // No caching to always get fresh data on refresh - cache: 'no-store', - }); - - if (!response.ok) { - throw new Error( - `HTTP error! status: ${response.status} ${response.statusText}`, - ); - } - - const eventsData = await response.json(); - - // Store URL for auto-refresh on loop-back - this.eventSourceURL = url; - - this.loadingManager.itemLoaded(`url_events_${url}`); - this.infoLogger.add('Events loaded successfully from URL', 'Event'); - - // Parse and load events - return this.parsePhoenixEvents(eventsData); - } catch (error) { - this.loadingManager.itemLoaded(`url_events_${url}`); - const errorMessage = - error instanceof Error ? error.message : 'Unknown error'; - this.infoLogger.add( - `Failed to load events from URL: ${errorMessage}`, - 'Event', - ); - throw error; - } - } - - /** - * Check if events are currently loaded from a URL (for live display). - * @returns True if events are from a URL, false otherwise. - */ - public isLoadedFromURL(): boolean { - return this.eventSourceURL !== null; - } - - /** - * Get the current event source URL if loaded from URL. - * @returns The URL or null if not loaded from URL. - */ - public getEventSourceURL(): string | null { - return this.eventSourceURL; - } - - /** - * Manually refresh events from the current URL source. - * @returns Promise that resolves when refresh completes, or rejects if not loaded from URL. - */ - public async refreshEventsFromURL(): Promise { - if (!this.eventSourceURL) { - throw new Error('No URL source to refresh from'); - } - return this.loadEventsFromURL(this.eventSourceURL); - } - /** * Get the loading manager for managing loadable items. * @returns The loading manager. diff --git a/packages/phoenix-event-display/src/managers/ui-manager/dat-gui-ui.ts b/packages/phoenix-event-display/src/managers/ui-manager/dat-gui-ui.ts index e86f3f43..81af57d8 100644 --- a/packages/phoenix-event-display/src/managers/ui-manager/dat-gui-ui.ts +++ b/packages/phoenix-event-display/src/managers/ui-manager/dat-gui-ui.ts @@ -458,51 +458,4 @@ export class DatGUIMenuUI implements PhoenixUI { public getEventDataTypeFolder(typeName: string): GUI { return this.eventFolder.__folders[typeName]; } - - /** - * Add event URL loader controls to the menu. - * @param eventDisplay The event display instance for loading events from URL. - */ - public addEventURLLoader(eventDisplay: any): void { - const loaderFolder = this.gui.addFolder('Load from URL'); - - const params = { - url: 'https://example.com/events.json', - loadFromURL: () => { - if (params.url) { - eventDisplay - .loadEventsFromURL(params.url) - .then(() => { - alert('Events loaded successfully from URL'); - }) - .catch((error: Error) => { - alert(`Failed to load: ${error.message}`); - }); - } - }, - refreshURL: () => { - eventDisplay - .refreshEventsFromURL() - .then(() => { - alert('Events refreshed from URL'); - }) - .catch((error: Error) => { - alert(`Refresh failed: ${error.message}`); - }); - }, - liveStatus: () => { - const isLive = eventDisplay.isLoadedFromURL(); - const url = eventDisplay.getEventSourceURL(); - return isLive ? `Live: ${url}` : 'Not loaded from URL'; - }, - }; - - loaderFolder.add(params, 'url').name('Event URL'); - loaderFolder.add(params, 'loadFromURL').name('Load from URL'); - loaderFolder.add(params, 'refreshURL').name('Refresh from URL'); - loaderFolder - .add({ status: params.liveStatus() }, 'status') - .name('Status') - .listen(); - } } diff --git a/packages/phoenix-event-display/src/managers/ui-manager/index.ts b/packages/phoenix-event-display/src/managers/ui-manager/index.ts index 374e9250..02686a65 100644 --- a/packages/phoenix-event-display/src/managers/ui-manager/index.ts +++ b/packages/phoenix-event-display/src/managers/ui-manager/index.ts @@ -88,9 +88,6 @@ export class UIManager { /** State manager for managing the event display's state. */ private stateManager: StateManager; - /** Event display instance for URL loading functionality. */ - private eventDisplay: any; - /** * Constructor for the UI manager. * @param three Three manager to perform three.js related operations. @@ -170,19 +167,6 @@ export class UIManager { } /** - * Set the event display instance (for URL loading functionality). - * @param eventDisplay The event display instance. - */ - public setEventDisplay(eventDisplay: any): void { - this.eventDisplay = eventDisplay; - // Add URL loader to all UI menus - this.uiMenus.forEach((menu) => { - if (menu.addEventURLLoader) { - menu.addEventURLLoader(eventDisplay); - } - }); - } - /** * Add geometry (detector geometry) folder to the dat.GUI and Phoenix menu. */ diff --git a/packages/phoenix-event-display/src/managers/ui-manager/phoenix-menu/phoenix-menu-ui.ts b/packages/phoenix-event-display/src/managers/ui-manager/phoenix-menu/phoenix-menu-ui.ts index 15ef17b1..1edf3bba 100644 --- a/packages/phoenix-event-display/src/managers/ui-manager/phoenix-menu/phoenix-menu-ui.ts +++ b/packages/phoenix-event-display/src/managers/ui-manager/phoenix-menu/phoenix-menu-ui.ts @@ -488,69 +488,4 @@ export class PhoenixMenuUI implements PhoenixUI { this.eventFolder.loadStateFromJSON(this.eventFolderState); } } - - /** - * Add event URL loader controls to the Phoenix menu. - * @param eventDisplay The event display instance for loading events from URL. - */ - public addEventURLLoader(eventDisplay: any): void { - const urlLoaderNode = this.phoenixMenuRoot.addChild( - 'Load from URL', - undefined, - 'cloud', - ); - - // URL input field (simulated as label + button since we can't directly input) - let eventURL = 'https://example.com/events.json'; - - urlLoaderNode.addConfig({ - type: 'button', - label: 'Set URL (prompt)', - onClick: () => { - const url = prompt('Enter event data URL:', eventURL); - if (url) { - eventURL = url; - } - }, - }); - - urlLoaderNode.addConfig({ - type: 'button', - label: 'Load from URL', - onClick: () => { - if (eventURL) { - eventDisplay - .loadEventsFromURL(eventURL) - .then(() => { - alert('Events loaded successfully from URL'); - }) - .catch((error: Error) => { - alert(`Failed to load: ${error.message}`); - }); - } - }, - }); - - urlLoaderNode.addConfig({ - type: 'button', - label: 'Refresh from URL', - onClick: () => { - eventDisplay - .refreshEventsFromURL() - .then(() => { - alert('Events refreshed from URL'); - }) - .catch((error: Error) => { - alert(`Refresh failed: ${error.message}`); - }); - }, - }); - - urlLoaderNode.addConfig({ - type: 'label', - label: eventDisplay.isLoadedFromURL() - ? `Live: ${eventDisplay.getEventSourceURL()}` - : 'Not loaded from URL', - }); - } } diff --git a/packages/phoenix-event-display/src/managers/ui-manager/phoenix-ui.ts b/packages/phoenix-event-display/src/managers/ui-manager/phoenix-ui.ts index 3ca3e2ae..7d9f9a56 100644 --- a/packages/phoenix-event-display/src/managers/ui-manager/phoenix-ui.ts +++ b/packages/phoenix-event-display/src/managers/ui-manager/phoenix-ui.ts @@ -73,10 +73,4 @@ export interface PhoenixUI { * @returns Folder of the event data type. */ getEventDataTypeFolder(typeName: string): T | undefined; - - /** - * Add event URL loader controls to the menu. - * @param eventDisplay The event display instance for loading events from URL. - */ - addEventURLLoader?(eventDisplay: any): void; } From c555c82bf3d352306e83b16067451cffea414316 Mon Sep 17 00:00:00 2001 From: DhanashreePetare Date: Sat, 31 Jan 2026 02:05:16 +0530 Subject: [PATCH 4/4] docs: align URL loading guide with cycling-only reload (#448) --- guides/developers/url-event-loading.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/guides/developers/url-event-loading.md b/guides/developers/url-event-loading.md index 1d703b2e..424aea41 100644 --- a/guides/developers/url-event-loading.md +++ b/guides/developers/url-event-loading.md @@ -2,7 +2,7 @@ ## Overview -The URL Event Loading feature enables loading event data directly from a URL. When using the cycling component with reloading enabled, events automatically refresh when wrapping from the last event back to the first. This is useful for live event displays where new events are continuously being generated. +Phoenix already supports loading event data from a URL via the existing file loader. This update focuses on the cycling flow: when reloading is enabled, the URL is re-fetched with `cache: 'no-store'` when cycling wraps from the last event back to the first. This is useful for live event displays where new events are continuously being generated. ## Important Limitations @@ -13,8 +13,8 @@ The URL Event Loading feature enables loading event data directly from a URL. Wh ## Features -- **Load events from URL**: Use the existing file loader to fetch event data from HTTP/HTTPS endpoints - **Auto-refresh on cycling**: When cycling is active with reloading enabled, automatically refresh events when wrapping from last → first +- **Cache bypass on reload**: Uses `cache: 'no-store'` to avoid stale URL responses - **Multiple format support**: Works with JSON, JiveXML, and zipped files (via existing fileLoader) - **Error handling**: Graceful error handling with logging @@ -90,6 +90,7 @@ The URL should return event data in a format supported by Phoenix loaders: - On reload, `cache: 'no-store'` is passed to the fetch options to bypass caches - The cycling component calls `fileLoader.reloadLastEvents()` when wrapping with reload enabled - All existing format parsing (JSON, JiveXML, zip) works transparently with URLs +- No new EventDisplay URL APIs or UI controls are introduced in this change ## Error Handling