From 2513831c93e04c6a51e552bf289140aacff2e117 Mon Sep 17 00:00:00 2001 From: ugyballoons Date: Wed, 19 Nov 2025 17:22:06 +0000 Subject: [PATCH 1/6] Match updated data fields by property --- lib/workspace/data.dart | 31 +++++++++++++++++++++++++------ 1 file changed, 25 insertions(+), 6 deletions(-) diff --git a/lib/workspace/data.dart b/lib/workspace/data.dart index 1ad19ae..0e1205b 100644 --- a/lib/workspace/data.dart +++ b/lib/workspace/data.dart @@ -455,18 +455,37 @@ class DataCenter { } SchemaField field = dataSource.tables[tableName]!.fields[columnName]!; - if (series.fields.containsValue(field)) { + + // Find matching field by name and table instead of object reference + SchemaField? matchingSeriesField; + AxisId? matchingAxisId; + + for (MapEntry entry in series.fields.entries) { + SchemaField seriesField = entry.value; + if (seriesField.name == field.name && + seriesField.schema.name == field.schema.name && + seriesField.database.name == field.database.name) { + matchingSeriesField = field; + matchingAxisId = entry.key; + break; + } + } + + if (matchingSeriesField != null && matchingAxisId != null) { if (field.isString) { - columns[field] = List.from(data[plotColumn]!.map((e) => e)); + columns[matchingSeriesField] = List.from(data[plotColumn]!.map((e) => e)); } else if (field.isNumerical) { - columns[field] = List.from(data[plotColumn]!.map((e) => e.toDouble())); + columns[matchingSeriesField] = List.from(data[plotColumn]!.map((e) => e.toDouble())); } else if (field.isDateTime) { - columns[field] = List.from(data[plotColumn]!.map((e) => convertRubinDate(e))); + columns[matchingSeriesField] = + List.from(data[plotColumn]!.map((e) => convertRubinDate(e))); } // Add the column to the series columns - AxisId axisId = series.axes[series.fields.values.toList().indexOf(field)]; - seriesColumns[axisId] = field; + seriesColumns[matchingAxisId] = matchingSeriesField; + } else { + reportError("Plot column '$plotColumn' does not match any series fields."); + return; } } From bddee1ec924a0e59816a00b182e8f6ea5adfd667 Mon Sep 17 00:00:00 2001 From: ugyballoons Date: Thu, 20 Nov 2025 15:06:22 +0000 Subject: [PATCH 2/6] Clear workspace before loading state from file Also add copious debug logs. --- lib/chart/base.dart | 50 +++++++++------ lib/focal_plane/chart.dart | 21 ++++++- lib/workspace/controller.dart | 12 ++++ lib/workspace/data.dart | 34 ++++++++--- lib/workspace/state.dart | 112 +++++++++++++++++++++++++--------- lib/workspace/viewer.dart | 24 ++++++-- lib/workspace/window.dart | 13 ++++ 7 files changed, 202 insertions(+), 64 deletions(-) diff --git a/lib/chart/base.dart b/lib/chart/base.dart index 1703b1f..60c5563 100644 --- a/lib/chart/base.dart +++ b/lib/chart/base.dart @@ -306,7 +306,8 @@ class ChartState extends WindowState { SeriesData? seriesData = DataCenter().getSeriesData(seriesInfo.id); if (seriesData == null) { // Data not loaded yet, skip this series silently - developer.log("Data not yet loaded for series '${seriesInfo.name}'", name: "rubin_chart.workspace"); + developer.log("Data not yet loaded for series '${seriesInfo.name}'", + name: "rubintv.chart.workspace"); continue; } @@ -315,11 +316,11 @@ class ChartState extends WindowState { } catch (e) { if (e is DataConversionException) { developer.log("Failed to convert series '${seriesInfo.name}': ${e.message}", - name: "rubin_chart.workspace"); + name: "rubintv.chart.workspace"); reportError("Chart rendering error: ${e.message}"); } else { developer.log("Unexpected error converting series '${seriesInfo.name}': $e", - name: "rubin_chart.workspace", error: e); + name: "rubintv.chart.workspace", error: e); } // Skip this series and continue with others } @@ -399,6 +400,9 @@ class ChartBloc extends WindowBloc { } ChartBloc(super.initialState) { + developer.log("=== CREATING CHART BLOC ===", name: "rubintv.chart.base"); + developer.log("Initial state: id=${state.id}, type=${state.windowType}", name: "rubintv.chart.base"); + /// Listen for messages from the websocket. _subscription = WebSocketManager().messages.listen((message) { add(ChartReceiveMessageEvent(message)); @@ -406,6 +410,8 @@ class ChartBloc extends WindowBloc { /// Reload the data if the global query or global dayObs changes. _globalQuerySubscription = ControlCenter().globalQueryStream.listen((GlobalQuery? query) { + developer.log("Chart ${state.id} received global query update: dayObs=${query?.dayObs}", + name: "rubintv.chart.base"); for (SeriesInfo series in state._series.values) { add(UpdateSeriesEvent( series: series, @@ -415,6 +421,8 @@ class ChartBloc extends WindowBloc { } }); + developer.log("Chart bloc created and subscribed to streams", name: "rubintv.chart.base"); + /// Change the selection tool. on((event, emit) { emit(state.copyWith(tool: event.tool)); @@ -513,25 +521,25 @@ class ChartBloc extends WindowBloc { int columns = event.message["content"]["data"].length; developer.log( "received $columns columns and $rows rows for ${event.message["requestId"]} in series $seriesId", - name: "rubin_chart.workspace"); + name: "rubintv.chart.workspace"); // Add validation to ensure series exists and data is not empty final seriesInfo = state.series[seriesId]; if (seriesInfo == null) { - developer.log("Series $seriesId not found in state", name: "rubin_chart.workspace"); + developer.log("Series $seriesId not found in state", name: "rubintv.chart.workspace"); return; } final data = event.message["content"]["data"] as Map?; if (data == null || data.isEmpty) { - developer.log("Received empty data for series $seriesId", name: "rubin_chart.workspace"); + developer.log("Received empty data for series $seriesId", name: "rubintv.chart.workspace"); return; } // Validate that all expected columns are present final plotColumns = List.from(event.message["content"]["columns"].map((e) => e)); if (plotColumns.isEmpty) { - developer.log("No plot columns received for series $seriesId", name: "rubin_chart.workspace"); + developer.log("No plot columns received for series $seriesId", name: "rubintv.chart.workspace"); return; } @@ -566,12 +574,12 @@ class ChartBloc extends WindowBloc { )); } } else { - developer.log("Could not find pending series $seriesId", name: "rubin_chart.workspace"); - developer.log("Pending series: ${_pendingRowCountChecks.keys}", name: "rubin_chart.workspace"); + developer.log("Could not find pending series $seriesId", name: "rubintv.chart.workspace"); + developer.log("Pending series: ${_pendingRowCountChecks.keys}", name: "rubintv.chart.workspace"); } } catch (e) { - developer.log("Error processing count message: $e", name: "rubin_chart.workspace"); - developer.log("Message content: ${event.message["content"]}", name: "rubin_chart.workspace"); + developer.log("Error processing count message: $e", name: "rubintv.chart.workspace"); + developer.log("Message content: ${event.message["content"]}", name: "rubintv.chart.workspace"); } } emit(state.copyWith()); @@ -631,8 +639,13 @@ class ChartBloc extends WindowBloc { /// Reload all of the data from the server. on((event, emit) { + developer.log("=== SYNCHING DATA ===", name: "rubintv.chart.base"); + developer.log("Chart ${state.id}: dayObs=${event.dayObs}, skipGlobalUpdate=${event.skipGlobalUpdate}", + name: "rubintv.chart.base"); + // When loading from a file, we don't want to trigger the global query update unnecessarily if (!event.skipGlobalUpdate && event.globalQuery != null) { + developer.log("Updating global query in ControlCenter", name: "rubintv.chart.base"); // Update the global query in the control center which will notify all charts ControlCenter().updateGlobalQuery(GlobalQuery( query: event.globalQuery, @@ -643,14 +656,14 @@ class ChartBloc extends WindowBloc { } for (SeriesInfo series in state._series.values) { - developer.log("Synching data for series ${series.id}", name: "rubintv.chart.base.dart"); + developer.log("Synching data for series ${series.id}", name: "rubintv.chart.base"); _fetchSeriesData( series: series, globalQuery: event.globalQuery, dayObs: event.dayObs, ); - developer.log("synch request sent", name: "rubintv.chart.base.dart"); } + developer.log("Data sync requests sent for ${state._series.length} series", name: "rubintv.chart.base"); }); on((event, emit) { @@ -725,7 +738,7 @@ class ChartBloc extends WindowBloc { on((event, emit) { if (state.pendingRowCountDialog != null) { developer.log("User confirmed row count dialog, proceeding with series", - name: "rubin_chart.workspace"); + name: "rubintv.chart.workspace"); final dialogInfo = state.pendingRowCountDialog!; @@ -744,7 +757,7 @@ class ChartBloc extends WindowBloc { /// Handle user canceling the row count dialog on((event, emit) { if (state.pendingRowCountDialog != null) { - developer.log("User canceled row count dialog", name: "rubin_chart.workspace"); + developer.log("User canceled row count dialog", name: "rubintv.chart.workspace"); // Simply clear the dialog without proceeding emit(state.copyWith(pendingRowCountDialog: null)); @@ -785,7 +798,7 @@ class ChartBloc extends WindowBloc { // Check that the series has the correct number of columns and axes if (series.fields.length != state.axisInfo.length) { - developer.log("bad axes", name: "rubin_chart.core.chart.dart"); + developer.log("bad axes", name: "rubintv.core.chart.dart"); return null; } @@ -876,7 +889,7 @@ class ChartBloc extends WindowBloc { if (workspace.info?.instrument?.schema == null) { throw ArgumentError("The chosen instrument has no schema"); } - developer.log("New series fields: ${series.fields}", name: "rubin_chart.core.chart.dart"); + developer.log("New series fields: ${series.fields}", name: "rubintv.core.chart.dart"); return showDialog( context: context, builder: (BuildContext context) => Dialog( @@ -1008,8 +1021,11 @@ class ChartBloc extends WindowBloc { @override Future close() async { + developer.log("=== CLOSING CHART BLOC ===", name: "rubintv.chart.base"); + developer.log("Chart ${state.id} being closed", name: "rubintv.chart.base"); await _subscription.cancel(); await _globalQuerySubscription.cancel(); + developer.log("Chart bloc closed", name: "rubintv.chart.base"); return super.close(); } } diff --git a/lib/focal_plane/chart.dart b/lib/focal_plane/chart.dart index 85037df..6ca9140 100644 --- a/lib/focal_plane/chart.dart +++ b/lib/focal_plane/chart.dart @@ -282,6 +282,9 @@ class FocalPlaneChartBloc extends WindowBloc { } FocalPlaneChartBloc(super.initialState) { + developer.log("=== CREATING FOCAL PLANE CHART BLOC ===", name: "rubintv.focal_plane.chart"); + developer.log("Initial state: id=${state.id}", name: "rubintv.focal_plane.chart"); + _websocketSubscription = WebSocketManager().messages.listen((message) { add(FocalPlaneReceiveMessageEvent(message)); }); @@ -289,6 +292,9 @@ class FocalPlaneChartBloc extends WindowBloc { /// Subscribe to the selection controller to update the chart when points are selected. /// We use a timer so that we don't load data until the selection has stopped ControlCenter().selectionController.subscribe(state.id, (Object? origin, Set dataPoints) { + developer.log( + "Focal plane ${state.id} received selection update from $origin: ${dataPoints.length} points", + name: "rubintv.focal_plane.chart"); if (origin == state.id) { return; } @@ -300,6 +306,8 @@ class FocalPlaneChartBloc extends WindowBloc { /// Subscribe to the global query stream to update the chart when the query changes. _globalQuerySubscription = ControlCenter().globalQueryStream.listen((GlobalQuery? query) { + developer.log("Focal plane ${state.id} received global query update: dayObs=${query?.dayObs}", + name: "rubintv.focal_plane.chart"); Set? selected = ControlCenter().selectionController.selectedDataPoints.map((e) => e as DataId).toSet(); if (selected.isEmpty) { @@ -311,6 +319,8 @@ class FocalPlaneChartBloc extends WindowBloc { _fetchSeriesData(series: state.series, query: query?.query, dayObs: query?.dayObs, selected: selected); }); + developer.log("Focal plane chart bloc created and subscribed", name: "rubintv.focal_plane.chart"); + /// Initialize the chart. on((event, emit) { ColorbarController colorbarController = event.colorbarController; @@ -367,7 +377,7 @@ class FocalPlaneChartBloc extends WindowBloc { }, ); - developer.log("Selected data points: ${event.selected}", name: "rubin_chart.focal_plane.chart.dart"); + developer.log("Selected data points: ${event.selected}", name: "rubintv.focal_plane.chart.dart"); bool isNewPlot = state.data.isEmpty; @@ -436,6 +446,8 @@ class FocalPlaneChartBloc extends WindowBloc { /// Reload all of the data from the server. on((event, emit) { + developer.log("=== SYNCHING FOCAL PLANE DATA ===", name: "rubintv.focal_plane.chart"); + developer.log("Focal plane ${state.id}: dayObs=${event.dayObs}", name: "rubintv.focal_plane.chart"); _updateSeries(); }); } @@ -455,7 +467,7 @@ class FocalPlaneChartBloc extends WindowBloc { int rows = event.message["content"]["data"].values.first.length; int columns = event.message["content"]["data"].length; developer.log("received $columns columns and $rows rows for ${event.message["requestId"]}", - name: "rubin_chart.workspace"); + name: "rubintv.workspace"); if (rows > 0) { // Extract the data from the message @@ -545,8 +557,11 @@ class FocalPlaneChartBloc extends WindowBloc { /// Close the bloc. @override Future close() { + developer.log("=== CLOSING FOCAL PLANE CHART BLOC ===", name: "rubintv.focal_plane.chart"); + developer.log("Focal plane ${state.id} being closed", name: "rubintv.focal_plane.chart"); _websocketSubscription.cancel(); _globalQuerySubscription.cancel(); + developer.log("Focal plane chart bloc closed", name: "rubintv.focal_plane.chart"); return super.close(); } } @@ -584,7 +599,7 @@ class FocalPlaneChartViewerState extends State { /// We use a special editor for the series in a focal plane chart. Future _editSeries(BuildContext context, SeriesInfo series) async { WorkspaceViewerState workspace = WorkspaceViewer.of(context); - developer.log("New series fields: ${series.fields}", name: "rubin_chart.core.chart.dart"); + developer.log("New series fields: ${series.fields}", name: "rubintv.core.chart.dart"); DatabaseSchema schema = DataCenter().databases[workspace.info!.instrument!.schema]!; SchemaField field; if (series.fields.isNotEmpty) { diff --git a/lib/workspace/controller.dart b/lib/workspace/controller.dart index 2a6ac82..e22c24e 100644 --- a/lib/workspace/controller.dart +++ b/lib/workspace/controller.dart @@ -20,6 +20,7 @@ /// along with this program. If not, see . import 'dart:async'; +import 'dart:developer' as developer; import 'package:rubin_chart/rubin_chart.dart'; import 'package:rubintv_visualization/workspace/state.dart'; @@ -57,21 +58,30 @@ class ControlCenter { /// Update the global query. void updateGlobalQuery(GlobalQuery? query) { + developer.log("=== UPDATING GLOBAL QUERY ===", name: "rubintv.workspace.controller"); + developer.log( + "New query: ${query?.query?.toJson()}, dayObs: ${query?.dayObs}, instrument: ${query?.instrument?.name}", + name: "rubintv.workspace.controller"); _globalQueryController.add(query); } /// Update the selection data points. void updateSelection(Object chartId, Set dataPoints) { + developer.log("Updating selection from $chartId: ${dataPoints.length} points", + name: "rubintv.workspace.controller"); _selectionController.updateSelection(chartId, dataPoints); } /// Update the drill down data points. void updateDrillDown(Object chartId, Set dataPoints) { + developer.log("Updating drill down from $chartId: ${dataPoints.length} points", + name: "rubintv.workspace.controller"); _drillDownController.updateSelection(chartId, dataPoints); } /// Dispose of the stream controllers. void dispose() { + developer.log("Disposing ControlCenter", name: "rubintv.workspace.controller"); _globalQueryController.close(); _selectionController.dispose(); _drillDownController.dispose(); @@ -79,8 +89,10 @@ class ControlCenter { /// Reset the stream controllers. void reset() { + developer.log("=== RESETTING CONTROL CENTER ===", name: "rubintv.workspace.controller"); _globalQueryController.add(null); _selectionController.reset(); _drillDownController.reset(); + developer.log("ControlCenter reset complete", name: "rubintv.workspace.controller"); } } diff --git a/lib/workspace/data.dart b/lib/workspace/data.dart index 0e1205b..c2a3f63 100644 --- a/lib/workspace/data.dart +++ b/lib/workspace/data.dart @@ -382,20 +382,34 @@ class DataCenter { required List plotColumns, required Map> data, }) { + developer.log("=== UPDATING SERIES DATA ===", name: "rubintv_visualization.workspace.data"); + developer.log("Series: ${series.id}, DataSource: $dataSourceName, Columns: ${plotColumns.length}", + name: "rubintv_visualization.workspace.data"); + // Extensive validation if (data.isEmpty) { + developer.log("No data found for series ${series.id}", name: "rubintv_visualization.workspace.data"); reportError("No data found for the selected columns."); return; } // Check if any data lists are empty if (data.values.any((list) => list.isEmpty)) { + developer.log("Empty data lists found for series ${series.id}", + name: "rubintv_visualization.workspace.data"); + developer.log( + "Data keys with empty lists: ${data.entries.where((e) => e.value.isEmpty).map((e) => e.key)}", + name: "rubintv_visualization.workspace.data"); reportError("One or more columns contain no data."); return; } int rows = data.values.first.length; + developer.log("Data has $rows rows", name: "rubintv_visualization.workspace.data"); + if (rows == 0) { + developer.log("No non-null data found for series ${series.id}", + name: "rubintv_visualization.workspace.data"); reportError("No non-null data found for the selected columns."); return; } @@ -510,32 +524,34 @@ class DataCenter { dataIds: dataIds, ); + developer.log("Series data updated successfully for ${series.id}", + name: "rubintv_visualization.workspace.data"); _seriesData[series.id] = seriesData; } else { throw DataAccessException("Unknown data source: $dataSource"); } } - /// Check if two [SchemaField]s are compatible - bool isFieldCompatible(SchemaField field1, SchemaField field2) => { - field1.dataType == field2.dataType, - field1.unit == field2.unit, - }.every((e) => e); - - @override - String toString() => "DataCenter:[${databases.keys}]"; - void removeSeriesData(SeriesId id) { + developer.log("Removing series data for $id", name: "rubintv_visualization.workspace.data"); _seriesData.remove(id); } void clearSeriesData() { + developer.log("=== CLEARING ALL SERIES DATA ===", name: "rubintv_visualization.workspace.data"); + developer.log("Clearing ${_seriesData.length} series", name: "rubintv_visualization.workspace.data"); _seriesData.clear(); } void dispose() { _subscription.cancel(); } + + /// Check if two [SchemaField]s are compatible + bool isFieldCompatible(SchemaField field1, SchemaField field2) => { + field1.dataType == field2.dataType, + field1.unit == field2.unit, + }.every((e) => e); } /// DataId for an entry in the exposure or visit table diff --git a/lib/workspace/state.dart b/lib/workspace/state.dart index 1558433..2ff8495 100644 --- a/lib/workspace/state.dart +++ b/lib/workspace/state.dart @@ -432,7 +432,7 @@ class WorkspaceState extends WorkspaceStateBase { AppVersion fileVersion = AppVersion.fromJson(json["version"]); if (fileVersion != version) { developer.log("File version $fileVersion does not match current version $version. ", - name: "rubin_chart.workspace"); + name: "rubintv.workspace"); json = convertWorkspace(json, theme, version); } @@ -623,7 +623,7 @@ class WorkspaceBloc extends Bloc { /// A message is received from the websocket. on((event, emit) { - developer.log("Workspace Received message: ${event.message["type"]}", name: "rubin_chart.workspace"); + developer.log("Workspace Received message: ${event.message["type"]}", name: "rubintv.workspace"); if (event.message["type"] == "instrument info") { // Update the workspace to use the new instrument WorkspaceState state = this.state as WorkspaceState; @@ -640,19 +640,14 @@ class WorkspaceBloc extends Bloc { if (state.status == WorkspaceStatus.loadingInstrument && state.pendingJson != null) { // Build new workspace from JSON - WorkspaceState newState = WorkspaceState.fromJson( - state.pendingJson!, - (this.state as WorkspaceState).theme, - state.version, - ); - _applyWorkspaceJson(emit, newState); + _applyWorkspaceJsonWithClear(emit, state.pendingJson!, state); } } else if (event.message["type"] == "file content") { // Load the workspace from the file content add(LoadWorkspaceFromTextEvent(event.message["content"]["content"])); } else if (event.message["type"] == "error") { // Display the error message - developer.log("Received error message: ${event.message["content"]}", name: "rubin_chart.workspace"); + developer.log("Received error message: ${event.message["content"]}", name: "rubintv.workspace"); // Extract error details Map errorContent = event.message["content"]; @@ -684,7 +679,7 @@ class WorkspaceBloc extends Bloc { /// Update the global observation date. on((event, emit) { - developer.log("updating date to ${event.dayObs}!", name: "rubin_chart.workspace"); + developer.log("updating date to ${event.dayObs}!", name: "rubintv.workspace"); WorkspaceState state = this.state as WorkspaceState; state = state.updateObsDate(event.dayObs); ControlCenter().updateGlobalQuery(state.getGlobalQuery()); @@ -723,7 +718,7 @@ class WorkspaceBloc extends Bloc { Map windows = {...state.windows}; windows[newWindow.id] = newWindow; - developer.log("Added new focal plane window: $newWindow", name: "rubin_chart.workspace"); + developer.log("Added new focal plane window: $newWindow", name: "rubintv.workspace"); emit(state.copyWith(windows: windows)); }); @@ -742,7 +737,16 @@ class WorkspaceBloc extends Bloc { on((event, emit) { WorkspaceState state = this.state as WorkspaceState; Map windows = {...state.windows}; - windows.remove(event.windowId); + WindowMetaData? windowToRemove = windows[event.windowId]; + if (windowToRemove != null) { + // Close the bloc to cancel all its subscriptions + windowToRemove.bloc.close(); + + // Remove the window from the map + windows.remove(event.windowId); + + developer.log("Window ${event.windowId} removed and bloc closed", name: "rubintv.workspace.state"); + } emit(state.copyWith(windows: windows)); }); @@ -871,42 +875,70 @@ class WorkspaceBloc extends Bloc { void _onLoadWorkspaceFromText(LoadWorkspaceFromTextEvent event, Emitter emit) { WorkspaceState state = this.state as WorkspaceState; + developer.log("=== WORKSPACE LOAD START ===", name: "rubintv.workspace.load"); + developer.log("Current state: windows=${state.windows.length}, instrument=${state.instrument?.name}", + name: "rubintv.workspace.load"); + try { Map json = jsonDecode(event.text); + developer.log("JSON parsed successfully", name: "rubintv.workspace.load"); + Instrument newInstrument = Instrument.fromJson(json["instrument"]); + developer.log("New instrument: ${newInstrument.name}, current: ${state.instrument?.name}", + name: "rubintv.workspace.load"); if (state.instrument?.name != newInstrument.name) { + developer.log("Instrument mismatch - waiting for instrument load", name: "rubintv.workspace.load"); emit(state.copyWith( status: WorkspaceStatus.loadingInstrument, pendingJson: json, )); WebSocketManager().sendMessage(LoadInstrumentAction(instrument: newInstrument.name).toJson()); } else { + developer.log("Instrument matches - building workspace directly", name: "rubintv.workspace.load"); // Build new workspace from JSON - WorkspaceState newState = WorkspaceState.fromJson( - json, - (this.state as WorkspaceState).theme, - state.version, - ); - _applyWorkspaceJson(emit, newState); + _applyWorkspaceJsonWithClear(emit, json, state); } - } catch (e) { + } catch (e, stackTrace) { + developer.log("Error loading workspace: $e", + name: "rubintv.workspace.load", error: e, stackTrace: stackTrace); emit(state.copyWith(status: WorkspaceStatus.error, errorMessage: "Failed to load workspace: $e")); } } + Future _applyWorkspaceJsonWithClear( + Emitter emit, Map json, WorkspaceState currentState) async { + developer.log("=== CLEARING BEFORE JSON LOAD ===", name: "rubintv.workspace.load"); + + // 1. Clear the old workspace FIRST + await _clearWorkspace(currentState, skipGlobalQueryReset: true); + developer.log("Workspace cleared, now building from JSON", name: "rubintv.workspace.load"); + + // 2. Build new workspace from JSON AFTER clearing + WorkspaceState newState = WorkspaceState.fromJson( + json, + currentState.theme, + currentState.version, + ); + + // 3. Continue with the rest of the application logic + _applyWorkspaceJson(emit, newState); + } + /// Build a workspace from a JSON object. void _applyWorkspaceJson(Emitter emit, WorkspaceState newState) async { - WorkspaceState state = this.state as WorkspaceState; - - // Skip calling ControlCenter().reset() which would broadcast a null global query - // Instead, we'll explicitly close old windows and reset controllers but avoid unnecessary global query updates - await _clearWorkspace(state, skipGlobalQueryReset: true); + developer.log("=== APPLYING JSON WORKSPACE ===", name: "rubintv.workspace.load"); + developer.log("New state: windows=${newState.windows.length}, instrument=${newState.instrument?.name}", + name: "rubintv.workspace.load"); // First emit the new state so it's available everywhere - emit(newState); + if (!emit.isDone) { + emit(newState); + developer.log("New workspace state emitted", name: "rubintv.workspace.load"); + } String? dayObs = getFormattedDate(newState.dayObs); + developer.log("DayObs for sync: $dayObs", name: "rubintv.workspace.load"); // Use a flag to track whether the global query update was made, to avoid duplicate updates bool globalQueryUpdated = false; @@ -914,9 +946,12 @@ class WorkspaceBloc extends Bloc { // Then sync data for all windows, but we don't need to trigger the global query stream // as the charts will get their data directly for (var window in newState.windows.values) { + developer.log("Processing window ${window.id} of type ${window.windowType}", + name: "rubintv.workspace.load"); + if (window.bloc is ChartBloc) { - developer.log("Syncing data for window ${window.id} with dayObs=$dayObs", - name: "rubin_chart.workspace"); + developer.log("Syncing ChartBloc data for window ${window.id} with dayObs=$dayObs", + name: "rubintv.workspace.load"); // Send direct SynchData event instead of going through global query stream (window.bloc as ChartBloc).add(SynchDataEvent( @@ -926,43 +961,60 @@ class WorkspaceBloc extends Bloc { )); if (!globalQueryUpdated) { + developer.log("Updating global query (first time)", name: "rubintv.workspace.load"); // Update global query only once, after the first window is processed ControlCenter().updateGlobalQuery(newState.getGlobalQuery()); globalQueryUpdated = true; } } else if (window.bloc is FocalPlaneChartBloc) { + developer.log("Syncing FocalPlaneChartBloc data for window ${window.id}", + name: "rubintv.workspace.load"); (window.bloc as FocalPlaneChartBloc).add(SynchDataEvent( dayObs: dayObs, globalQuery: newState.globalQuery, )); if (!globalQueryUpdated) { + developer.log("Updating global query (first time - focal plane)", name: "rubintv.workspace.load"); // Update global query only once, after the first window is processed ControlCenter().updateGlobalQuery(newState.getGlobalQuery()); globalQueryUpdated = true; } } } + + developer.log("=== WORKSPACE LOAD COMPLETE ===", name: "rubintv.workspace.load"); } /// Clear the workspace and the DataCenter. Future _clearWorkspace(WorkspaceState state, {bool skipGlobalQueryReset = false}) async { + developer.log("=== CLEARING WORKSPACE ===", name: "rubintv.workspace.clear"); + developer.log("Windows to close: ${state.windows.length}, skipGlobalQueryReset: $skipGlobalQueryReset", + name: "rubintv.workspace.clear"); + // Close all of the windows and cancel their subscriptions. for (WindowMetaData window in state.windows.values) { - await window.bloc.close(); + if (window.windowType.isChart || window.windowType == WindowTypes.focalPlane) { + developer.log("Closing window ${window.id} of type ${window.windowType}", + name: "rubintv.workspace.clear"); + await window.bloc.close(); + } } + developer.log("All window blocs closed", name: "rubintv.workspace.clear"); if (skipGlobalQueryReset) { - // Only reset selection controllers without affecting global query + developer.log("Resetting selection controllers only", name: "rubintv.workspace.clear"); ControlCenter().selectionController.reset(); ControlCenter().drillDownController.reset(); } else { - // Full reset of all controllers including global query stream + developer.log("Full ControlCenter reset", name: "rubintv.workspace.clear"); ControlCenter().reset(); } // Clear the DataCenter Series Data. DataCenter().clearSeriesData(); + developer.log("DataCenter series data cleared", name: "rubintv.workspace.clear"); + developer.log("=== WORKSPACE CLEAR COMPLETE ===", name: "rubintv.workspace.clear"); } /// Cancel the subscription to the websocket. diff --git a/lib/workspace/viewer.dart b/lib/workspace/viewer.dart index fcd0664..295a32b 100644 --- a/lib/workspace/viewer.dart +++ b/lib/workspace/viewer.dart @@ -89,7 +89,7 @@ class WorkspaceViewerState extends State { @override void initState() { - developer.log("Initializing WorkspaceViewerState", name: "rubin_chart.workspace"); + developer.log("=== INITIALIZING WORKSPACE VIEWER ===", name: "rubintv.workspace.viewer"); super.initState(); ControlCenter().selectionController.subscribe(id, _onSelectionUpdate); @@ -99,7 +99,8 @@ class WorkspaceViewerState extends State { /// This isn't used now, but can be used in the future if any plots cannot be /// matched to obs_date,seq_num data IDs. void _onSelectionUpdate(Object? origin, Set dataPoints) { - developer.log("Selection updated: ${dataPoints.length}", name: "rubin_chart.workspace"); + developer.log("Workspace viewer received selection update from $origin: ${dataPoints.length} points", + name: "rubintv.workspace.viewer"); } @override @@ -108,14 +109,21 @@ class WorkspaceViewerState extends State { create: (context) => WorkspaceBloc()..add(InitializeWorkspaceEvent(theme, version)), child: BlocBuilder( builder: (context, state) { + developer.log("=== BUILDING WORKSPACE ===", name: "rubintv.workspace.viewer"); + developer.log("State type: ${state.runtimeType}", name: "rubintv.workspace.viewer"); + if (state is WorkspaceStateInitial) { + developer.log("Workspace state is initial - showing progress indicator", + name: "rubintv.workspace.viewer"); return const Center( child: CircularProgressIndicator(), ); } - + info = state as WorkspaceState?; if (state is WorkspaceState) { - info = state; + developer.log( + "Workspace state loaded: ${state.windows.length} windows, instrument=${state.instrument?.name}", + name: "rubintv.workspace.viewer"); return Column(children: [ Toolbar(workspace: state), SizedBox( @@ -124,13 +132,16 @@ class WorkspaceViewerState extends State { child: Builder( builder: (BuildContext context) { List children = []; - for (WindowMetaData window in info!.windows.values) { + for (WindowMetaData window in state.windows.values) { + developer.log("Building window ${window.id} of type ${window.windowType}", + name: "rubintv.workspace.viewer"); children.add(Positioned( left: window.offset.dx, top: window.offset.dy, child: buildWindow(window, state), )); } + developer.log("Built ${children.length} windows", name: "rubintv.workspace.viewer"); return Stack( children: children, @@ -149,6 +160,9 @@ class WorkspaceViewerState extends State { /// Build a window widget based on the type of the window. Widget buildWindow(WindowMetaData window, WorkspaceState workspace) { + developer.log("Building window widget for ${window.id} type ${window.windowType}", + name: "rubintv.workspace.viewer"); + if (window.windowType == WindowTypes.cartesianScatter || window.windowType == WindowTypes.polarScatter) { return ScatterPlotWidget(window: window, bloc: window.bloc as ChartBloc); } diff --git a/lib/workspace/window.dart b/lib/workspace/window.dart index 3ac6e30..08e34c7 100644 --- a/lib/workspace/window.dart +++ b/lib/workspace/window.dart @@ -165,18 +165,31 @@ class WindowMetaData { Offset offset = Offset(json["offset"]["dx"], json["offset"]["dy"]); Size size = Size(json["size"]["width"], json["size"]["height"]); String? title = json["title"] == "" ? null : json["title"]; + + developer.log("=== DESERIALIZING WINDOW ===", name: "rubintv_visualization.workspace.window"); + developer.log("Window ID: ${state.id}, Type: ${state.windowType}", + name: "rubintv_visualization.workspace.window"); + late WindowBloc bloc; if (state.windowType == WindowTypes.detectorSelector) { + developer.log("Creating detector selector bloc from JSON", + name: "rubintv_visualization.workspace.window"); bloc = WindowBloc(state); } else if (state.windowType.isBinned) { + developer.log("Creating binned bloc from JSON", name: "rubintv_visualization.workspace.window"); bloc = ChartBloc(state as BinnedState); } else if (state.windowType.isScatter) { + developer.log("Creating scatter bloc from JSON", name: "rubintv_visualization.workspace.window"); bloc = ChartBloc(state as ChartState); } else if (state.windowType == WindowTypes.focalPlane) { + developer.log("Creating focal plane bloc from JSON", name: "rubintv_visualization.workspace.window"); bloc = FocalPlaneChartBloc(state as FocalPlaneChartState); } else { throw ArgumentError("Unrecognized window type ${state.windowType}"); } + + developer.log("Window bloc created from JSON successfully", + name: "rubintv_visualization.workspace.window"); return WindowMetaData(offset: offset, size: size, title: title, bloc: bloc); } } From a69916705fdc1e4593010ab5490ad822e080b751 Mon Sep 17 00:00:00 2001 From: ugyballoons Date: Fri, 21 Nov 2025 14:13:11 +0000 Subject: [PATCH 3/6] Ensure charts are built when workspace changes --- lib/workspace/viewer.dart | 58 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/lib/workspace/viewer.dart b/lib/workspace/viewer.dart index 295a32b..ac68d78 100644 --- a/lib/workspace/viewer.dart +++ b/lib/workspace/viewer.dart @@ -33,6 +33,7 @@ import 'package:rubintv_visualization/workspace/controller.dart'; import 'package:rubintv_visualization/workspace/state.dart'; import 'package:rubintv_visualization/workspace/toolbar.dart'; import 'package:rubintv_visualization/workspace/window.dart'; +import 'package:rubintv_visualization/id.dart'; /// A [Widget] used to display a set of re-sizable and translatable [WindowMetaData] widgets in a container. class WorkspaceViewer extends StatefulWidget { @@ -108,6 +109,63 @@ class WorkspaceViewerState extends State { return BlocProvider( create: (context) => WorkspaceBloc()..add(InitializeWorkspaceEvent(theme, version)), child: BlocBuilder( + buildWhen: (previous, current) { + developer.log("BlocBuilder buildWhen: ${previous.runtimeType} -> ${current.runtimeType}", + name: "rubintv.workspace.viewer"); + + // Always rebuild if state types are different + if (previous.runtimeType != current.runtimeType) { + developer.log("State type changed - rebuilding", name: "rubintv.workspace.viewer"); + return true; + } + + // Always rebuild if we're coming from or going to initial state + if (previous is WorkspaceStateInitial || current is WorkspaceStateInitial) { + developer.log("Initial state transition - rebuilding", name: "rubintv.workspace.viewer"); + return true; + } + + if (previous is WorkspaceState && current is WorkspaceState) { + developer.log("Windows: ${previous.windows.length} -> ${current.windows.length}", + name: "rubintv.workspace.viewer"); + + // Rebuild if window count changed + if (previous.windows.length != current.windows.length) { + developer.log("Window count changed - rebuilding", name: "rubintv.workspace.viewer"); + return true; + } + + // Rebuild if window IDs are different (indicating different windows) + Set previousIds = previous.windows.keys.toSet(); + Set currentIds = current.windows.keys.toSet(); + if (!previousIds.containsAll(currentIds) || !currentIds.containsAll(previousIds)) { + developer.log("Window IDs changed - rebuilding", name: "rubintv.workspace.viewer"); + return true; + } + + // Rebuild if instrument changed + if (previous.instrument != current.instrument) { + developer.log("Instrument changed - rebuilding", name: "rubintv.workspace.viewer"); + return true; + } + + // If we have the same windows but different references, rebuild + for (UniqueId id in currentIds) { + if (previous.windows[id] != current.windows[id]) { + developer.log("Window $id content changed - rebuilding", name: "rubintv.workspace.viewer"); + return true; + } + } + + developer.log("No significant changes detected - not rebuilding", + name: "rubintv.workspace.viewer"); + return false; + } + + // For any other case, rebuild to be safe + developer.log("Unhandled state combination - rebuilding", name: "rubintv.workspace.viewer"); + return true; + }, builder: (context, state) { developer.log("=== BUILDING WORKSPACE ===", name: "rubintv.workspace.viewer"); developer.log("State type: ${state.runtimeType}", name: "rubintv.workspace.viewer"); From 8a00ee481456bbca19799412de916b747068bd2d Mon Sep 17 00:00:00 2001 From: ugyballoons Date: Mon, 24 Nov 2025 12:33:30 +0000 Subject: [PATCH 4/6] Improve workspace loading and resource cleanup - Fix workspace clearing to properly unsubscribe blocs before closing - Add proper async/await handling in workspace loading to prevent emit-after-completion errors - Fix memory leaks by cancelling subscriptions in FocalPlaneChartBloc.close() - Use stable ID for WorkspaceViewer selection subscription and add dispose cleanup - Add extensive logging throughout workspace load/clear cycle for debugging - Improve error handling and reporting during workspace deserialization - Add schema field validation with user-friendly error messages - Fix workspace rebuild logic to avoid unnecessary rebuilds during data loading --- lib/chart/base.dart | 81 ++++++++++++++++++++++---- lib/chart/series.dart | 105 +++++++++++++++++++++++----------- lib/editors/series.dart | 8 ++- lib/focal_plane/chart.dart | 10 +++- lib/main.dart | 3 + lib/workspace/controller.dart | 12 ++++ lib/workspace/data.dart | 55 +++++++++++++++++- lib/workspace/state.dart | 87 +++++++++++++++++++++++----- lib/workspace/viewer.dart | 51 ++++++++++------- 9 files changed, 325 insertions(+), 87 deletions(-) diff --git a/lib/chart/base.dart b/lib/chart/base.dart index 60c5563..13739ed 100644 --- a/lib/chart/base.dart +++ b/lib/chart/base.dart @@ -345,19 +345,47 @@ class ChartState extends WindowState { /// Create a [ChartState] from a JSON object. @override factory ChartState.fromJson(Map json) { - return ChartState( - id: UniqueId.fromString(json["id"]), - series: Map.fromEntries((json["series"] as List).map((e) { + developer.log("=== CHART STATE FROM JSON ===", name: "rubintv.chart.base"); + developer.log("JSON keys: ${json.keys}", name: "rubintv.chart.base"); + developer.log("ID: ${json['id']}", name: "rubintv.chart.base"); + developer.log("Window type: ${json['windowType']}", name: "rubintv.chart.base"); + developer.log("Series count: ${json['series']?.length}", name: "rubintv.chart.base"); + developer.log("Axis info count: ${json['axisInfo']?.length}", name: "rubintv.chart.base"); + + if (json['series'] != null) { + for (int i = 0; i < (json['series'] as List).length; i++) { + var seriesJson = json['series'][i]; + developer.log("Series $i: ${seriesJson.keys}", name: "rubintv.chart.base"); + if (seriesJson is Map && seriesJson.containsKey('fields')) { + developer.log("Series $i fields: ${seriesJson['fields']}", name: "rubintv.chart.base"); + } + } + } + + try { + Map series = Map.fromEntries((json["series"] as List).map((e) { + developer.log("Processing series JSON: ${e.keys}", name: "rubintv.chart.base"); SeriesInfo seriesInfo = SeriesInfo.fromJson(e); + developer.log("Series info created: ${seriesInfo.id}", name: "rubintv.chart.base"); return MapEntry(seriesInfo.id, seriesInfo); - })), - axisInfo: List.from(json["axisInfo"].map((e) => ChartAxisInfo.fromJson(e))), - legend: json["legend"] == null ? null : Legend.fromJson(json["legend"]), - useGlobalQuery: json["useGlobalQuery"], - windowType: WindowTypes.fromString(json["windowType"]), - tool: MultiSelectionTool.fromString(json["tool"]), - resetController: StreamController.broadcast(), - ); + })); + + return ChartState( + id: UniqueId.fromString(json["id"]), + series: series, + axisInfo: List.from(json["axisInfo"].map((e) => ChartAxisInfo.fromJson(e))), + legend: json["legend"] == null ? null : Legend.fromJson(json["legend"]), + useGlobalQuery: json["useGlobalQuery"], + windowType: WindowTypes.fromString(json["windowType"]), + tool: MultiSelectionTool.fromString(json["tool"]), + resetController: StreamController.broadcast(), + ); + } catch (e, stackTrace) { + developer.log("Error in ChartState.fromJson: $e", + name: "rubintv.chart.base", error: e, stackTrace: stackTrace); + developer.log("Full JSON: $json", name: "rubintv.chart.base"); + rethrow; + } } } @@ -408,6 +436,34 @@ class ChartBloc extends WindowBloc { add(ChartReceiveMessageEvent(message)); }); + /// Subscribe to selection controller to update when points are selected. + developer.log("Subscribing chart ${state.id} to selection controller", name: "rubintv.chart.base"); + ControlCenter().selectionController.subscribe(state.id, (Object? origin, Set dataPoints) { + developer.log("=== CHART SELECTION UPDATE ===", name: "rubintv.chart.base"); + developer.log("Chart ${state.id} received selection update from $origin: ${dataPoints.length} points", + name: "rubintv.chart.base"); + if (origin == state.id) { + developer.log("Ignoring selection update from self", name: "rubintv.chart.base"); + return; + } + developer.log("Processing selection update for chart ${state.id}", name: "rubintv.chart.base"); + // TODO: Handle selection update in chart + }); + + /// Subscribe to drill down controller. + developer.log("Subscribing chart ${state.id} to drill down controller", name: "rubintv.chart.base"); + ControlCenter().drillDownController.subscribe(state.id, (Object? origin, Set dataPoints) { + developer.log("=== CHART DRILL DOWN UPDATE ===", name: "rubintv.chart.base"); + developer.log("Chart ${state.id} received drill down update from $origin: ${dataPoints.length} points", + name: "rubintv.chart.base"); + if (origin == state.id) { + developer.log("Ignoring drill down update from self", name: "rubintv.chart.base"); + return; + } + developer.log("Processing drill down update for chart ${state.id}", name: "rubintv.chart.base"); + // TODO: Handle drill down update in chart + }); + /// Reload the data if the global query or global dayObs changes. _globalQuerySubscription = ControlCenter().globalQueryStream.listen((GlobalQuery? query) { developer.log("Chart ${state.id} received global query update: dayObs=${query?.dayObs}", @@ -1023,6 +1079,9 @@ class ChartBloc extends WindowBloc { Future close() async { developer.log("=== CLOSING CHART BLOC ===", name: "rubintv.chart.base"); developer.log("Chart ${state.id} being closed", name: "rubintv.chart.base"); + developer.log("Unsubscribing chart ${state.id} from selection controllers", name: "rubintv.chart.base"); + ControlCenter().selectionController.unsubscribe(state.id); + ControlCenter().drillDownController.unsubscribe(state.id); await _subscription.cancel(); await _globalQuerySubscription.cancel(); developer.log("Chart bloc closed", name: "rubintv.chart.base"); diff --git a/lib/chart/series.dart b/lib/chart/series.dart index 3ae767b..f7d44fe 100644 --- a/lib/chart/series.dart +++ b/lib/chart/series.dart @@ -1,23 +1,4 @@ -/// This file is part of the rubintv_visualization package. -/// -/// Developed for the LSST Data Management System. -/// This product includes software developed by the LSST Project -/// (https://www.lsst.org). -/// See the COPYRIGHT file at the top-level directory of this distribution -/// for details of code ownership. -/// -/// This program is free software: you can redistribute it and/or modify -/// it under the terms of the GNU General Public License as published by -/// the Free Software Foundation, either version 3 of the License, or -/// (at your option) any later version. -/// -/// This program is distributed in the hope that it will be useful, -/// but WITHOUT ANY WARRANTY; without even the implied warranty of -/// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -/// GNU General Public License for more details. -/// -/// You should have received a copy of the GNU General Public License -/// along with this program. If not, see . +import 'dart:developer' as developer; import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; @@ -161,24 +142,82 @@ class SeriesInfo { "marker": marker?.toJson(), "errorBars": errorBars?.toJson(), "axes": axes.map((e) => e.toJson()).toList(), - "fields": fields.entries.map((entry) => [entry.key.toJson(), entry.value.toJson()]).toList(), + "fields": Map.fromEntries( + fields.entries.map((entry) => MapEntry(entry.key.toJson(), entry.value.toJson()))), "query": query?.toJson(), }; } /// Create a [SeriesInfo] from a JSON object. static SeriesInfo fromJson(Map json) { - return SeriesInfo( - id: SeriesId.fromString(json["id"]), - name: json["name"], - marker: json["marker"] == null ? null : Marker.fromJson(json["marker"]), - errorBars: json["errorBars"] == null ? null : ErrorBars.fromJson(json["errorBars"]), - axes: (json["axes"] as List).map((e) => AxisId.fromJson(e)).toList(), - fields: Map.fromEntries((json["fields"] as List).map((e) { - List entry = e; - return MapEntry(AxisId.fromJson(entry[0]), SchemaField.fromJson(entry[1])); - })), - query: json["query"] == null ? null : QueryExpression.fromJson(json["query"]), - ); + developer.log("=== SERIES INFO FROM JSON ===", name: "rubintv.chart.series"); + developer.log("JSON keys: ${json.keys}", name: "rubintv.chart.series"); + developer.log("ID: ${json['id']}", name: "rubintv.chart.series"); + developer.log("Name: ${json['name']}", name: "rubintv.chart.series"); + developer.log("Fields: ${json['fields']}", name: "rubintv.chart.series"); + + try { + Map fields = {}; + + if (json["fields"] is List) { + // Handle old format: list of pairs + developer.log("Processing fields as list format", name: "rubintv.chart.series"); + for (dynamic fieldItem in json["fields"]) { + if (fieldItem is List && fieldItem.length == 2) { + var axisData = fieldItem[0]; + var fieldData = fieldItem[1]; + + developer.log("Processing field pair: $axisData -> $fieldData", name: "rubintv.chart.series"); + + if (axisData != null && fieldData != null) { + AxisId axisId = AxisId.fromJson(axisData); + SchemaField field = SchemaField.fromJson(fieldData as Map); + fields[axisId] = field; + developer.log("Field processed from list: $axisId -> ${field.name}", + name: "rubintv.chart.series"); + } else { + developer.log("Skipping null field pair: $axisData -> $fieldData", + name: "rubintv.chart.series"); + } + } else { + developer.log("Invalid field item format: $fieldItem", name: "rubintv.chart.series"); + } + } + } else if (json["fields"] is Map) { + // Handle new format: map + developer.log("Processing fields as map format", name: "rubintv.chart.series"); + Map fieldsMap = json["fields"] as Map; + for (MapEntry entry in fieldsMap.entries) { + developer.log("Processing field: ${entry.key} -> ${entry.value}", name: "rubintv.chart.series"); + if (entry.value != null) { + AxisId axisId = AxisId.fromJson(entry.key); + SchemaField field = SchemaField.fromJson(entry.value as Map); + fields[axisId] = field; + developer.log("Field processed from map: ${axisId} -> ${field.name}", + name: "rubintv.chart.series"); + } else { + developer.log("Skipping null field value for key: ${entry.key}", name: "rubintv.chart.series"); + } + } + } else { + developer.log("Unknown fields format: ${json["fields"].runtimeType}", name: "rubintv.chart.series"); + throw ArgumentError("Unknown fields format in JSON"); + } + + return SeriesInfo( + id: SeriesId.fromString(json["id"]), + name: json["name"], + axes: List.from(json["axes"].map((e) => AxisId.fromJson(e))), + fields: fields, + query: json.containsKey("query") && json["query"] != null + ? QueryExpression.fromJson(json["query"]) + : null, + ); + } catch (e, stackTrace) { + developer.log("Error in SeriesInfo.fromJson: $e", + name: "rubintv.chart.series", error: e, stackTrace: stackTrace); + developer.log("Full JSON: $json", name: "rubintv.chart.series"); + rethrow; + } } } diff --git a/lib/editors/series.dart b/lib/editors/series.dart index 49e89b7..ac606ff 100644 --- a/lib/editors/series.dart +++ b/lib/editors/series.dart @@ -345,7 +345,8 @@ class ColumnEditorState extends State { @override Widget build(BuildContext context) { if (_field != null) { - _table = _field!.schema; + // Ensure we're using the table instance from the database, not a deserialized copy + _table = widget.databaseSchema.tables[_field!.schema.name]; } List> tableEntries = []; @@ -376,7 +377,10 @@ class ColumnEditorState extends State { onChanged: (TableSchema? newTable) { setState(() { _table = newTable; - _field = _table!.fields.values.first; + // Ensure we get the field from the new table instance + if (_table != null && _table!.fields.isNotEmpty) { + _field = _table!.fields.values.first; + } }); widget.onChanged(_field); }, diff --git a/lib/focal_plane/chart.dart b/lib/focal_plane/chart.dart index 6ca9140..a62e4e2 100644 --- a/lib/focal_plane/chart.dart +++ b/lib/focal_plane/chart.dart @@ -556,11 +556,15 @@ class FocalPlaneChartBloc extends WindowBloc { /// Close the bloc. @override - Future close() { + Future close() async { developer.log("=== CLOSING FOCAL PLANE CHART BLOC ===", name: "rubintv.focal_plane.chart"); developer.log("Focal plane ${state.id} being closed", name: "rubintv.focal_plane.chart"); - _websocketSubscription.cancel(); - _globalQuerySubscription.cancel(); + + await _websocketSubscription.cancel(); + await _globalQuerySubscription.cancel(); + _playTimer?.cancel(); + _selectionTimer?.cancel(); + developer.log("Focal plane chart bloc closed", name: "rubintv.focal_plane.chart"); return super.close(); } diff --git a/lib/main.dart b/lib/main.dart index 34069fb..ed2359a 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -31,6 +31,7 @@ import 'package:rubintv_visualization/app.dart'; import 'package:rubintv_visualization/workspace/data.dart'; import 'package:rubintv_visualization/error.dart'; import 'package:rubintv_visualization/utils/browser_logger.dart'; +import 'package:rubintv_visualization/id.dart'; /// A function to get the current version of the application. Future getAppVersion() async { @@ -49,7 +50,9 @@ Future getAppVersion() async { /// The main function for the application. Future main() async { // Initialize browser-based logging + final firstId = UniqueId.next(); developer.log('Starting application with browser-based logging', name: 'rubinTV.visualization.main'); + developer.log('Test value of global _nextId: $firstId', name: 'rubinTV.visualization.main'); FlutterError.onError = (FlutterErrorDetails details) { if (details.exception is FlutterError) { diff --git a/lib/workspace/controller.dart b/lib/workspace/controller.dart index e22c24e..3bf88b7 100644 --- a/lib/workspace/controller.dart +++ b/lib/workspace/controller.dart @@ -67,16 +67,24 @@ class ControlCenter { /// Update the selection data points. void updateSelection(Object chartId, Set dataPoints) { + developer.log("=== UPDATING SELECTION ===", name: "rubintv.workspace.controller"); developer.log("Updating selection from $chartId: ${dataPoints.length} points", name: "rubintv.workspace.controller"); + developer.log("Current subscribers: ${_selectionController.observers.keys}", + name: "rubintv.workspace.controller"); _selectionController.updateSelection(chartId, dataPoints); + developer.log("Selection update complete", name: "rubintv.workspace.controller"); } /// Update the drill down data points. void updateDrillDown(Object chartId, Set dataPoints) { + developer.log("=== UPDATING DRILL DOWN ===", name: "rubintv.workspace.controller"); developer.log("Updating drill down from $chartId: ${dataPoints.length} points", name: "rubintv.workspace.controller"); + developer.log("Current drill down subscribers: ${_drillDownController.observers.keys}", + name: "rubintv.workspace.controller"); _drillDownController.updateSelection(chartId, dataPoints); + developer.log("Drill down update complete", name: "rubintv.workspace.controller"); } /// Dispose of the stream controllers. @@ -90,6 +98,10 @@ class ControlCenter { /// Reset the stream controllers. void reset() { developer.log("=== RESETTING CONTROL CENTER ===", name: "rubintv.workspace.controller"); + developer.log("Selection subscribers before reset: ${_selectionController.observers.keys}", + name: "rubintv.workspace.controller"); + developer.log("Drill down subscribers before reset: ${_drillDownController.observers.keys}", + name: "rubintv.workspace.controller"); _globalQueryController.add(null); _selectionController.reset(); _drillDownController.reset(); diff --git a/lib/workspace/data.dart b/lib/workspace/data.dart index c2a3f63..9e67cf9 100644 --- a/lib/workspace/data.dart +++ b/lib/workspace/data.dart @@ -173,10 +173,50 @@ class SchemaField { /// The [SchemaField] must be a child of a [TableSchema] /// that is already loaded by the [DataCenter]. static SchemaField fromJson(Map json) { + developer.log("=== SCHEMA FIELD FROM JSON ===", name: "rubintv.workspace.data"); + developer.log("JSON keys: ${json.keys}", name: "rubintv.workspace.data"); + developer.log("Field name: ${json['name']}", name: "rubintv.workspace.data"); + developer.log("Schema: ${json['schema']}", name: "rubintv.workspace.data"); + developer.log("Database: ${json['database']}", name: "rubintv.workspace.data"); + DataCenter dataCenter = DataCenter(); - TableSchema schema = - dataCenter.databases[json["database"]]!.tables.values.firstWhere((e) => e.name == json["schema"]); - return schema.fields[json["name"]]!; + developer.log("Available databases: ${dataCenter.databases.keys}", name: "rubintv.workspace.data"); + + if (!dataCenter.databases.containsKey(json["database"])) { + String errorMsg = + "Database '${json["database"]}' not found. Available databases: ${dataCenter.databases.keys.join(', ')}"; + developer.log(errorMsg, name: "rubintv.workspace.data"); + reportError("Workspace load error: $errorMsg"); + throw ArgumentError(errorMsg); + } + + DatabaseSchema database = dataCenter.databases[json["database"]]!; + developer.log("Available tables in database: ${database.tables.keys}", name: "rubintv.workspace.data"); + + TableSchema? schema; + try { + schema = database.tables.values.firstWhere((e) => e.name == json["schema"]); + developer.log("Found schema: ${schema.name}", name: "rubintv.workspace.data"); + } catch (e) { + String errorMsg = "Table '${json["schema"]}' not found in database '${json["database"]}'. " + "Available tables: ${database.tables.keys.join(', ')}. " + "This workspace may have been saved with a different instrument schema."; + developer.log(errorMsg, name: "rubintv.workspace.data"); + reportError("Workspace load error: $errorMsg"); + throw ArgumentError(errorMsg); + } + + if (!schema.fields.containsKey(json["name"])) { + String errorMsg = "Field '${json["name"]}' not found in table '${schema.name}'. " + "Available fields: ${schema.fields.keys.join(', ')}"; + developer.log(errorMsg, name: "rubintv.workspace.data"); + reportError("Workspace load error: $errorMsg"); + throw ArgumentError(errorMsg); + } + + SchemaField result = schema.fields[json["name"]]!; + developer.log("Schema field found successfully: ${result.name}", name: "rubintv.workspace.data"); + return result; } } @@ -199,6 +239,15 @@ class TableSchema { field.schema = this; } } + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + return other is TableSchema && other.name == name && other.database.name == database.name; + } + + @override + int get hashCode => Object.hash(name, database.name); } /// A data source. diff --git a/lib/workspace/state.dart b/lib/workspace/state.dart index 2ff8495..d42517d 100644 --- a/lib/workspace/state.dart +++ b/lib/workspace/state.dart @@ -429,6 +429,9 @@ class WorkspaceState extends WorkspaceStateBase { AppTheme theme, AppVersion version, ) { + developer.log("=== WORKSPACE FROM JSON ===", name: "rubintv.workspace.state"); + developer.log("JSON keys: ${json.keys}", name: "rubintv.workspace.state"); + AppVersion fileVersion = AppVersion.fromJson(json["version"]); if (fileVersion != version) { developer.log("File version $fileVersion does not match current version $version. ", @@ -436,11 +439,46 @@ class WorkspaceState extends WorkspaceStateBase { json = convertWorkspace(json, theme, version); } + developer.log("Processing windows: ${json['windows']?.keys}", name: "rubintv.workspace.state"); + Map windows = {}; + + if (json["windows"] != null) { + for (var entry in (json["windows"] as Map).entries) { + try { + developer.log("Processing window ${entry.key}", name: "rubintv.workspace.state"); + UniqueId windowId = UniqueId.fromString(entry.key); + WindowMetaData window = WindowMetaData.fromJson(entry.value, theme.chartTheme); + windows[windowId] = window; + developer.log("Window ${entry.key} processed successfully", name: "rubintv.workspace.state"); + } catch (e, stackTrace) { + developer.log("Error processing window ${entry.key}: $e", + name: "rubintv.workspace.state", error: e, stackTrace: stackTrace); + developer.log("Window JSON: ${entry.value}", name: "rubintv.workspace.state"); + + // Report a user-friendly error but continue loading other windows + if (e is ArgumentError && e.message.toString().contains("not found")) { + reportError("Skipped window ${entry.key}: ${e.message}"); + developer.log("Skipping window ${entry.key} due to schema mismatch", + name: "rubintv.workspace.state"); + continue; // Skip this window but continue loading others + } + rethrow; // Re-throw other errors + } + } + } + + // If we couldn't load any windows, provide a helpful error + if (json["windows"] != null && (json["windows"] as Map).isNotEmpty && windows.isEmpty) { + String errorMsg = "Could not load any windows from workspace. " + "The workspace may have been saved with a different instrument schema. " + "Please create a new workspace or update the saved workspace to match the current instrument."; + reportError(errorMsg); + developer.log(errorMsg, name: "rubintv.workspace.state"); + } + return WorkspaceState( version: AppVersion.fromJson(json["version"]), - windows: (json["windows"] as Map).map((key, value) { - return MapEntry(UniqueId.fromString(key), WindowMetaData.fromJson(value, theme.chartTheme)); - }), + windows: windows, instrument: json.containsKey("instrument") ? Instrument.fromJson(json["instrument"]) : null, globalQuery: json.containsKey("globalQuery") ? QueryExpression.fromJson(json["globalQuery"]) : null, dayObs: json.containsKey("dayObs") ? DateTime.parse(json["dayObs"]) : null, @@ -872,7 +910,7 @@ class WorkspaceBloc extends Bloc { } /// Load a workspace from a text string. - void _onLoadWorkspaceFromText(LoadWorkspaceFromTextEvent event, Emitter emit) { + void _onLoadWorkspaceFromText(LoadWorkspaceFromTextEvent event, Emitter emit) async { WorkspaceState state = this.state as WorkspaceState; developer.log("=== WORKSPACE LOAD START ===", name: "rubintv.workspace.load"); @@ -882,6 +920,18 @@ class WorkspaceBloc extends Bloc { try { Map json = jsonDecode(event.text); developer.log("JSON parsed successfully", name: "rubintv.workspace.load"); + developer.log("=== JSON SUMMARY ===", name: "rubintv.workspace.load"); + developer.log("JSON keys: ${json.keys}", name: "rubintv.workspace.load"); + developer.log("Version: ${json['version']}", name: "rubintv.workspace.load"); + developer.log("Instrument: ${json['instrument']?['name']}", name: "rubintv.workspace.load"); + developer.log("Windows count: ${json['windows']?.length}", name: "rubintv.workspace.load"); + if (json['windows'] != null) { + for (var entry in (json['windows'] as Map).entries) { + developer.log( + "Window ${entry.key}: type=${entry.value['state']?['windowType']}, series count=${entry.value['state']?['series']?.length}", + name: "rubintv.workspace.load"); + } + } Instrument newInstrument = Instrument.fromJson(json["instrument"]); developer.log("New instrument: ${newInstrument.name}, current: ${state.instrument?.name}", @@ -897,7 +947,7 @@ class WorkspaceBloc extends Bloc { } else { developer.log("Instrument matches - building workspace directly", name: "rubintv.workspace.load"); // Build new workspace from JSON - _applyWorkspaceJsonWithClear(emit, json, state); + await _applyWorkspaceJsonWithClear(emit, json, state); } } catch (e, stackTrace) { developer.log("Error loading workspace: $e", @@ -922,20 +972,18 @@ class WorkspaceBloc extends Bloc { ); // 3. Continue with the rest of the application logic - _applyWorkspaceJson(emit, newState); + await _applyWorkspaceJson(emit, newState); } /// Build a workspace from a JSON object. - void _applyWorkspaceJson(Emitter emit, WorkspaceState newState) async { + Future _applyWorkspaceJson(Emitter emit, WorkspaceState newState) async { developer.log("=== APPLYING JSON WORKSPACE ===", name: "rubintv.workspace.load"); developer.log("New state: windows=${newState.windows.length}, instrument=${newState.instrument?.name}", name: "rubintv.workspace.load"); - // First emit the new state so it's available everywhere - if (!emit.isDone) { - emit(newState); - developer.log("New workspace state emitted", name: "rubintv.workspace.load"); - } + // Emit the new state BEFORE syncing data so the UI updates + emit(newState); + developer.log("New workspace state emitted", name: "rubintv.workspace.load"); String? dayObs = getFormattedDate(newState.dayObs); developer.log("DayObs for sync: $dayObs", name: "rubintv.workspace.load"); @@ -992,7 +1040,20 @@ class WorkspaceBloc extends Bloc { developer.log("Windows to close: ${state.windows.length}, skipGlobalQueryReset: $skipGlobalQueryReset", name: "rubintv.workspace.clear"); - // Close all of the windows and cancel their subscriptions. + // First unsubscribe all windows from controllers BEFORE closing them + for (WindowMetaData window in state.windows.values) { + if (window.windowType.isChart) { + developer.log("Unsubscribing chart ${window.id} from controllers", name: "rubintv.workspace.clear"); + ControlCenter().selectionController.unsubscribe(window.id); + ControlCenter().drillDownController.unsubscribe(window.id); + } else if (window.windowType == WindowTypes.focalPlane) { + developer.log("Unsubscribing focal plane ${window.id} from controllers", + name: "rubintv.workspace.clear"); + ControlCenter().selectionController.unsubscribe(window.id); + } + } + + // Now close all of the windows and cancel their subscriptions for (WindowMetaData window in state.windows.values) { if (window.windowType.isChart || window.windowType == WindowTypes.focalPlane) { developer.log("Closing window ${window.id} of type ${window.windowType}", diff --git a/lib/workspace/viewer.dart b/lib/workspace/viewer.dart index ac68d78..0a03d6a 100644 --- a/lib/workspace/viewer.dart +++ b/lib/workspace/viewer.dart @@ -86,20 +86,34 @@ class WorkspaceViewerState extends State { /// The current state of the workspace. WorkspaceState? info; - UniqueKey get id => UniqueKey(); + + /// Use a stable ID for the workspace viewer's subscription + final Object _viewerId = Object(); @override void initState() { developer.log("=== INITIALIZING WORKSPACE VIEWER ===", name: "rubintv.workspace.viewer"); super.initState(); - ControlCenter().selectionController.subscribe(id, _onSelectionUpdate); + developer.log("Subscribing workspace viewer to selection controller", name: "rubintv.workspace.viewer"); + ControlCenter().selectionController.subscribe(_viewerId, _onSelectionUpdate); + developer.log("Workspace viewer subscription complete", name: "rubintv.workspace.viewer"); + } + + @override + void dispose() { + developer.log("=== DISPOSING WORKSPACE VIEWER ===", name: "rubintv.workspace.viewer"); + developer.log("Unsubscribing workspace viewer from selection controller", + name: "rubintv.workspace.viewer"); + ControlCenter().selectionController.unsubscribe(_viewerId); + super.dispose(); } /// Update the selection data points. /// This isn't used now, but can be used in the future if any plots cannot be /// matched to obs_date,seq_num data IDs. void _onSelectionUpdate(Object? origin, Set dataPoints) { + developer.log("=== WORKSPACE VIEWER SELECTION UPDATE ===", name: "rubintv.workspace.viewer"); developer.log("Workspace viewer received selection update from $origin: ${dataPoints.length} points", name: "rubintv.workspace.viewer"); } @@ -126,8 +140,11 @@ class WorkspaceViewerState extends State { } if (previous is WorkspaceState && current is WorkspaceState) { - developer.log("Windows: ${previous.windows.length} -> ${current.windows.length}", - name: "rubintv.workspace.viewer"); + // Always rebuild if instrument changed - this indicates a new workspace + if (previous.instrument != current.instrument) { + developer.log("Instrument changed - rebuilding", name: "rubintv.workspace.viewer"); + return true; + } // Rebuild if window count changed if (previous.windows.length != current.windows.length) { @@ -135,23 +152,10 @@ class WorkspaceViewerState extends State { return true; } - // Rebuild if window IDs are different (indicating different windows) - Set previousIds = previous.windows.keys.toSet(); - Set currentIds = current.windows.keys.toSet(); - if (!previousIds.containsAll(currentIds) || !currentIds.containsAll(previousIds)) { - developer.log("Window IDs changed - rebuilding", name: "rubintv.workspace.viewer"); - return true; - } - - // Rebuild if instrument changed - if (previous.instrument != current.instrument) { - developer.log("Instrument changed - rebuilding", name: "rubintv.workspace.viewer"); - return true; - } - // If we have the same windows but different references, rebuild - for (UniqueId id in currentIds) { - if (previous.windows[id] != current.windows[id]) { + // This handles updates to individual windows + for (UniqueId id in current.windows.keys) { + if (previous.windows.containsKey(id) && previous.windows[id] != current.windows[id]) { developer.log("Window $id content changed - rebuilding", name: "rubintv.workspace.viewer"); return true; } @@ -194,6 +198,7 @@ class WorkspaceViewerState extends State { developer.log("Building window ${window.id} of type ${window.windowType}", name: "rubintv.workspace.viewer"); children.add(Positioned( + key: ValueKey(window.id), // Add unique key to force recreation left: window.offset.dx, top: window.offset.dy, child: buildWindow(window, state), @@ -222,19 +227,21 @@ class WorkspaceViewerState extends State { name: "rubintv.workspace.viewer"); if (window.windowType == WindowTypes.cartesianScatter || window.windowType == WindowTypes.polarScatter) { - return ScatterPlotWidget(window: window, bloc: window.bloc as ChartBloc); + return ScatterPlotWidget(key: ValueKey(window.id), window: window, bloc: window.bloc as ChartBloc); } if (window.windowType == WindowTypes.histogram || window.windowType == WindowTypes.box) { - return BinnedChartWidget(window: window, bloc: window.bloc as ChartBloc); + return BinnedChartWidget(key: ValueKey(window.id), window: window, bloc: window.bloc as ChartBloc); } if (window.windowType == WindowTypes.detectorSelector) { return DetectorSelector( + key: ValueKey(window.id), window: window, workspace: workspace, ); } if (window.windowType == WindowTypes.focalPlane) { return FocalPlaneChartViewer( + key: ValueKey(window.id), window: window, workspace: workspace, bloc: window.bloc as FocalPlaneChartBloc, From bcb7d3e7fce87e56fcd70372b3138ae606cdc5cf Mon Sep 17 00:00:00 2001 From: ugyballoons Date: Mon, 24 Nov 2025 13:25:43 +0000 Subject: [PATCH 5/6] Add handler for data syncing in FocalPlaneChart --- lib/focal_plane/chart.dart | 177 ++++++++++++++++++++----------------- 1 file changed, 94 insertions(+), 83 deletions(-) diff --git a/lib/focal_plane/chart.dart b/lib/focal_plane/chart.dart index a62e4e2..aae9526 100644 --- a/lib/focal_plane/chart.dart +++ b/lib/focal_plane/chart.dart @@ -285,43 +285,11 @@ class FocalPlaneChartBloc extends WindowBloc { developer.log("=== CREATING FOCAL PLANE CHART BLOC ===", name: "rubintv.focal_plane.chart"); developer.log("Initial state: id=${state.id}", name: "rubintv.focal_plane.chart"); - _websocketSubscription = WebSocketManager().messages.listen((message) { - add(FocalPlaneReceiveMessageEvent(message)); - }); - - /// Subscribe to the selection controller to update the chart when points are selected. - /// We use a timer so that we don't load data until the selection has stopped - ControlCenter().selectionController.subscribe(state.id, (Object? origin, Set dataPoints) { - developer.log( - "Focal plane ${state.id} received selection update from $origin: ${dataPoints.length} points", - name: "rubintv.focal_plane.chart"); - if (origin == state.id) { - return; - } - _selectionTimer?.cancel(); - _selectionTimer = Timer(const Duration(milliseconds: 500), () { - _updateSeries(dataPoints); - }); - }); - - /// Subscribe to the global query stream to update the chart when the query changes. - _globalQuerySubscription = ControlCenter().globalQueryStream.listen((GlobalQuery? query) { - developer.log("Focal plane ${state.id} received global query update: dayObs=${query?.dayObs}", - name: "rubintv.focal_plane.chart"); - Set? selected = - ControlCenter().selectionController.selectedDataPoints.map((e) => e as DataId).toSet(); - if (selected.isEmpty) { - selected = ControlCenter().drillDownController.selectedDataPoints.map((e) => e as DataId).toSet(); - } - if (selected.isEmpty) { - selected = null; - } - _fetchSeriesData(series: state.series, query: query?.query, dayObs: query?.dayObs, selected: selected); - }); - + // Subscribe to selection updates + ControlCenter().selectionController.subscribe(state.id, _onSelectionUpdate); developer.log("Focal plane chart bloc created and subscribed", name: "rubintv.focal_plane.chart"); - /// Initialize the chart. + /// Initialize the focal plane chart on((event, emit) { ColorbarController colorbarController = event.colorbarController; emit(FocalPlaneChartState( @@ -339,7 +307,7 @@ class FocalPlaneChartBloc extends WindowBloc { )); }); - /// Process data received from the websocket. + /// A message has been received from the websocket on((event, emit) { List? splitId = event.message["requestId"]?.split(","); if (splitId == null || splitId.length != 2) { @@ -355,7 +323,7 @@ class FocalPlaneChartBloc extends WindowBloc { } }); - /// Update the Series and fetch the data. + /// Update the column being displayed on((event, emit) { final String tableName = event.field.schema.name; SchemaField detectorField; @@ -390,19 +358,19 @@ class FocalPlaneChartBloc extends WindowBloc { emit(state.copyWith(series: newSeries)); }); - /// Update the data index. + /// Update the data index on((event, emit) { emit(state.copyWith(dataIndex: event.index)); }); - /// Increase the data index. + /// Increase the data index on((event, emit) { if (state.dataIndex < state.dataIds.length - 1) { emit(state.copyWith(dataIndex: state.dataIndex + 1)); } }); - /// Update the playback speed. + /// Update the playback speed on((event, emit) { emit(state.copyWith(playbackSpeed: event.speed)); if (state.isPlaying) { @@ -410,7 +378,7 @@ class FocalPlaneChartBloc extends WindowBloc { } }); - /// Start the playback timer, which increases the [dataIndex] periodically. + /// Start the timer on((event, emit) { _createTimer(); int dataIndex = state.dataIndex; @@ -420,14 +388,14 @@ class FocalPlaneChartBloc extends WindowBloc { emit(state.copyWith(isPlaying: true, dataIndex: dataIndex)); }); - /// Stop the playback timer. + /// Stop the timer on((event, emit) { _playTimer?.cancel(); _playTimer = null; emit(state.copyWith(isPlaying: false)); }); - /// Update the data index when a tick is received. + /// A tick has occurred on((event, emit) { if (state.dataIndex < state.dataIds.length - 1) { emit(state.copyWith(dataIndex: state.dataIndex + 1)); @@ -439,16 +407,55 @@ class FocalPlaneChartBloc extends WindowBloc { } }); - /// Toggle the loop playback. + /// Toggle loop on((event, emit) { emit(state.copyWith(loopPlayback: !state.loopPlayback)); }); - /// Reload all of the data from the server. + /// Subscribe to global query updates + _globalQuerySubscription = ControlCenter().globalQueryStream.listen((GlobalQuery? query) { + Set? selected = + ControlCenter().selectionController.selectedDataPoints.map((e) => e as DataId).toSet(); + if (selected.isEmpty) { + selected = ControlCenter().drillDownController.selectedDataPoints.map((e) => e as DataId).toSet(); + } + if (selected.isEmpty) { + selected = null; + } + _fetchSeriesData(series: state.series, query: query?.query, dayObs: query?.dayObs, selected: selected); + }); + + /// Subscribe to websocket messages + _websocketSubscription = WebSocketManager().messages.listen((message) { + add(FocalPlaneReceiveMessageEvent(message)); + }); + + /// Handle synchronization events (used when loading workspaces) on((event, emit) { - developer.log("=== SYNCHING FOCAL PLANE DATA ===", name: "rubintv.focal_plane.chart"); - developer.log("Focal plane ${state.id}: dayObs=${event.dayObs}", name: "rubintv.focal_plane.chart"); - _updateSeries(); + // Fetch data for the current series with the provided dayObs + Set? selected = + ControlCenter().selectionController.selectedDataPoints.map((e) => e as DataId).toSet(); + if (selected.isEmpty) { + selected = ControlCenter().drillDownController.selectedDataPoints.map((e) => e as DataId).toSet(); + } + if (selected.isEmpty) { + selected = null; + } + + _fetchSeriesData( + series: state.series, + query: event.globalQuery, + dayObs: event.dayObs, + selected: selected, + ); + }); + } + + /// Callback when selection is updated + void _onSelectionUpdate(Object? origin, Set dataPoints) { + _selectionTimer?.cancel(); + _selectionTimer = Timer(const Duration(milliseconds: 500), () { + _updateSeries(dataPoints); }); } @@ -468,7 +475,6 @@ class FocalPlaneChartBloc extends WindowBloc { int columns = event.message["content"]["data"].length; developer.log("received $columns columns and $rows rows for ${event.message["requestId"]}", name: "rubintv.workspace"); - if (rows > 0) { // Extract the data from the message Map> allData = Map>.from( @@ -557,13 +563,15 @@ class FocalPlaneChartBloc extends WindowBloc { /// Close the bloc. @override Future close() async { - developer.log("=== CLOSING FOCAL PLANE CHART BLOC ===", name: "rubintv.focal_plane.chart"); - developer.log("Focal plane ${state.id} being closed", name: "rubintv.focal_plane.chart"); + _playTimer?.cancel(); + _selectionTimer?.cancel(); await _websocketSubscription.cancel(); await _globalQuerySubscription.cancel(); - _playTimer?.cancel(); - _selectionTimer?.cancel(); + developer.log("Subscriptions cancelled", name: "rubintv.focal_plane.chart"); + + ControlCenter().selectionController.unsubscribe(state.id); + developer.log("Unsubscribed from selection controller", name: "rubintv.focal_plane.chart"); developer.log("Focal plane chart bloc closed", name: "rubintv.focal_plane.chart"); return super.close(); @@ -790,13 +798,15 @@ class FocalPlaneChartViewerState extends State { width: 50, child: IconButton( icon: state.isPlaying ? const Icon(Icons.pause) : const Icon(Icons.play_arrow), - onPressed: () { - if (!state.isPlaying) { - context.read().add(FocalPlaneStartTimerEvent()); - } else { - context.read().add(FocalPlaneStopTimerEvent()); - } - }, + onPressed: state.dataIds.length > 1 + ? () { + if (!state.isPlaying) { + context.read().add(FocalPlaneStartTimerEvent()); + } else { + context.read().add(FocalPlaneStopTimerEvent()); + } + } + : null, ), )), const SizedBox(width: 10), @@ -804,46 +814,47 @@ class FocalPlaneChartViewerState extends State { message: "Previous data ID", child: Material( color: Colors.grey[300], - shape: const CircleBorder(), child: IconButton( icon: const Icon(Icons.remove), - onPressed: () { - if (state.dataIndex > 0) { - context - .read() - .add(FocalPlaneUpdateDataIndexEvent(state.dataIndex - 1)); - } - }, + onPressed: (state.dataIds.length > 1 && state.dataIndex > 0) + ? () { + context + .read() + .add(FocalPlaneUpdateDataIndexEvent(state.dataIndex - 1)); + } + : null, ), )), Expanded( child: Slider( value: state.dataIndex.toDouble(), min: 0, - max: state.dataIds.isNotEmpty ? state.dataIds.length.toDouble() - 1 : 2, - divisions: state.dataIds.isNotEmpty ? state.dataIds.length - 1 : 2, + max: state.dataIds.isNotEmpty ? state.dataIds.length.toDouble() - 1 : 1, + divisions: state.dataIds.length > 1 ? state.dataIds.length - 1 : null, label: state.dataIndex.round().toString(), - onChanged: (value) { - context - .read() - .add(FocalPlaneUpdateDataIndexEvent(value.round().toInt())); - }, + onChanged: state.dataIds.length > 1 + ? (value) { + context + .read() + .add(FocalPlaneUpdateDataIndexEvent(value.round().toInt())); + } + : null, ), ), Tooltip( message: "Next data ID", child: Material( color: Colors.grey[300], - shape: const CircleBorder(), child: IconButton( icon: const Icon(Icons.add), - onPressed: () { - if (state.dataIndex < state.dataIds.length - 1) { - context - .read() - .add(FocalPlaneUpdateDataIndexEvent(state.dataIndex + 1)); - } - }, + onPressed: + (state.dataIds.length > 1 && state.dataIndex < state.dataIds.length - 1) + ? () { + context + .read() + .add(FocalPlaneUpdateDataIndexEvent(state.dataIndex + 1)); + } + : null, ), )), ]), From 0a855334b2554f0b28e768f90e4cfed702460bc9 Mon Sep 17 00:00:00 2001 From: ugyballoons Date: Thu, 27 Nov 2025 12:22:39 +0000 Subject: [PATCH 6/6] Remove extraneous logging and comments --- lib/chart/base.dart | 38 ---------------------------------- lib/chart/series.dart | 26 +++++++++++++++++------ lib/focal_plane/chart.dart | 3 --- lib/utils/browser_logger.dart | 2 +- lib/workspace/controller.dart | 24 --------------------- lib/workspace/data.dart | 12 ----------- lib/workspace/state.dart | 39 ----------------------------------- lib/workspace/viewer.dart | 16 -------------- lib/workspace/window.dart | 13 ------------ 9 files changed, 21 insertions(+), 152 deletions(-) diff --git a/lib/chart/base.dart b/lib/chart/base.dart index 13739ed..ef164af 100644 --- a/lib/chart/base.dart +++ b/lib/chart/base.dart @@ -345,28 +345,9 @@ class ChartState extends WindowState { /// Create a [ChartState] from a JSON object. @override factory ChartState.fromJson(Map json) { - developer.log("=== CHART STATE FROM JSON ===", name: "rubintv.chart.base"); - developer.log("JSON keys: ${json.keys}", name: "rubintv.chart.base"); - developer.log("ID: ${json['id']}", name: "rubintv.chart.base"); - developer.log("Window type: ${json['windowType']}", name: "rubintv.chart.base"); - developer.log("Series count: ${json['series']?.length}", name: "rubintv.chart.base"); - developer.log("Axis info count: ${json['axisInfo']?.length}", name: "rubintv.chart.base"); - - if (json['series'] != null) { - for (int i = 0; i < (json['series'] as List).length; i++) { - var seriesJson = json['series'][i]; - developer.log("Series $i: ${seriesJson.keys}", name: "rubintv.chart.base"); - if (seriesJson is Map && seriesJson.containsKey('fields')) { - developer.log("Series $i fields: ${seriesJson['fields']}", name: "rubintv.chart.base"); - } - } - } - try { Map series = Map.fromEntries((json["series"] as List).map((e) { - developer.log("Processing series JSON: ${e.keys}", name: "rubintv.chart.base"); SeriesInfo seriesInfo = SeriesInfo.fromJson(e); - developer.log("Series info created: ${seriesInfo.id}", name: "rubintv.chart.base"); return MapEntry(seriesInfo.id, seriesInfo); })); @@ -428,9 +409,6 @@ class ChartBloc extends WindowBloc { } ChartBloc(super.initialState) { - developer.log("=== CREATING CHART BLOC ===", name: "rubintv.chart.base"); - developer.log("Initial state: id=${state.id}, type=${state.windowType}", name: "rubintv.chart.base"); - /// Listen for messages from the websocket. _subscription = WebSocketManager().messages.listen((message) { add(ChartReceiveMessageEvent(message)); @@ -439,11 +417,7 @@ class ChartBloc extends WindowBloc { /// Subscribe to selection controller to update when points are selected. developer.log("Subscribing chart ${state.id} to selection controller", name: "rubintv.chart.base"); ControlCenter().selectionController.subscribe(state.id, (Object? origin, Set dataPoints) { - developer.log("=== CHART SELECTION UPDATE ===", name: "rubintv.chart.base"); - developer.log("Chart ${state.id} received selection update from $origin: ${dataPoints.length} points", - name: "rubintv.chart.base"); if (origin == state.id) { - developer.log("Ignoring selection update from self", name: "rubintv.chart.base"); return; } developer.log("Processing selection update for chart ${state.id}", name: "rubintv.chart.base"); @@ -453,11 +427,7 @@ class ChartBloc extends WindowBloc { /// Subscribe to drill down controller. developer.log("Subscribing chart ${state.id} to drill down controller", name: "rubintv.chart.base"); ControlCenter().drillDownController.subscribe(state.id, (Object? origin, Set dataPoints) { - developer.log("=== CHART DRILL DOWN UPDATE ===", name: "rubintv.chart.base"); - developer.log("Chart ${state.id} received drill down update from $origin: ${dataPoints.length} points", - name: "rubintv.chart.base"); if (origin == state.id) { - developer.log("Ignoring drill down update from self", name: "rubintv.chart.base"); return; } developer.log("Processing drill down update for chart ${state.id}", name: "rubintv.chart.base"); @@ -695,10 +665,6 @@ class ChartBloc extends WindowBloc { /// Reload all of the data from the server. on((event, emit) { - developer.log("=== SYNCHING DATA ===", name: "rubintv.chart.base"); - developer.log("Chart ${state.id}: dayObs=${event.dayObs}, skipGlobalUpdate=${event.skipGlobalUpdate}", - name: "rubintv.chart.base"); - // When loading from a file, we don't want to trigger the global query update unnecessarily if (!event.skipGlobalUpdate && event.globalQuery != null) { developer.log("Updating global query in ControlCenter", name: "rubintv.chart.base"); @@ -1077,14 +1043,10 @@ class ChartBloc extends WindowBloc { @override Future close() async { - developer.log("=== CLOSING CHART BLOC ===", name: "rubintv.chart.base"); - developer.log("Chart ${state.id} being closed", name: "rubintv.chart.base"); - developer.log("Unsubscribing chart ${state.id} from selection controllers", name: "rubintv.chart.base"); ControlCenter().selectionController.unsubscribe(state.id); ControlCenter().drillDownController.unsubscribe(state.id); await _subscription.cancel(); await _globalQuerySubscription.cancel(); - developer.log("Chart bloc closed", name: "rubintv.chart.base"); return super.close(); } } diff --git a/lib/chart/series.dart b/lib/chart/series.dart index f7d44fe..ffc90d3 100644 --- a/lib/chart/series.dart +++ b/lib/chart/series.dart @@ -1,3 +1,23 @@ +/// This file is part of the rubintv_visualization package. +/// +/// Developed for the LSST Data Management System. +/// This product includes software developed by the LSST Project +/// (https://www.lsst.org). +/// See the COPYRIGHT file at the top-level directory of this distribution +/// for details of code ownership. +/// +/// This program is free software: you can redistribute it and/or modify +/// it under the terms of the GNU General Public License as published by +/// the Free Software Foundation, either version 3 of the License, or +/// (at your option) any later version. +/// +/// This program is distributed in the hope that it will be useful, +/// but WITHOUT ANY WARRANTY; without even the implied warranty of +/// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +/// GNU General Public License for more details. +/// +/// You should have received a copy of the GNU General Public License +/// along with this program. If not, see . import 'dart:developer' as developer; import 'package:flutter/foundation.dart'; @@ -150,12 +170,6 @@ class SeriesInfo { /// Create a [SeriesInfo] from a JSON object. static SeriesInfo fromJson(Map json) { - developer.log("=== SERIES INFO FROM JSON ===", name: "rubintv.chart.series"); - developer.log("JSON keys: ${json.keys}", name: "rubintv.chart.series"); - developer.log("ID: ${json['id']}", name: "rubintv.chart.series"); - developer.log("Name: ${json['name']}", name: "rubintv.chart.series"); - developer.log("Fields: ${json['fields']}", name: "rubintv.chart.series"); - try { Map fields = {}; diff --git a/lib/focal_plane/chart.dart b/lib/focal_plane/chart.dart index aae9526..5100c6c 100644 --- a/lib/focal_plane/chart.dart +++ b/lib/focal_plane/chart.dart @@ -282,9 +282,6 @@ class FocalPlaneChartBloc extends WindowBloc { } FocalPlaneChartBloc(super.initialState) { - developer.log("=== CREATING FOCAL PLANE CHART BLOC ===", name: "rubintv.focal_plane.chart"); - developer.log("Initial state: id=${state.id}", name: "rubintv.focal_plane.chart"); - // Subscribe to selection updates ControlCenter().selectionController.subscribe(state.id, _onSelectionUpdate); developer.log("Focal plane chart bloc created and subscribed", name: "rubintv.focal_plane.chart"); diff --git a/lib/utils/browser_logger.dart b/lib/utils/browser_logger.dart index 869e94e..f222fb9 100644 --- a/lib/utils/browser_logger.dart +++ b/lib/utils/browser_logger.dart @@ -84,7 +84,7 @@ String _getLevelString(int level) { // Helper to expose logs through the console void printLogsToConsole() { try { - final key = 'rubintv_visualization_log'; + const key = 'rubintv_visualization_log'; final logs = web.window.localStorage[key] ?? 'No logs found'; developer.log('=== BEGIN LOGS ==='); developer.log(logs); diff --git a/lib/workspace/controller.dart b/lib/workspace/controller.dart index 3bf88b7..2a6ac82 100644 --- a/lib/workspace/controller.dart +++ b/lib/workspace/controller.dart @@ -20,7 +20,6 @@ /// along with this program. If not, see . import 'dart:async'; -import 'dart:developer' as developer; import 'package:rubin_chart/rubin_chart.dart'; import 'package:rubintv_visualization/workspace/state.dart'; @@ -58,38 +57,21 @@ class ControlCenter { /// Update the global query. void updateGlobalQuery(GlobalQuery? query) { - developer.log("=== UPDATING GLOBAL QUERY ===", name: "rubintv.workspace.controller"); - developer.log( - "New query: ${query?.query?.toJson()}, dayObs: ${query?.dayObs}, instrument: ${query?.instrument?.name}", - name: "rubintv.workspace.controller"); _globalQueryController.add(query); } /// Update the selection data points. void updateSelection(Object chartId, Set dataPoints) { - developer.log("=== UPDATING SELECTION ===", name: "rubintv.workspace.controller"); - developer.log("Updating selection from $chartId: ${dataPoints.length} points", - name: "rubintv.workspace.controller"); - developer.log("Current subscribers: ${_selectionController.observers.keys}", - name: "rubintv.workspace.controller"); _selectionController.updateSelection(chartId, dataPoints); - developer.log("Selection update complete", name: "rubintv.workspace.controller"); } /// Update the drill down data points. void updateDrillDown(Object chartId, Set dataPoints) { - developer.log("=== UPDATING DRILL DOWN ===", name: "rubintv.workspace.controller"); - developer.log("Updating drill down from $chartId: ${dataPoints.length} points", - name: "rubintv.workspace.controller"); - developer.log("Current drill down subscribers: ${_drillDownController.observers.keys}", - name: "rubintv.workspace.controller"); _drillDownController.updateSelection(chartId, dataPoints); - developer.log("Drill down update complete", name: "rubintv.workspace.controller"); } /// Dispose of the stream controllers. void dispose() { - developer.log("Disposing ControlCenter", name: "rubintv.workspace.controller"); _globalQueryController.close(); _selectionController.dispose(); _drillDownController.dispose(); @@ -97,14 +79,8 @@ class ControlCenter { /// Reset the stream controllers. void reset() { - developer.log("=== RESETTING CONTROL CENTER ===", name: "rubintv.workspace.controller"); - developer.log("Selection subscribers before reset: ${_selectionController.observers.keys}", - name: "rubintv.workspace.controller"); - developer.log("Drill down subscribers before reset: ${_drillDownController.observers.keys}", - name: "rubintv.workspace.controller"); _globalQueryController.add(null); _selectionController.reset(); _drillDownController.reset(); - developer.log("ControlCenter reset complete", name: "rubintv.workspace.controller"); } } diff --git a/lib/workspace/data.dart b/lib/workspace/data.dart index 9e67cf9..1b9768d 100644 --- a/lib/workspace/data.dart +++ b/lib/workspace/data.dart @@ -173,12 +173,6 @@ class SchemaField { /// The [SchemaField] must be a child of a [TableSchema] /// that is already loaded by the [DataCenter]. static SchemaField fromJson(Map json) { - developer.log("=== SCHEMA FIELD FROM JSON ===", name: "rubintv.workspace.data"); - developer.log("JSON keys: ${json.keys}", name: "rubintv.workspace.data"); - developer.log("Field name: ${json['name']}", name: "rubintv.workspace.data"); - developer.log("Schema: ${json['schema']}", name: "rubintv.workspace.data"); - developer.log("Database: ${json['database']}", name: "rubintv.workspace.data"); - DataCenter dataCenter = DataCenter(); developer.log("Available databases: ${dataCenter.databases.keys}", name: "rubintv.workspace.data"); @@ -431,10 +425,6 @@ class DataCenter { required List plotColumns, required Map> data, }) { - developer.log("=== UPDATING SERIES DATA ===", name: "rubintv_visualization.workspace.data"); - developer.log("Series: ${series.id}, DataSource: $dataSourceName, Columns: ${plotColumns.length}", - name: "rubintv_visualization.workspace.data"); - // Extensive validation if (data.isEmpty) { developer.log("No data found for series ${series.id}", name: "rubintv_visualization.workspace.data"); @@ -587,8 +577,6 @@ class DataCenter { } void clearSeriesData() { - developer.log("=== CLEARING ALL SERIES DATA ===", name: "rubintv_visualization.workspace.data"); - developer.log("Clearing ${_seriesData.length} series", name: "rubintv_visualization.workspace.data"); _seriesData.clear(); } diff --git a/lib/workspace/state.dart b/lib/workspace/state.dart index d42517d..eab5d3a 100644 --- a/lib/workspace/state.dart +++ b/lib/workspace/state.dart @@ -429,9 +429,6 @@ class WorkspaceState extends WorkspaceStateBase { AppTheme theme, AppVersion version, ) { - developer.log("=== WORKSPACE FROM JSON ===", name: "rubintv.workspace.state"); - developer.log("JSON keys: ${json.keys}", name: "rubintv.workspace.state"); - AppVersion fileVersion = AppVersion.fromJson(json["version"]); if (fileVersion != version) { developer.log("File version $fileVersion does not match current version $version. ", @@ -913,25 +910,8 @@ class WorkspaceBloc extends Bloc { void _onLoadWorkspaceFromText(LoadWorkspaceFromTextEvent event, Emitter emit) async { WorkspaceState state = this.state as WorkspaceState; - developer.log("=== WORKSPACE LOAD START ===", name: "rubintv.workspace.load"); - developer.log("Current state: windows=${state.windows.length}, instrument=${state.instrument?.name}", - name: "rubintv.workspace.load"); - try { Map json = jsonDecode(event.text); - developer.log("JSON parsed successfully", name: "rubintv.workspace.load"); - developer.log("=== JSON SUMMARY ===", name: "rubintv.workspace.load"); - developer.log("JSON keys: ${json.keys}", name: "rubintv.workspace.load"); - developer.log("Version: ${json['version']}", name: "rubintv.workspace.load"); - developer.log("Instrument: ${json['instrument']?['name']}", name: "rubintv.workspace.load"); - developer.log("Windows count: ${json['windows']?.length}", name: "rubintv.workspace.load"); - if (json['windows'] != null) { - for (var entry in (json['windows'] as Map).entries) { - developer.log( - "Window ${entry.key}: type=${entry.value['state']?['windowType']}, series count=${entry.value['state']?['series']?.length}", - name: "rubintv.workspace.load"); - } - } Instrument newInstrument = Instrument.fromJson(json["instrument"]); developer.log("New instrument: ${newInstrument.name}, current: ${state.instrument?.name}", @@ -945,7 +925,6 @@ class WorkspaceBloc extends Bloc { )); WebSocketManager().sendMessage(LoadInstrumentAction(instrument: newInstrument.name).toJson()); } else { - developer.log("Instrument matches - building workspace directly", name: "rubintv.workspace.load"); // Build new workspace from JSON await _applyWorkspaceJsonWithClear(emit, json, state); } @@ -958,29 +937,19 @@ class WorkspaceBloc extends Bloc { Future _applyWorkspaceJsonWithClear( Emitter emit, Map json, WorkspaceState currentState) async { - developer.log("=== CLEARING BEFORE JSON LOAD ===", name: "rubintv.workspace.load"); - - // 1. Clear the old workspace FIRST await _clearWorkspace(currentState, skipGlobalQueryReset: true); - developer.log("Workspace cleared, now building from JSON", name: "rubintv.workspace.load"); - // 2. Build new workspace from JSON AFTER clearing WorkspaceState newState = WorkspaceState.fromJson( json, currentState.theme, currentState.version, ); - // 3. Continue with the rest of the application logic await _applyWorkspaceJson(emit, newState); } /// Build a workspace from a JSON object. Future _applyWorkspaceJson(Emitter emit, WorkspaceState newState) async { - developer.log("=== APPLYING JSON WORKSPACE ===", name: "rubintv.workspace.load"); - developer.log("New state: windows=${newState.windows.length}, instrument=${newState.instrument?.name}", - name: "rubintv.workspace.load"); - // Emit the new state BEFORE syncing data so the UI updates emit(newState); developer.log("New workspace state emitted", name: "rubintv.workspace.load"); @@ -1023,23 +992,16 @@ class WorkspaceBloc extends Bloc { )); if (!globalQueryUpdated) { - developer.log("Updating global query (first time - focal plane)", name: "rubintv.workspace.load"); // Update global query only once, after the first window is processed ControlCenter().updateGlobalQuery(newState.getGlobalQuery()); globalQueryUpdated = true; } } } - - developer.log("=== WORKSPACE LOAD COMPLETE ===", name: "rubintv.workspace.load"); } /// Clear the workspace and the DataCenter. Future _clearWorkspace(WorkspaceState state, {bool skipGlobalQueryReset = false}) async { - developer.log("=== CLEARING WORKSPACE ===", name: "rubintv.workspace.clear"); - developer.log("Windows to close: ${state.windows.length}, skipGlobalQueryReset: $skipGlobalQueryReset", - name: "rubintv.workspace.clear"); - // First unsubscribe all windows from controllers BEFORE closing them for (WindowMetaData window in state.windows.values) { if (window.windowType.isChart) { @@ -1075,7 +1037,6 @@ class WorkspaceBloc extends Bloc { // Clear the DataCenter Series Data. DataCenter().clearSeriesData(); developer.log("DataCenter series data cleared", name: "rubintv.workspace.clear"); - developer.log("=== WORKSPACE CLEAR COMPLETE ===", name: "rubintv.workspace.clear"); } /// Cancel the subscription to the websocket. diff --git a/lib/workspace/viewer.dart b/lib/workspace/viewer.dart index 0a03d6a..7bfe3d5 100644 --- a/lib/workspace/viewer.dart +++ b/lib/workspace/viewer.dart @@ -92,19 +92,12 @@ class WorkspaceViewerState extends State { @override void initState() { - developer.log("=== INITIALIZING WORKSPACE VIEWER ===", name: "rubintv.workspace.viewer"); super.initState(); - - developer.log("Subscribing workspace viewer to selection controller", name: "rubintv.workspace.viewer"); ControlCenter().selectionController.subscribe(_viewerId, _onSelectionUpdate); - developer.log("Workspace viewer subscription complete", name: "rubintv.workspace.viewer"); } @override void dispose() { - developer.log("=== DISPOSING WORKSPACE VIEWER ===", name: "rubintv.workspace.viewer"); - developer.log("Unsubscribing workspace viewer from selection controller", - name: "rubintv.workspace.viewer"); ControlCenter().selectionController.unsubscribe(_viewerId); super.dispose(); } @@ -124,9 +117,6 @@ class WorkspaceViewerState extends State { create: (context) => WorkspaceBloc()..add(InitializeWorkspaceEvent(theme, version)), child: BlocBuilder( buildWhen: (previous, current) { - developer.log("BlocBuilder buildWhen: ${previous.runtimeType} -> ${current.runtimeType}", - name: "rubintv.workspace.viewer"); - // Always rebuild if state types are different if (previous.runtimeType != current.runtimeType) { developer.log("State type changed - rebuilding", name: "rubintv.workspace.viewer"); @@ -156,13 +146,10 @@ class WorkspaceViewerState extends State { // This handles updates to individual windows for (UniqueId id in current.windows.keys) { if (previous.windows.containsKey(id) && previous.windows[id] != current.windows[id]) { - developer.log("Window $id content changed - rebuilding", name: "rubintv.workspace.viewer"); return true; } } - developer.log("No significant changes detected - not rebuilding", - name: "rubintv.workspace.viewer"); return false; } @@ -171,9 +158,6 @@ class WorkspaceViewerState extends State { return true; }, builder: (context, state) { - developer.log("=== BUILDING WORKSPACE ===", name: "rubintv.workspace.viewer"); - developer.log("State type: ${state.runtimeType}", name: "rubintv.workspace.viewer"); - if (state is WorkspaceStateInitial) { developer.log("Workspace state is initial - showing progress indicator", name: "rubintv.workspace.viewer"); diff --git a/lib/workspace/window.dart b/lib/workspace/window.dart index 08e34c7..3ac6e30 100644 --- a/lib/workspace/window.dart +++ b/lib/workspace/window.dart @@ -165,31 +165,18 @@ class WindowMetaData { Offset offset = Offset(json["offset"]["dx"], json["offset"]["dy"]); Size size = Size(json["size"]["width"], json["size"]["height"]); String? title = json["title"] == "" ? null : json["title"]; - - developer.log("=== DESERIALIZING WINDOW ===", name: "rubintv_visualization.workspace.window"); - developer.log("Window ID: ${state.id}, Type: ${state.windowType}", - name: "rubintv_visualization.workspace.window"); - late WindowBloc bloc; if (state.windowType == WindowTypes.detectorSelector) { - developer.log("Creating detector selector bloc from JSON", - name: "rubintv_visualization.workspace.window"); bloc = WindowBloc(state); } else if (state.windowType.isBinned) { - developer.log("Creating binned bloc from JSON", name: "rubintv_visualization.workspace.window"); bloc = ChartBloc(state as BinnedState); } else if (state.windowType.isScatter) { - developer.log("Creating scatter bloc from JSON", name: "rubintv_visualization.workspace.window"); bloc = ChartBloc(state as ChartState); } else if (state.windowType == WindowTypes.focalPlane) { - developer.log("Creating focal plane bloc from JSON", name: "rubintv_visualization.workspace.window"); bloc = FocalPlaneChartBloc(state as FocalPlaneChartState); } else { throw ArgumentError("Unrecognized window type ${state.windowType}"); } - - developer.log("Window bloc created from JSON successfully", - name: "rubintv_visualization.workspace.window"); return WindowMetaData(offset: offset, size: size, title: title, bloc: bloc); } }