diff --git a/vcell-cli/src/main/java/org/vcell/cli/run/plotting/Results2DLinePlot.java b/vcell-cli/src/main/java/org/vcell/cli/run/plotting/Results2DLinePlot.java index b04a69b837..08f009bf96 100644 --- a/vcell-cli/src/main/java/org/vcell/cli/run/plotting/Results2DLinePlot.java +++ b/vcell-cli/src/main/java/org/vcell/cli/run/plotting/Results2DLinePlot.java @@ -5,9 +5,15 @@ import com.lowagie.text.DocumentException; import org.jfree.chart.ChartFactory; +import org.jfree.chart.ChartPanel; import org.jfree.chart.JFreeChart; +import org.jfree.chart.block.BlockBorder; import org.jfree.chart.labels.StandardXYItemLabelGenerator; +import org.jfree.chart.plot.PlotOrientation; import org.jfree.chart.plot.XYPlot; +import org.jfree.chart.renderer.xy.XYDifferenceRenderer; +import org.jfree.chart.renderer.xy.XYLineAndShapeRenderer; +import org.jfree.chart.ui.RectangleEdge; import org.jfree.data.xy.XYDataset; import org.jfree.data.xy.XYSeries; import org.jfree.data.xy.XYSeriesCollection; @@ -16,7 +22,9 @@ import org.apache.logging.log4j.Logger; import javax.imageio.ImageIO; +import javax.swing.*; import java.awt.*; +import java.awt.geom.Path2D; import java.awt.image.BufferedImage; import java.awt.print.PageFormat; import java.awt.print.Paper; @@ -319,4 +327,246 @@ private static PageFormat generateAlternatePageFormat(){ pageFormat.setOrientation(PageFormat.LANDSCAPE); return pageFormat; } + + + public static void main(String[] args) { + SwingUtilities.invokeLater(() -> { + + Random rand = new Random(); + + int n = 50; + double xMin = 0.0; + double xMax = 7.0; + double dx = (xMax - xMin) / (n - 1); + + double xLower = 0.0; // these are the limits for the x values of the axis + double xUpper = 7.5; + + Color sinColor = new Color(31, 119, 180); // blue + Color tanColor = new Color(255, 127, 14); // orange + + // --- SIN series --- + XYSeries sinMin = new XYSeries("sin-min"); + XYSeries sinMax = new XYSeries("sin-max"); + XYSeries sinMain = new XYSeries("sin"); + XYSeries sinStd = new XYSeries("sin-std"); + + for (int i = 0; i < n; i++) { + double x = xMin + i * dx; + double y = Math.sin(x); + + double delta = 0.2 + rand.nextDouble() * 0.2; + double yMin = y - delta; + double yMax = y + delta; + + double std = 0.08 + rand.nextDouble() * 0.08; + + sinMin.add(x, yMin); + sinMax.add(x, yMax); + sinMain.add(x, y); + sinStd.add(x, y + std); + } + + // --- TAN series --- + XYSeries tanMin = new XYSeries("tan-min"); + XYSeries tanMax = new XYSeries("tan-max"); + XYSeries tanMain = new XYSeries("tan"); + XYSeries tanStd = new XYSeries("tan-std"); + + for (int i = 0; i < n; i++) { + double x = xMin + i * dx; + double y = Math.tan(x); + + if (y > 3) y = 3; + if (y < -3) y = -3; + + double delta = 0.3 + rand.nextDouble() * 0.3; + double yMin = y - delta; + double yMax = y + delta; + + double std = 0.2 + rand.nextDouble() * 0.2; + + tanMin.add(x, yMin); + tanMax.add(x, yMax); + tanMain.add(x, y); + tanStd.add(x, y + std); + } + + double globalMin = Double.POSITIVE_INFINITY; + double globalMax = Double.NEGATIVE_INFINITY; + for (int i = 0; i < n; i++) { + globalMin = Math.min(globalMin, sinMin.getY(i).doubleValue()); + globalMin = Math.min(globalMin, tanMin.getY(i).doubleValue()); + globalMax = Math.max(globalMax, sinMax.getY(i).doubleValue()); + globalMax = Math.max(globalMax, tanMax.getY(i).doubleValue()); + } + // Add padding + double pad = 0.1 * (globalMax - globalMin); + globalMin -= pad; + globalMax += pad; + + // --- Datasets --- + + // Dataset 0: sin min/max (for band) + XYSeriesCollection sinMinMaxDataset = new XYSeriesCollection(); + sinMinMaxDataset.addSeries(sinMax); // upper + sinMinMaxDataset.addSeries(sinMin); // lower + + // Dataset 1: tan min/max (for band) + XYSeriesCollection tanMinMaxDataset = new XYSeriesCollection(); + tanMinMaxDataset.addSeries(tanMax); // upper + tanMinMaxDataset.addSeries(tanMin); // lower + + // Dataset 2: main curves + XYSeriesCollection mainDataset = new XYSeriesCollection(); + mainDataset.addSeries(sinMain); + mainDataset.addSeries(tanMain); + + // Dataset 3: std diamonds + XYSeriesCollection stdDataset = new XYSeriesCollection(); + stdDataset.addSeries(sinStd); + stdDataset.addSeries(tanStd); + + // --- Chart skeleton --- + JFreeChart chart = ChartFactory.createXYLineChart( + "Min/Max Bands + STD Demo", + "x", + "y", + null, + PlotOrientation.VERTICAL, + true, + true, + false + ); + XYPlot plot = chart.getXYPlot(); + + // Transparent backgrounds + chart.setBackgroundPaint(Color.WHITE); + plot.setBackgroundPaint(Color.WHITE); + plot.setOutlinePaint(null); + plot.setDomainGridlinePaint(new Color(180, 180, 180)); // very light + plot.setRangeGridlinePaint(new Color(180, 180, 180)); + + plot.getDomainAxis().setAutoRange(false); // lock the axis so that they never resize + plot.getRangeAxis().setAutoRange(false); + plot.getDomainAxis().setRange(xLower, xUpper); + plot.getRangeAxis().setRange(globalMin, globalMax); + + // --- Legend to the right + chart.getLegend().setPosition(RectangleEdge.RIGHT); + chart.getLegend().setBackgroundPaint(Color.WHITE); +// chart.getLegend().setFrame(BlockBorder.NONE); + + // --- Renderer 0: sin band --- + XYDifferenceRenderer sinBandRenderer = new XYDifferenceRenderer(); + Color sinBandColor = new Color(sinColor.getRed(), sinColor.getGreen(), sinColor.getBlue(), 40); + sinBandRenderer.setPositivePaint(sinBandColor); + sinBandRenderer.setNegativePaint(sinBandColor); + sinBandRenderer.setSeriesStroke(0, new BasicStroke(0f)); + sinBandRenderer.setSeriesStroke(1, new BasicStroke(0f)); +// sinBandRenderer.setOutlinePaint(null); + sinBandRenderer.setSeriesVisibleInLegend(0, false); // hide sin-max legend entries + sinBandRenderer.setSeriesVisibleInLegend(1, false); + + plot.setDataset(0, sinMinMaxDataset); + plot.setRenderer(0, sinBandRenderer); + + // --- Renderer 1: tan band --- + XYDifferenceRenderer tanBandRenderer = new XYDifferenceRenderer(); + Color tanBandColor = new Color(tanColor.getRed(), tanColor.getGreen(), tanColor.getBlue(), 40); + tanBandRenderer.setPositivePaint(tanBandColor); + tanBandRenderer.setNegativePaint(tanBandColor); + tanBandRenderer.setSeriesStroke(0, new BasicStroke(0f)); + tanBandRenderer.setSeriesStroke(1, new BasicStroke(0f)); +// tanBandRenderer.setOutlinePaint(null); + tanBandRenderer.setSeriesVisibleInLegend(0, false); + tanBandRenderer.setSeriesVisibleInLegend(1, false); + + plot.setDataset(1, tanMinMaxDataset); + plot.setRenderer(1, tanBandRenderer); + + // --- Renderer 2: main curves --- + XYLineAndShapeRenderer lineRenderer = new XYLineAndShapeRenderer(true, false); + lineRenderer.setSeriesPaint(0, sinColor); + lineRenderer.setSeriesPaint(1, tanColor); + lineRenderer.setSeriesStroke(0, new BasicStroke(2f)); + lineRenderer.setSeriesStroke(1, new BasicStroke(2f)); + lineRenderer.setSeriesVisibleInLegend(0, true); // keep the main curves visible in Legend + lineRenderer.setSeriesVisibleInLegend(1, true); + + plot.setDataset(2, mainDataset); + plot.setRenderer(2, lineRenderer); + + // --- Renderer 3: STD diamonds --- + XYLineAndShapeRenderer stdRenderer = new XYLineAndShapeRenderer(false, true); + Shape diamond = createDiamondShape(4); + + stdRenderer.setSeriesShape(0, diamond); + stdRenderer.setSeriesShape(1, diamond); + stdRenderer.setSeriesPaint(0, sinColor.darker()); + stdRenderer.setSeriesPaint(1, tanColor.darker()); + stdRenderer.setSeriesVisibleInLegend(0, false); + stdRenderer.setSeriesVisibleInLegend(1, false); + + plot.setDataset(3, stdDataset); + plot.setRenderer(3, stdRenderer); + + // --- ChartPanel --- + ChartPanel panel = new ChartPanel(chart); + panel.setOpaque(true); // must be opaque for white to show + panel.setBackground(Color.WHITE); + + // --- checkboxes to control display + JPanel controls = new JPanel(); + JCheckBox cbAvg = new JCheckBox("Averages", true); + JCheckBox cbMinMax = new JCheckBox("Min-Max", false); + JCheckBox cbStd = new JCheckBox("STD", false); + controls.add(cbAvg); + controls.add(cbMinMax); + controls.add(cbStd); + + // Averages (dataset 2) + cbAvg.addActionListener(e -> { + boolean on = cbAvg.isSelected(); + plot.getRenderer(2).setSeriesVisible(0, on); + plot.getRenderer(2).setSeriesVisible(1, on); + lineRenderer.setSeriesVisibleInLegend(0, true); // force legend visible + lineRenderer.setSeriesVisibleInLegend(1, true); + }); + + // Min-Max (datasets 0 and 1) + cbMinMax.addActionListener(e -> { + boolean on = cbMinMax.isSelected(); + plot.setRenderer(0, on ? sinBandRenderer : null); + plot.setRenderer(1, on ? tanBandRenderer : null); + }); + + // STD (dataset 3) + cbStd.addActionListener(e -> { + boolean on = cbStd.isSelected(); + stdRenderer.setSeriesVisible(0, on); + stdRenderer.setSeriesVisible(1, on); + }); + + // --- Frame --- + JFrame frame = new JFrame("JFreeChart Min/Max/STD Demo"); + frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); + frame.add(panel, BorderLayout.CENTER); + frame.add(controls, BorderLayout.SOUTH); + frame.setSize(900, 600); + frame.setLocationRelativeTo(null); + frame.setVisible(true); + }); + } + + private static Shape createDiamondShape(int size) { + Path2D.Double p = new Path2D.Double(); + p.moveTo(0, -size); + p.lineTo(size, 0); + p.lineTo(0, size); + p.lineTo(-size, 0); + p.closePath(); + return p; + } + } diff --git a/vcell-client/src/main/java/cbit/plot/gui/AbstractDataPanel.java b/vcell-client/src/main/java/cbit/plot/gui/AbstractDataPanel.java new file mode 100644 index 0000000000..99f9235782 --- /dev/null +++ b/vcell-client/src/main/java/cbit/plot/gui/AbstractDataPanel.java @@ -0,0 +1,201 @@ +package cbit.plot.gui; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.vcell.util.gui.NonEditableDefaultTableModel; +import org.vcell.util.gui.ScrollTable; + +import javax.swing.*; +import javax.swing.event.ChangeEvent; +import javax.swing.event.ChangeListener; +import javax.swing.table.DefaultTableCellRenderer; +import javax.swing.table.TableCellRenderer; +import java.awt.*; +import java.awt.event.*; +import java.beans.PropertyChangeEvent; +import java.beans.PropertyChangeListener; + +public abstract class AbstractDataPanel extends JPanel { + + protected static final Logger lg = LogManager.getLogger(AbstractDataPanel.class); + + protected ScrollTable scrollPaneTable; + protected NonEditableDefaultTableModel nonEditableDefaultTableModel = null; + + protected JPopupMenu popupMenu = null; + protected JMenuItem miCopyAll = null; + protected JMenuItem miCopyHDF5 = null; + + protected enum CopyAction { copy, copyrow, copyall } + + protected final AbstractDataPanel.IvjEventHandler ivjEventHandler = new AbstractDataPanel.IvjEventHandler(); + + // ------------------------------------------------------------ + // Event handler (shared) + // ------------------------------------------------------------ + protected class IvjEventHandler implements ActionListener, MouseListener, PropertyChangeListener, ChangeListener { + + @Override + public void mouseClicked(MouseEvent e) { + if (e.getSource() == getScrollPaneTable()) { + int row = getScrollPaneTable().rowAtPoint(e.getPoint()); + int col = getScrollPaneTable().columnAtPoint(e.getPoint()); + lg.debug(getClass().getSimpleName() + ": clicked row=" + row + " col=" + col); + + if (SwingUtilities.isRightMouseButton(e)) { + getPopupMenu().show(e.getComponent(), e.getX(), e.getY()); + } + onMouseClick(row, col, e); + } + } + + @Override public void mousePressed(MouseEvent e) {} + @Override public void mouseReleased(MouseEvent e) {} + @Override public void mouseEntered(MouseEvent e) {} + @Override public void mouseExited(MouseEvent e) {} + + @Override + public void actionPerformed(ActionEvent e) { + // Reserved for future shared actions + } + + @Override + public void propertyChange(PropertyChangeEvent evt) { + onPropertyChange(evt); + } + + @Override + public void stateChanged(ChangeEvent e) { + onStateChanged(e); + } + } + + // ------------------------------------------------------------ + // Header renderer wrapper (shared) + // ------------------------------------------------------------ + protected class GenericHeaderRenderer extends DefaultTableCellRenderer { + + private final TableCellRenderer base; + + public GenericHeaderRenderer(TableCellRenderer base) { + this.base = base; + } + + @Override + public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, + boolean hasFocus, int row, int column) { + Component c = base.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column); + if (!(c instanceof JLabel)) { + return c; + } + return c; + } + } + + // ------------------------------------------------------------ + // Constructor + // ------------------------------------------------------------ + public AbstractDataPanel() { + super(); + initialize(); + } + + // ------------------------------------------------------------ + // Initialization + // ------------------------------------------------------------ + private void initialize() { + try { + setLayout(new BorderLayout()); + add(getScrollPaneTable().getEnclosingScrollPane(), BorderLayout.CENTER); + + JLabel footer = new JLabel(getFooterLabelText()); + add(footer, BorderLayout.SOUTH); + + initConnections(); + } catch (Throwable exc) { + handleException(exc); + } + } + + protected String getFooterLabelText() { + return "To Copy table data to the clipboard use the right mouse button menu."; + } + + protected void initConnections() throws Exception { + this.addPropertyChangeListener(ivjEventHandler); + getScrollPaneTable().addMouseListener(ivjEventHandler); + + getScrollPaneTable().setModel(getNonEditableDefaultTableModel()); + getScrollPaneTable().createDefaultColumnsFromModel(); + + TableCellRenderer baseHeaderRenderer = getScrollPaneTable().getTableHeader().getDefaultRenderer(); + getScrollPaneTable().getTableHeader().setDefaultRenderer(new GenericHeaderRenderer(baseHeaderRenderer)); + } + + protected void handleException(Throwable exception) { + lg.error("Uncaught exception in " + getClass().getSimpleName(), exception); + System.out.println("--------- UNCAUGHT EXCEPTION ---------"); + exception.printStackTrace(System.out); + } + + // ------------------------------------------------------------ + // Lazy getters + // ------------------------------------------------------------ + protected ScrollTable getScrollPaneTable() { + if (scrollPaneTable == null) { + try { + scrollPaneTable = new ScrollTable(); + scrollPaneTable.setCellSelectionEnabled(true); + } catch (Throwable ivjExc) { + handleException(ivjExc); + } + } + return scrollPaneTable; + } + + protected NonEditableDefaultTableModel getNonEditableDefaultTableModel() { + if (nonEditableDefaultTableModel == null) { + try { + nonEditableDefaultTableModel = new NonEditableDefaultTableModel(); + } catch (Throwable ivjExc) { + handleException(ivjExc); + } + } + return nonEditableDefaultTableModel; + } + + protected JPopupMenu getPopupMenu() { + if (popupMenu == null) { + popupMenu = new JPopupMenu(); + + miCopyAll = new JMenuItem("Copy All"); + miCopyAll.addActionListener(e -> onCopyCells(false)); + popupMenu.add(miCopyAll); + + miCopyHDF5 = new JMenuItem("Copy to HDF5"); + miCopyHDF5.setEnabled(false); + miCopyHDF5.addActionListener(e -> onCopyCells(true)); + popupMenu.add(miCopyHDF5); + } + return popupMenu; + } + + // ------------------------------------------------------------ + // Hooks for subclasses + // ------------------------------------------------------------ + protected void onCopyCells(boolean isHDF5) { + // Subclasses override if needed + } + + protected void onMouseClick(int row, int col, MouseEvent e) { + // Subclasses override if needed + } + + protected void onPropertyChange(PropertyChangeEvent evt) { + // Subclasses override if needed + } + + protected void onStateChanged(ChangeEvent e) { + // Subclasses override if needed + } +} \ No newline at end of file diff --git a/vcell-client/src/main/java/cbit/plot/gui/AbstractPlotPanel.java b/vcell-client/src/main/java/cbit/plot/gui/AbstractPlotPanel.java new file mode 100644 index 0000000000..da4adcaadb --- /dev/null +++ b/vcell-client/src/main/java/cbit/plot/gui/AbstractPlotPanel.java @@ -0,0 +1,542 @@ +package cbit.plot.gui; + +import cbit.vcell.solver.ode.gui.MoleculeVisualizationPanel; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import java.awt.*; +import java.awt.event.*; +import java.awt.geom.*; +import java.text.DecimalFormat; +import java.text.NumberFormat; +import java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; + +import javax.swing.JFrame; +import javax.swing.JLabel; +import javax.swing.JOptionPane; +import javax.swing.JPanel; +import javax.swing.KeyStroke; +import javax.swing.border.Border; + +public abstract class AbstractPlotPanel extends JPanel { + + private static final Logger lg = LogManager.getLogger(AbstractPlotPanel.class); + + // Insets and strokes + protected static final int LEFT_INSET = 50; + protected static final int RIGHT_INSET = 20; + protected static final int TOP_INSET = 20; + protected static final int BOTTOM_INSET = 30; + + protected static final float AXIS_STROKE = 1.0f; + protected static final float CURVE_STROKE = 1.5f; + + // Generic renderer interface + protected interface SeriesRenderer { + void draw(Graphics2D g2, + int x0, int x1, int y0, int y1, + int plotWidth, int plotHeight, + double xMaxRounded, double yMaxRounded, double yMinRounded, + double dt); + /** + * Returns the closest pixel-space point to (mouseX, mouseY), + * or null if this renderer does not support snapping. + */ + default Point getClosestPoint(int mouseX, int mouseY) { + return null; // default: no snapping + } + } + + // AVG renderer: polyline + protected static class AvgRenderer implements SeriesRenderer { + final double[] time; + final double[] values; + final Color color; + final AbstractPlotPanel parent; + + private int[] xs; + private int[] ys; + + AvgRenderer(double[] time, double[] values, Color color, AbstractPlotPanel parent) { + this.time = time; + this.values = values; + this.color = color; + this.parent = parent; + } + @Override + public void draw(Graphics2D g2, + int x0, int x1, int y0, int y1, + int plotWidth, int plotHeight, + double xMaxRounded, double yMaxRounded, double yMinRounded, + double dt) { + + int n = values.length; + if (n < 2) return; + + xs = new int[n]; + ys = new int[n]; + + for (int i = 0; i < n; i++) { + double t = (time != null ? time[i] : i * dt); + xs[i] = x0 + (int)Math.round((t / xMaxRounded) * plotWidth); + double v = values[i]; + double norm = (v - yMinRounded) / (yMaxRounded - yMinRounded); + ys[i] = y0 - (int)Math.round(norm * plotHeight); + } + + g2.setColor(color); + g2.setStroke(new BasicStroke(CURVE_STROKE, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND)); + g2.drawPolyline(xs, ys, n); + + // Draw the polyline + g2.setColor(color); + g2.setStroke(new BasicStroke(CURVE_STROKE, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND)); + g2.drawPolyline(xs, ys, n); + + // Draw nodes if enabled + if (parent.getShowNodes()) { // parent is the AbstractPlotPanel + g2.setColor(color); + int diameter = 4; // small, unobtrusive + int radius = diameter / 2; + + for (int i = 0; i < n; i++) { + int cx = xs[i] - radius; + int cy = ys[i] - radius; + g2.fillOval(cx, cy, diameter, diameter); + } + } + } + @Override + public Point getClosestPoint(int mouseX, int mouseY) { + if (xs == null || ys == null) return null; + int bestIndex = -1; + double bestDist2 = Double.POSITIVE_INFINITY; + for (int i = 0; i < xs.length; i++) { + double dx = xs[i] - mouseX; + double dy = ys[i] - mouseY; + double d2 = dx*dx + dy*dy; + + if (d2 < bestDist2) { + bestDist2 = d2; + bestIndex = i; + } + } + if (bestIndex < 0) return null; + return new Point(xs[bestIndex], ys[bestIndex]); + } + } + + // Band renderer: min/max or sd (envelope) + protected static class BandRenderer implements SeriesRenderer { + final double[] time; + final double[] upper; + final double[] lower; + final Color fillColor; + final AbstractPlotPanel parent; + + BandRenderer(double[] time, double[] upper, double[] lower, Color fillColor, AbstractPlotPanel parent) { + this.time = time; + this.upper = upper; + this.lower = lower; + this.fillColor = fillColor; + this.parent = parent; + } + + @Override + public void draw(Graphics2D g2, + int x0, int x1, int y0, int y1, + int plotWidth, int plotHeight, + double xMaxRounded, double yMaxRounded, double yMinRounded, + double dt) { + + int n = upper.length; + if (n < 2) return; + + Path2D path = new Path2D.Double(); + + double t0 = (time != null ? time[0] : 0.0); + double v0 = upper[0]; + double norm0 = (v0 - yMinRounded) / (yMaxRounded - yMinRounded); + int xStart = x0 + (int)Math.round((t0 / xMaxRounded) * plotWidth); + int yStart = y0 - (int)Math.round(norm0 * plotHeight); + path.moveTo(xStart, yStart); + + for (int i = 1; i < n; i++) { + double t = (time != null ? time[i] : i * dt); + double v = upper[i]; + double norm = (v - yMinRounded) / (yMaxRounded - yMinRounded); + int x = x0 + (int)Math.round((t / xMaxRounded) * plotWidth); + int y = y0 - (int)Math.round(norm * plotHeight); + path.lineTo(x, y); + } + + for (int i = n - 1; i >= 0; i--) { + double t = (time != null ? time[i] : i * dt); + double v = lower[i]; + double norm = (v - yMinRounded) / (yMaxRounded - yMinRounded); + int x = x0 + (int)Math.round((t / xMaxRounded) * plotWidth); + int y = y0 - (int)Math.round(norm * plotHeight); + path.lineTo(x, y); + } + + path.closePath(); + g2.setColor(fillColor); + g2.fill(path); + } + } + + // Renderer options list + private boolean fieldShowNodes = true; // whether to draw small circles at the data points (nodes) + private boolean fieldSnapToNodes = true; // whether the crosshair snaps to the nearest node (if false, it shows exact mouse coordinates) + private boolean fieldShowCrosshair = true; // whether to show the crosshair at all (if false, mouse coordinates are still tracked and sent to the callback, but no crosshair is drawn) + + // Renderers list + protected final List renderers = new ArrayList<>(); + + // Scaling state + protected double globalMin = 0; // on the-y axis; x-axis is always 0 to dt*(N-1) + protected double globalMax = 1; + protected double dt = 1; + + // Crosshair state + protected Integer mouseX = null; // mouse coordinates in pixels, relative to the panel; null if mouse is outside the plot area + protected Integer mouseY = null; + protected boolean crosshairEnabled = true; + protected Consumer coordCallback; + + // Cached plot area + protected int lastX0, lastX1, lastY0, lastY1; // pixel coordinates of the plot area (insets from the panel edges) + protected double lastXMaxRounded; // in seconds + protected double lastYMaxRounded; // in molecules + protected double lastYMinRounded; // in molecules (could be negative if avg-sd<0 + + public AbstractPlotPanel() { + addMouseMotionListener(new MouseMotionAdapter() { + @Override + public void mouseMoved(MouseEvent e) { + int mx = e.getX(); // in pixels, relative to the panel + int my = e.getY(); + + if (mx >= lastX0 && mx <= lastX1 && my >= lastY1 && my <= lastY0) { + mouseX = mx; + mouseY = my; + } else { + mouseX = null; + mouseY = null; + } + + if (crosshairEnabled && mouseX != null && mouseY != null) { + mx = mouseX; + my = mouseY; + if (fieldSnapToNodes) { + Point snapped = findClosestNode(mx, my); + if (snapped != null) { + mx = snapped.x; // mx and my are now snapped to the nearest node's pixel coordinates + my = snapped.y; + mouseX = mx; // update mouseX and mouseY to the snapped coordinates for crosshair drawing + mouseY = my; + } + } + double fracX = (mx - lastX0) / (double)(lastX1 - lastX0); + double xVal = fracX * lastXMaxRounded; // mouse coord on x-axis in seconds + double fracY = (lastY0 - my) / (double)(lastY0 - lastY1); + double yVal = lastYMinRounded + fracY * (lastYMaxRounded - lastYMinRounded); // mouse coord on y-axis in molecules + + if (coordCallback != null) { + coordCallback.accept(new double[]{xVal, yVal}); + } + } else { + if (coordCallback != null) { + coordCallback.accept(null); + } + } + repaint(); + } + }); + + addMouseListener(new MouseAdapter() { + @Override + public void mouseExited(MouseEvent e) { + mouseX = null; + mouseY = null; + repaint(); + } + }); + } + + // Public API + + public void setCrosshairEnabled(boolean enabled) { + this.crosshairEnabled = enabled; + repaint(); + } + + public void setCoordinateCallback(Consumer cb) { + this.coordCallback = cb; + } + + public void clearAllRenderers() { + renderers.clear(); + } + + public void setGlobalMinMax(double min, double max) { + this.globalMin = min; + this.globalMax = max; + } + + public void setDt(double dt) { + this.dt = dt; + } + + // High-level, stat-aware renderers + + public void addAvgRenderer(double[] time, double[] avg, Color color, String name, Object statTag) { + renderers.add(new AvgRenderer(time, avg, color, this)); + } + + public void addMinMaxRenderer(double[] time, double[] min, double[] max, Color color, String name, Object statTag) { + renderers.add(new BandRenderer(time, max, min, color, this)); + } + + public void addSDRenderer(double[] time, double[] low, double[] high, Color color, String name, Object statTag) { + renderers.add(new BandRenderer(time, high, low, color, this)); + } + + // Utilities + + protected double roundUpNice(double value) { + if (value <= 0) return 1; + double exp = Math.pow(10, Math.floor(Math.log10(value))); + double n = value / exp; + double[] steps = {1,2,3,4,5,6,7,8,9,10}; // 1–10 sequence + for (double s : steps) { + if (n <= s) return s * exp; + } + return 10 * exp; + } + + public static String formatTick(double value, double step) { + double absStep = Math.abs(step); + String s; + if (absStep >= 1.0) s = String.format("%.0f", value); + else if (absStep >= 0.1) s = String.format("%.1f", value); + else if (absStep >= 0.01) s = String.format("%.2f", value); + else if (absStep >= 0.001)s = String.format("%.3f", value); + else if (absStep >= 0.0001)s = String.format("%.4f", value); + else return String.format("%.2E", value); + + while (s.contains(".") && s.endsWith("0")) { + s = s.substring(0, s.length() - 1); + } + if (s.endsWith(".")) { + s = s.substring(0, s.length() - 1); + } + return s; + } + + public boolean getShowNodes() { + return fieldShowNodes; + } + public boolean getSnapToNodes() { + return fieldSnapToNodes; + } + private Point findClosestNode(int mouseX, int mouseY) { // use for SnapToNodes feature + Point best = null; + double bestDist2 = Double.POSITIVE_INFINITY; + for (SeriesRenderer r : renderers) { + Point p = r.getClosestPoint(mouseX, mouseY); + if (p != null) { + double dx = p.x - mouseX; + double dy = p.y - mouseY; + double d2 = dx*dx + dy*dy; + if (d2 < bestDist2) { + bestDist2 = d2; + best = p; + } + } + } + // Snap only if within a threshold (e.g., 10px) + if (best != null && bestDist2 <= 400) { // 10px radius + return best; + } + return null; + } + + private static Shape createDiamondShape(int size) { + Path2D.Double p = new Path2D.Double(); + p.moveTo(0, -size); + p.lineTo(size, 0); + p.lineTo(0, size); + p.lineTo(-size, 0); + p.closePath(); + return p; + } + + + // ------------------------------------------------------------------- + + @Override + protected void paintComponent(Graphics g) { + super.paintComponent(g); + + Graphics2D g2 = (Graphics2D) g; + g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + + int w = getWidth(); // width of the component (in pixels) + int h = getHeight(); // height of the component + g2.setColor(Color.white); + g2.fillRect(0, 0, w, h); + + int x0 = LEFT_INSET; + int x1 = w - RIGHT_INSET; + int y0 = h - BOTTOM_INSET; + int y1 = TOP_INSET; + lastX0 = x0; // in pixels + lastX1 = x1; + lastY0 = y0; + lastY1 = y1; + + int plotWidth = x1 - x0; + int plotHeight = y0 - y1; + if (plotWidth <= 0 || plotHeight <= 0) return; + + // --- determine max length from all renderers that use arrays ----- + int maxLength = 0; // number of timepoints + for (SeriesRenderer r : renderers) { + if (r instanceof AvgRenderer ar) { + maxLength = Math.max(maxLength, ar.values.length); + } else if (r instanceof BandRenderer br) { + maxLength = Math.max(maxLength, br.upper.length); + } + } + if (maxLength < 2) return; + + // --- compute axis scaling ----------------------------------------- + double yMaxRounded = roundUpNice(globalMax); // in molecules + double yMinRounded = (globalMin < 0) ? -roundUpNice(-globalMin) : 0; // may be negative if avg-sd<0 + double xMax = dt * (maxLength - 1); // in seconds + double xMaxRounded = roundUpNice(xMax); + lastXMaxRounded = xMaxRounded; // seconds + lastYMaxRounded = yMaxRounded; // molecules + lastYMinRounded = yMinRounded; // molecules + + // --- compute pixel location of value zero on the y-axis, to draw the horizontal axis there + double normZero = (0 - yMinRounded) / (yMaxRounded - yMinRounded); + int yZeroPix = y0 - (int)Math.round(normZero * plotHeight); + + FontMetrics fm = g2.getFontMetrics(); + + // --- grid lines ---------------------------------------------- + g2.setColor(new Color(220, 220, 220)); + g2.setStroke(new BasicStroke(1f)); + + int yTicks = 5; // number of major horizontal ticks (above 0) + double yStep = yMaxRounded / yTicks; + // k runs over all integer multiples of yStep that fall inside the range + int kMin = (int)Math.floor(yMinRounded / yStep); + int kMax = (int)Math.ceil(yMaxRounded / yStep); + for (int k = kMin; k <= kMax; k++) { + double valueMajor = k * yStep; // ----- major gridline ----- + if (valueMajor >= yMinRounded - 1e-9 && valueMajor <= yMaxRounded + 1e-9) { + double normMajor = (valueMajor - yMinRounded) / (yMaxRounded - yMinRounded); + int yPixMajor = y0 - (int)Math.round(normMajor * plotHeight); + g2.drawLine(x0, yPixMajor, x1, yPixMajor); + } + double valueMid = valueMajor + yStep / 2.0; // ----- mid gridline ----- + if (valueMid >= yMinRounded && valueMid <= yMaxRounded) { + double normMid = (valueMid - yMinRounded) / (yMaxRounded - yMinRounded); + int yPixMid = y0 - (int)Math.round(normMid * plotHeight); + g2.drawLine(x0, yPixMid, x1, yPixMid); + } + } + + double[] xMajor = {0, xMaxRounded / 2, xMaxRounded}; // vertical grid lines + for (int i = 0; i < xMajor.length; i++) { + double xvMajor = xMajor[i]; + int xPixMajor = x0 + (int)Math.round((xvMajor / xMaxRounded) * plotWidth); + g2.drawLine(xPixMajor, y1, xPixMajor, y0); + + if (i < xMajor.length - 1) { + double xvMid = (xMajor[i] + xMajor[i + 1]) / 2.0; + int xPixMid = x0 + (int)Math.round((xvMid / xMaxRounded) * plotWidth); + g2.drawLine(xPixMid, y1, xPixMid, y0); + } + } + + // --- draw axes ------------------------------------------------ + g2.setColor(Color.black); + g2.setStroke(new BasicStroke(AXIS_STROKE)); + + g2.drawLine(x0, yZeroPix, x1, yZeroPix); // horizontal axis, going through the "0 molecules" point + g2.drawLine(x0, y0, x0, y1); // vertical axis + + // --- ticks --------------------------------------------------- + g2.setColor(Color.black); + g2.setStroke(new BasicStroke(AXIS_STROKE)); + + // yStep was computed as: yStep = yMaxRounded / yTicks; + // and gridlines were drawn at k * yStep for k in [floor(min/step), ceil(max/step)] + kMin = (int)Math.floor(yMinRounded / yStep); // y-axis ticks (on the vertical axis) + kMax = (int)Math.ceil(yMaxRounded / yStep); + for (int k = kMin; k <= kMax; k++) { + double value = k * yStep; + if (value < yMinRounded - 1e-9 || value > yMaxRounded + 1e-9) { + continue; // skip values outside the rounded range (floating‑point guard) + } + // convert to pixel using the SAME normalization as gridlines and renderer + double norm = (value - yMinRounded) / (yMaxRounded - yMinRounded); + int yPix = y0 - (int)Math.round(norm * plotHeight); + g2.drawLine(x0 - 5, yPix, x0, yPix); // major tick (little horizontal line on the vertical axis) + String label = formatTick(value, yStep); // label + int sw = fm.stringWidth(label); + g2.drawString(label, x0 - 10 - sw, yPix + fm.getAscent() / 2); + + if (k < kMax) { // mid tick (between this and next) + double midValue = value + yStep / 2.0; + if (midValue >= yMinRounded && midValue <= yMaxRounded) { + double normMid = (midValue - yMinRounded) / (yMaxRounded - yMinRounded); + int yPixMid = y0 - (int)Math.round(normMid * plotHeight); + g2.drawLine(x0 - 3, yPixMid, x0, yPixMid); + } + } + } + double xStep = xMajor[1] - xMajor[0]; // x-axis ticks (on the horizontal axis) + for (int i = 0; i < xMajor.length; i++) { + double xvMajor = xMajor[i]; + int xPixMajor = x0 + (int)Math.round((xvMajor / xMaxRounded) * plotWidth); + g2.drawLine(xPixMajor, yZeroPix, xPixMajor, yZeroPix + 5); // draw major tick on the x-axis (yZeroPix), not at y0 + + String label = formatTick(xvMajor, xStep); // label stays at the bottom + int sw = fm.stringWidth(label); + g2.drawString(label, xPixMajor - sw / 2, y0 + fm.getAscent() + 5); + + if (i < xMajor.length - 1) { + double xvMid = (xMajor[i] + xMajor[i + 1]) / 2.0; + int xPixMid = x0 + (int)Math.round((xvMid / xMaxRounded) * plotWidth); + g2.drawLine(xPixMid, yZeroPix, xPixMid, yZeroPix + 3); // mid tick also on the x‑axis + } + } + + // Crosshair + if (crosshairEnabled && mouseX != null && mouseY != null) { + g2.setColor(new Color(180, 180, 180)); + g2.setStroke(new BasicStroke(1f)); + g2.drawLine(mouseX, y1, mouseX, y0); + g2.drawLine(x0, mouseY, x1, mouseY); + } + + // Renderers (bands first, then lines) + for (SeriesRenderer r : renderers) { + if (r instanceof BandRenderer) { + r.draw(g2, x0, x1, y0, y1, plotWidth, plotHeight, xMaxRounded, yMaxRounded, yMinRounded, dt); + } + } + for (SeriesRenderer r : renderers) { + if (r instanceof AvgRenderer) { + r.draw(g2, x0, x1, y0, y1, plotWidth, plotHeight, xMaxRounded, yMaxRounded, yMinRounded, dt); + } + } + } +} \ No newline at end of file diff --git a/vcell-client/src/main/java/cbit/plot/gui/ClusterDataPanel.java b/vcell-client/src/main/java/cbit/plot/gui/ClusterDataPanel.java new file mode 100644 index 0000000000..2e9a791610 --- /dev/null +++ b/vcell-client/src/main/java/cbit/plot/gui/ClusterDataPanel.java @@ -0,0 +1,321 @@ +package cbit.plot.gui; + +import cbit.vcell.desktop.VCellTransferable; +import cbit.vcell.math.ReservedVariable; +import cbit.vcell.parser.Expression; +import cbit.vcell.parser.ExpressionException; +import cbit.vcell.parser.SymbolTableEntry; +import cbit.vcell.simdata.UiTableExporterToHDF5; +import cbit.vcell.solver.ode.ODESolverResultSet; +import cbit.vcell.solver.ode.gui.ClusterSpecificationPanel; +import cbit.vcell.util.ColumnDescription; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.vcell.util.gui.NonEditableDefaultTableModel; +import org.vcell.util.gui.ScrollTable; +import org.vcell.util.gui.SpecialtyTableRenderer; + +import javax.swing.*; +import javax.swing.event.ChangeEvent; +import javax.swing.event.ChangeListener; +import javax.swing.table.DefaultTableCellRenderer; +import javax.swing.table.TableCellRenderer; +import javax.swing.table.TableColumn; +import java.awt.*; +import java.awt.event.MouseEvent; +import java.beans.PropertyChangeEvent; +import java.io.File; + +public class ClusterDataPanel extends AbstractDataPanel { + + private static final Logger lg = LogManager.getLogger(ClusterDataPanel.class); + + // ------------------------------------------------------------------------- + // Cluster-specific header renderer + // ------------------------------------------------------------------------- + private class ClusterHeaderRenderer extends DefaultTableCellRenderer { + + private final TableCellRenderer base; + + public ClusterHeaderRenderer(TableCellRenderer base) { + this.base = base; + } + + @Override + public Component getTableCellRendererComponent( + JTable table, Object value, boolean isSelected, + boolean hasFocus, int row, int column) { + + Component c = base.getTableCellRendererComponent( + table, value, isSelected, hasFocus, row, column); + + if (!(c instanceof JLabel)) { + return c; + } + + JLabel lbl = (JLabel) c; + String name = value == null ? "" : value.toString(); + + ClusterSpecificationPanel.DisplayMode mode = + (ClusterSpecificationPanel.DisplayMode) + ((JComponent) table).getClientProperty("ClusterDisplayMode"); + + if (mode == null) { + lbl.setToolTipText(null); + return lbl; + } + + String text = ""; + String unit = ""; + String tooltip = ""; + + ClusterSpecificationPanel.ClusterStatistic stat = + ClusterSpecificationPanel.ClusterStatistic.fromString(name); + + if (column == 0) { + unit = "seconds"; + text = "" + name + " [" + unit + "]"; + tooltip = "Simulation time"; + } else { + switch (mode) { + case COUNTS: + unit = "molecules"; + text = "" + name + " [" + unit + "]"; + tooltip = "Number of clusters made of " + name + " " + unit + ""; + break; + + case MEAN: + case OVERALL: + if (stat != null) { + unit = stat.unit(); + text = "" + stat.fullName() + + " [" + unit + "]"; + tooltip = "" + stat.description() + ""; + } + break; + } + } + + lbl.setText(text); + lbl.setToolTipText(tooltip); + return lbl; + } + } + + // ------------------------------------------------------------------------- + // Constructor + // ------------------------------------------------------------------------- + public ClusterDataPanel() { + super(); + } + + // ------------------------------------------------------------------------- + // Hook up cluster-specific header renderer + // ------------------------------------------------------------------------- + @Override + protected void initConnections() throws Exception { + super.initConnections(); + + TableCellRenderer baseHeaderRenderer = + getScrollPaneTable().getTableHeader().getDefaultRenderer(); + + getScrollPaneTable().getTableHeader() + .setDefaultRenderer(new ClusterHeaderRenderer(baseHeaderRenderer)); + } + + // ------------------------------------------------------------------------- + // Override copy handler + // ------------------------------------------------------------------------- + @Override + protected void onCopyCells(boolean isHDF5) { + copyCells(isHDF5); + } + + // ------------------------------------------------------------------------- + // Instance version of old static copyCells() + // ------------------------------------------------------------------------- + private void copyCells(boolean isHDF5) { + try { + int r = getScrollPaneTable().getRowCount(); + int c = getScrollPaneTable().getColumnCount(); + + if (r < 1 || c < 1) { + throw new Exception("No table cell is selected."); + } + + int[] rows = new int[r]; + int[] columns = new int[c]; + for (int i = 0; i < r; i++) rows[i] = i; + for (int i = 0; i < c; i++) columns[i] = i; + + lg.debug("Copying cluster data: rows=" + r + " columns=" + c + " isHDF5=" + isHDF5); + + boolean bHistogram = false; + String blankCellValue = "-1"; + boolean bHasTimeColumn = true; + + StringBuffer buffer = new StringBuffer(); + + if (isHDF5) { + int columnCount = c; + int rowCount = r; + + String[] columnNames = new String[columnCount]; + for (int i = 0; i < columnCount; i++) { + columnNames[i] = getScrollPaneTable().getColumnName(i); + } + + Object[][] rowColValues = new Object[rowCount][columnCount]; + for (int i = 0; i < rowCount; i++) { + for (int j = 0; j < columnCount; j++) { + rowColValues[i][j] = getScrollPaneTable().getValueAt(i, j); + } + } + + UiTableExporterToHDF5.exportTableToHDF5( + bHistogram, + blankCellValue, + columns, + rows, + "t", + "hdf5DescriptionText", + columnNames, + null, + null, + rowColValues + ); + } + + // Column headers + for (int i = 0; i < c; i++) { + buffer.append(getScrollPaneTable().getColumnName(i)); + if (i < c - 1) buffer.append("\t"); + } + + Expression[] resolvedValues = + new Expression[c - (bHasTimeColumn ? 1 : 0)]; + + // Rows + for (int i = 0; i < r; i++) { + buffer.append("\n"); + for (int j = 0; j < c; j++) { + Object cell = getScrollPaneTable().getValueAt(i, j); + cell = (cell != null ? cell : ""); + + buffer.append(cell.toString()); + if (j < c - 1) buffer.append("\t"); + + if (!cell.equals("") && (!bHasTimeColumn || j > 0)) { + resolvedValues[j - (bHasTimeColumn ? 1 : 0)] = + new Expression(((Double) cell).doubleValue()); + } + } + } + + VCellTransferable.ResolvedValuesSelection rvs = + new VCellTransferable.ResolvedValuesSelection( + new SymbolTableEntry[c - 1], + null, + resolvedValues, + buffer.toString() + ); + + VCellTransferable.sendToClipboard(rvs); + + } catch (Exception ex) { + lg.error("Error copying cluster data", ex); + JOptionPane.showMessageDialog( + this, + "Error copying cluster data: " + ex.getMessage(), + "Copy Error", + JOptionPane.ERROR_MESSAGE + ); + } + } + + public void updateData(ClusterSpecificationPanel.ClusterSelection sel) + throws ExpressionException { + + if (sel == null) { + getScrollPaneTable().putClientProperty("ClusterDisplayMode", null); + } else { + getScrollPaneTable().putClientProperty("ClusterDisplayMode", sel.mode); + } + + if (sel == null || sel.resultSet == null || + sel.columns == null || sel.columns.isEmpty()) { + + getNonEditableDefaultTableModel() + .setDataVector(new Object[][]{}, new Object[]{"No data"}); + + getScrollPaneTable().createDefaultColumnsFromModel(); + revalidate(); + repaint(); + return; + } + + ODESolverResultSet srs = sel.resultSet; + java.util.List columns = sel.columns; + + int timeIndex = srs.findColumn(ReservedVariable.TIME.getName()); + double[] times = srs.extractColumn(timeIndex); + int rowCount = times.length; + + String[] columnNames = new String[1 + columns.size()]; + columnNames[0] = "time"; + for (int i = 0; i < columns.size(); i++) { + columnNames[i + 1] = columns.get(i).getName(); + } + + Object[][] data = new Object[rowCount][columnNames.length]; + for (int r = 0; r < rowCount; r++) { + int c = 0; + data[r][c++] = times[r]; + for (ColumnDescription cd : columns) { + int idx = srs.findColumn(cd.getName()); + double[] y = srs.extractColumn(idx); + data[r][c++] = y[r]; + } + } + + getNonEditableDefaultTableModel().setDataVector(data, columnNames); + getScrollPaneTable().createDefaultColumnsFromModel(); + autoSizeTableColumns(getScrollPaneTable()); + + revalidate(); + repaint(); + } + + // ------------------------------------------------------------------------- + // autoSizeTableColumns() — unchanged + // ------------------------------------------------------------------------- + private void autoSizeTableColumns(JTable table) { + final int margin = 12; + + for (int col = 0; col < table.getColumnCount(); col++) { + TableColumn column = table.getColumnModel().getColumn(col); + + int maxWidth = 0; + + TableCellRenderer headerRenderer = table.getTableHeader().getDefaultRenderer(); + Component headerComp = headerRenderer.getTableCellRendererComponent( + table, column.getHeaderValue(), false, false, 0, col); + maxWidth = Math.max(maxWidth, headerComp.getPreferredSize().width); + + for (int row = 0; row < table.getRowCount(); row++) { + TableCellRenderer cellRenderer = table.getCellRenderer(row, col); + Component comp = table.prepareRenderer(cellRenderer, row, col); + maxWidth = Math.max(maxWidth, comp.getPreferredSize().width); + } + + column.setPreferredWidth(maxWidth + margin); + } + } + + public void setSpecialityRenderer(SpecialtyTableRenderer str) { + // TODO: write some appropriate renderer when we decide what to show in the tooltip + // use RendererViewerDoubleWithTooltip for inspiration +// getScrollPaneTable().setSpecialityRenderer(str); + } + +} diff --git a/vcell-client/src/main/java/cbit/plot/gui/ClusterPlotPanel.java b/vcell-client/src/main/java/cbit/plot/gui/ClusterPlotPanel.java new file mode 100644 index 0000000000..b6849ac056 --- /dev/null +++ b/vcell-client/src/main/java/cbit/plot/gui/ClusterPlotPanel.java @@ -0,0 +1,32 @@ +package cbit.plot.gui; + +import java.awt.*; +import java.awt.event.*; +import java.awt.geom.*; +import java.text.DecimalFormat; +import java.text.NumberFormat; +import java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; + +import javax.swing.JFrame; +import javax.swing.JLabel; +import javax.swing.JOptionPane; +import javax.swing.JPanel; +import javax.swing.KeyStroke; +import javax.swing.border.Border; + +import org.vcell.util.gui.GeneralGuiUtils; +import org.vcell.util.*; + +public class ClusterPlotPanel extends AbstractPlotPanel { + + public ClusterPlotPanel() { + super(); + // No additional initialization. + // All rendering, listeners, scaling, and crosshair logic live in AbstractPlotPanel. + } + + // If cluster-specific helpers are ever needed, they go here. + // For now, ClusterPlotPanel is intentionally empty. +} diff --git a/vcell-client/src/main/java/cbit/plot/gui/MoleculeDataPanel.java b/vcell-client/src/main/java/cbit/plot/gui/MoleculeDataPanel.java new file mode 100644 index 0000000000..1f550dc476 --- /dev/null +++ b/vcell-client/src/main/java/cbit/plot/gui/MoleculeDataPanel.java @@ -0,0 +1,371 @@ +package cbit.plot.gui; + +import cbit.vcell.client.data.ODEDataViewer; +import cbit.vcell.desktop.VCellTransferable; +import cbit.vcell.math.ReservedVariable; +import cbit.vcell.parser.Expression; +import cbit.vcell.parser.ExpressionException; +import cbit.vcell.parser.SymbolTableEntry; +import cbit.vcell.simdata.LangevinSolverResultSet; +import cbit.vcell.simdata.UiTableExporterToHDF5; +import cbit.vcell.solver.OutputTimeSpec; +import cbit.vcell.solver.UniformOutputTimeSpec; +import cbit.vcell.solver.ode.ODESolverResultSet; +import cbit.vcell.solver.ode.gui.ClusterSpecificationPanel; +import cbit.vcell.solver.ode.gui.MoleculeSpecificationPanel; +import cbit.vcell.solver.ode.gui.MoleculeVisualizationPanel; +import cbit.vcell.util.ColumnDescription; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.vcell.util.gui.NonEditableDefaultTableModel; +import org.vcell.util.gui.ScrollTable; + +import javax.swing.*; +import javax.swing.event.ChangeEvent; +import javax.swing.event.ChangeListener; +import javax.swing.table.DefaultTableCellRenderer; +import javax.swing.table.TableCellRenderer; +import javax.swing.table.TableColumn; +import java.awt.*; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.event.MouseEvent; +import java.awt.event.MouseListener; +import java.beans.PropertyChangeEvent; +import java.beans.PropertyChangeListener; + +public class MoleculeDataPanel extends AbstractDataPanel { + + private static final Logger lg = LogManager.getLogger(MoleculeDataPanel.class); + + public enum SubStatistic { + AVG("AVG"), + MIN("MIN"), + MAX("MAX"), + SD("SD"); + public final String uiLabel; + SubStatistic(String uiLabel) { + this.uiLabel = uiLabel; + } + } + + public static class MoleculeColumnInfo { + public final String entityName; + public final MoleculeSpecificationPanel.StatisticSelection statistic; + public final SubStatistic subStatistic; + public MoleculeColumnInfo(String entityName, + MoleculeSpecificationPanel.StatisticSelection statistic, + SubStatistic subStatistic) { + this.entityName = entityName; + this.statistic = statistic; + this.subStatistic = subStatistic; + } + } + + + private class MoleculeTwoRowHeaderRenderer extends DefaultTableCellRenderer { + + private final TableCellRenderer base; + public MoleculeTwoRowHeaderRenderer(TableCellRenderer base) { + this.base = base; + } + @Override + public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, + boolean hasFocus, int row, int column) { + Component c = base.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column); + if (!(c instanceof JLabel)) { + return c; + } + JLabel lbl = (JLabel) c; + Object id = table.getColumnModel().getColumn(column).getIdentifier(); + if (!(id instanceof MoleculeColumnInfo)) { // time column w. unit in dark red + lbl.setText("
" +"time" +"
" + + " [seconds]" + "
"); + return lbl; + } + MoleculeColumnInfo info = (MoleculeColumnInfo) id; + String statLabel = "" + info.subStatistic.uiLabel + ""; // Statistic label (bold) + String unit = " [molecules]"; // Unit in dark red + lbl.setText("
" + info.entityName + "
" + + "" + statLabel + unit + "
" + ); + return lbl; + } + } + + // ------------------------------------------------------ + + ODEDataViewer owner; + + public MoleculeDataPanel(ODEDataViewer owner) { + super(); + this.owner = owner; + } + @Override + protected void initConnections() throws Exception { + super.initConnections(); + TableCellRenderer baseHeaderRenderer = getScrollPaneTable().getTableHeader().getDefaultRenderer(); + getScrollPaneTable().getTableHeader().setDefaultRenderer(new MoleculeTwoRowHeaderRenderer(baseHeaderRenderer)); + } + + // ------------------------------------------------------ + + public void updateData(MoleculeSpecificationPanel.MoleculeSelection sel, LangevinSolverResultSet lsrs) + throws ExpressionException { + + if (sel == null) { + getScrollPaneTable().putClientProperty("MoleculeSelection", null); // may be useful for tooltip generation + } else { + getScrollPaneTable().putClientProperty("MoleculeSelection", sel); + } + + if (sel == null || lsrs == null || lsrs.isAverageDataAvailable() == false || // guard clause + sel.selectedColumns == null || sel.selectedColumns.isEmpty() || + sel.selectedStatistics == null || sel.selectedStatistics.isEmpty()) { + + getNonEditableDefaultTableModel().setDataVector(new Object[][]{}, new Object[]{"No data"}); + getScrollPaneTable().createDefaultColumnsFromModel(); + revalidate(); + repaint(); + return; + } + + ODESolverResultSet avgRS = lsrs.getAvg(); + ODESolverResultSet minRS = lsrs.getMin(); + ODESolverResultSet maxRS = lsrs.getMax(); + ODESolverResultSet stdRS = lsrs.getStd(); + java.util.List selectedColumns = sel.selectedColumns; + java.util.List selectedStatistics = sel.selectedStatistics; + + int timeIndex = avgRS.findColumn(ReservedVariable.TIME.getName()); + double[] times = avgRS.extractColumn(timeIndex); + int rowCount = times.length; + OutputTimeSpec outputTimeSpec = owner.getSimulation().getSolverTaskDescription().getOutputTimeSpec(); + double dt = ((UniformOutputTimeSpec)outputTimeSpec).getOutputTimeStep(); // uniform output time step + double endingTime = owner.getSimulation().getSolverTaskDescription().getTimeBounds().getEndingTime(); + for (int i = 0; i < times.length; i++) { + times[i] = i * dt; + } + + + + // --------------------------------------------------------------------- + // Build column names + // --------------------------------------------------------------------- + java.util.List colNames = new java.util.ArrayList<>(); + java.util.List metadata = new java.util.ArrayList<>(); + + colNames.add("time"); + metadata.add(null); // time has no entity/statistic + + for (ColumnDescription cd : selectedColumns) { + String base = cd.getName(); + for (MoleculeSpecificationPanel.StatisticSelection statSel : sel.selectedStatistics) { + switch (statSel) { + case AVG: + colNames.add(base + " AVG"); + metadata.add(new MoleculeColumnInfo(base, statSel, SubStatistic.AVG)); + break; + + case MIN_MAX: + colNames.add(base + " MIN"); + metadata.add(new MoleculeColumnInfo(base, statSel, SubStatistic.MIN)); + colNames.add(base + " MAX"); + metadata.add(new MoleculeColumnInfo(base, statSel, SubStatistic.MAX)); + break; + + case SD: + colNames.add(base + " SD"); + metadata.add(new MoleculeColumnInfo(base, statSel, SubStatistic.SD)); + break; + } + } + } + + // --------------------------------------------------------------------- + // Build data matrix + // --------------------------------------------------------------------- + Object[][] data = new Object[rowCount][colNames.size()]; + + for (int r = 0; r < rowCount; r++) { + int c = 0; + data[r][c++] = times[r]; + + for (ColumnDescription cd : selectedColumns) { + String baseName = cd.getName(); + // These are guaranteed to exist because of guard clause + double[] avgCol = avgRS.extractColumn(avgRS.findColumn(baseName)); + double[] minCol = minRS.extractColumn(minRS.findColumn(baseName)); + double[] maxCol = maxRS.extractColumn(maxRS.findColumn(baseName)); + double[] stdCol = stdRS.extractColumn(stdRS.findColumn(baseName)); + for (MoleculeSpecificationPanel.StatisticSelection statSel : selectedStatistics) { + switch (statSel) { + case AVG: + data[r][c++] = avgCol[r]; + break; + case MIN_MAX: + data[r][c++] = minCol[r]; + data[r][c++] = maxCol[r]; + break; + case SD: + data[r][c++] = stdCol[r]; // raw SD value + break; + } + } + } + } + // Push to model + getNonEditableDefaultTableModel().setDataVector( + data, + colNames.toArray(new String[0]) + ); + + // Rebuild columns so we can attach metadata + renderer + getScrollPaneTable().createDefaultColumnsFromModel(); + for (int i = 0; i < metadata.size(); i++) { + getScrollPaneTable().getColumnModel().getColumn(i) + .setIdentifier(metadata.get(i)); + } + autoSizeTableColumns(getScrollPaneTable()); + revalidate(); + repaint(); + } + + private void autoSizeTableColumns(JTable table) { + final int margin = 12; + + for (int col = 0; col < table.getColumnCount(); col++) { + TableColumn column = table.getColumnModel().getColumn(col); + + int maxWidth = 0; + + // header + TableCellRenderer headerRenderer = table.getTableHeader().getDefaultRenderer(); + Component headerComp = headerRenderer.getTableCellRendererComponent( + table, column.getHeaderValue(), false, false, 0, col); + maxWidth = Math.max(maxWidth, headerComp.getPreferredSize().width); + + // cells + for (int row = 0; row < table.getRowCount(); row++) { + TableCellRenderer cellRenderer = table.getCellRenderer(row, col); + Component comp = table.prepareRenderer(cellRenderer, row, col); + maxWidth = Math.max(maxWidth, comp.getPreferredSize().width); + } + + column.setPreferredWidth(maxWidth + margin); + } + } + + // ------------------------------------------------------ + + // ------------------------------------------------------------------------- + // Override copy handler + // ------------------------------------------------------------------------- + @Override + protected void onCopyCells(boolean isHDF5) { + copyCells(isHDF5); + } + + // ------------------------------------------------------------------------- + // Instance version of old static copyCells() + // ------------------------------------------------------------------------- + private void copyCells(boolean isHDF5) { + try { + int r = getScrollPaneTable().getRowCount(); + int c = getScrollPaneTable().getColumnCount(); + + if (r < 1 || c < 1) { + throw new Exception("No table cell is selected."); + } + + int[] rows = new int[r]; + int[] columns = new int[c]; + for (int i = 0; i < r; i++) rows[i] = i; + for (int i = 0; i < c; i++) columns[i] = i; + + lg.debug("Copying cluster data: rows=" + r + " columns=" + c + " isHDF5=" + isHDF5); + + boolean bHistogram = false; + String blankCellValue = "-1"; + boolean bHasTimeColumn = true; + + StringBuffer buffer = new StringBuffer(); + + if (isHDF5) { + int columnCount = c; + int rowCount = r; + + String[] columnNames = new String[columnCount]; + for (int i = 0; i < columnCount; i++) { + columnNames[i] = getScrollPaneTable().getColumnName(i); + } + + Object[][] rowColValues = new Object[rowCount][columnCount]; + for (int i = 0; i < rowCount; i++) { + for (int j = 0; j < columnCount; j++) { + rowColValues[i][j] = getScrollPaneTable().getValueAt(i, j); + } + } + + UiTableExporterToHDF5.exportTableToHDF5( + bHistogram, + blankCellValue, + columns, + rows, + "t", + "hdf5DescriptionText", + columnNames, + null, + null, + rowColValues + ); + } + + // Column headers + for (int i = 0; i < c; i++) { + buffer.append(getScrollPaneTable().getColumnName(i)); + if (i < c - 1) buffer.append("\t"); + } + + Expression[] resolvedValues = + new Expression[c - (bHasTimeColumn ? 1 : 0)]; + + // Rows + for (int i = 0; i < r; i++) { + buffer.append("\n"); + for (int j = 0; j < c; j++) { + Object cell = getScrollPaneTable().getValueAt(i, j); + cell = (cell != null ? cell : ""); + + buffer.append(cell.toString()); + if (j < c - 1) buffer.append("\t"); + + if (!cell.equals("") && (!bHasTimeColumn || j > 0)) { + resolvedValues[j - (bHasTimeColumn ? 1 : 0)] = + new Expression(((Double) cell).doubleValue()); + } + } + } + + VCellTransferable.ResolvedValuesSelection rvs = + new VCellTransferable.ResolvedValuesSelection( + new SymbolTableEntry[c - 1], + null, + resolvedValues, + buffer.toString() + ); + + VCellTransferable.sendToClipboard(rvs); + + } catch (Exception ex) { + lg.error("Error copying cluster data", ex); + JOptionPane.showMessageDialog( + this, + "Error copying cluster data: " + ex.getMessage(), + "Copy Error", + JOptionPane.ERROR_MESSAGE + ); + } + } + +} diff --git a/vcell-client/src/main/java/cbit/plot/gui/MoleculePlotPanel.java b/vcell-client/src/main/java/cbit/plot/gui/MoleculePlotPanel.java new file mode 100644 index 0000000000..8a1b924c12 --- /dev/null +++ b/vcell-client/src/main/java/cbit/plot/gui/MoleculePlotPanel.java @@ -0,0 +1,16 @@ +package cbit.plot.gui; + +public class MoleculePlotPanel extends AbstractPlotPanel { + + public MoleculePlotPanel() { + super(); + // No additional initialization. + // All rendering, listeners, scaling, and crosshair logic live in AbstractPlotPanel. + } + + // If molecule-specific helpers are ever needed, they go here. + // For now, MoleculePlotPanel is intentionally empty. + + + +} diff --git a/vcell-client/src/main/java/cbit/plot/gui/Plot2DDataPanel.java b/vcell-client/src/main/java/cbit/plot/gui/Plot2DDataPanel.java index 0c669b6f08..099171907c 100644 --- a/vcell-client/src/main/java/cbit/plot/gui/Plot2DDataPanel.java +++ b/vcell-client/src/main/java/cbit/plot/gui/Plot2DDataPanel.java @@ -130,17 +130,10 @@ private void exportHDF5() { /** * connEtoC3: (Plot2DDataPanel.initialize() --> Plot2DDataPanel.controlKeys()V) */ -/* WARNING: THIS METHOD WILL BE REGENERATED. */ private void connEtoC3() { try { - // user code begin {1} - // user code end this.controlKeys(); - // user code begin {2} - // user code end } catch (java.lang.Throwable ivjExc) { - // user code begin {3} - // user code end handleException(ivjExc); } } @@ -149,21 +142,14 @@ private void connEtoC3() { * connEtoM1: (plot2D1.this --> DefaultTableModel1.setDataVector([[Ljava.lang.Object;[Ljava.lang.Object;)V) * @param value cbit.plot.Plot2D */ -/* WARNING: THIS METHOD WILL BE REGENERATED. */ private void connEtoM1(Plot2D value) { try { - // user code begin {1} - // user code end if (getplot2D1() != null) { getNonEditableDefaultTableModel1().setDataVector(getplot2D1().getVisiblePlotDataValuesByRow(), getplot2D1().getVisiblePlotColumnTitles()); }else{ getNonEditableDefaultTableModel1().setDataVector((Object [][])null,(Object [])null); } - // user code begin {2} - // user code end } catch (java.lang.Throwable ivjExc) { - // user code begin {3} - // user code end handleException(ivjExc); } } @@ -172,19 +158,12 @@ private void connEtoM1(Plot2D value) { * connEtoM2: (plot2D1.change.stateChanged(javax.swing.event.ChangeEvent) --> NonEditableDefaultTableModel1.setDataVector([[Ljava.lang.Object;[Ljava.lang.Object;)V) * @param arg1 javax.swing.event.ChangeEvent */ -/* WARNING: THIS METHOD WILL BE REGENERATED. */ private void connEtoM2(javax.swing.event.ChangeEvent arg1) { try { - // user code begin {1} - // user code end if (getplot2D1() != null) { getNonEditableDefaultTableModel1().setDataVector(getplot2D1().getVisiblePlotDataValuesByRow(), getplot2D1().getVisiblePlotColumnTitles()); } - // user code begin {2} - // user code end } catch (java.lang.Throwable ivjExc) { - // user code begin {3} - // user code end handleException(ivjExc); } } @@ -193,17 +172,12 @@ private void connEtoM2(javax.swing.event.ChangeEvent arg1) { /** * connPtoP1SetTarget: (DefaultTableModel1.this <--> ScrollPaneTable.model) */ -/* WARNING: THIS METHOD WILL BE REGENERATED. */ private void connPtoP1SetTarget() { /* Set the target from the source */ try { getScrollPaneTable().setModel(getNonEditableDefaultTableModel1()); getScrollPaneTable().createDefaultColumnsFromModel(); - // user code begin {1} - // user code end } catch (java.lang.Throwable ivjExc) { - // user code begin {3} - // user code end handleException(ivjExc); } } @@ -211,25 +185,18 @@ private void connPtoP1SetTarget() { /** * connPtoP2SetSource: (Plot2DDataPanel.plot2D <--> plot2D1.this) */ -/* WARNING: THIS METHOD WILL BE REGENERATED. */ private void connPtoP2SetSource() { /* Set the source from the target */ try { if (ivjConnPtoP2Aligning == false) { - // user code begin {1} - // user code end ivjConnPtoP2Aligning = true; if ((getplot2D1() != null)) { this.setPlot2D(getplot2D1()); } - // user code begin {2} - // user code end ivjConnPtoP2Aligning = false; } } catch (java.lang.Throwable ivjExc) { ivjConnPtoP2Aligning = false; - // user code begin {3} - // user code end handleException(ivjExc); } } @@ -238,23 +205,16 @@ private void connPtoP2SetSource() { /** * connPtoP2SetTarget: (Plot2DDataPanel.plot2D <--> plot2D1.this) */ -/* WARNING: THIS METHOD WILL BE REGENERATED. */ private void connPtoP2SetTarget() { /* Set the target from the source */ try { if (ivjConnPtoP2Aligning == false) { - // user code begin {1} - // user code end ivjConnPtoP2Aligning = true; setplot2D1(this.getPlot2D()); - // user code begin {2} - // user code end ivjConnPtoP2Aligning = false; } } catch (java.lang.Throwable ivjExc) { ivjConnPtoP2Aligning = false; - // user code begin {3} - // user code end handleException(ivjExc); } } @@ -453,18 +413,13 @@ else if (copyAction == CopyAction.copyrow) { * Return the JMenuItemCopy property value. * @return javax.swing.JMenuItem */ -/* WARNING: THIS METHOD WILL BE REGENERATED. */ private javax.swing.JMenuItem getJMenuItemCopy() { if (ivjJMenuItemCopy == null) { try { ivjJMenuItemCopy = new javax.swing.JMenuItem(); ivjJMenuItemCopy.setName("JMenuItemCopy"); ivjJMenuItemCopy.setText("Copy Cells"); - // user code begin {1} - // user code end } catch (java.lang.Throwable ivjExc) { - // user code begin {2} - // user code end handleException(ivjExc); } } @@ -476,18 +431,13 @@ private javax.swing.JMenuItem getJMenuItemCopy() { * Return the JMenuItemCopyAll property value. * @return javax.swing.JMenuItem */ -/* WARNING: THIS METHOD WILL BE REGENERATED. */ private javax.swing.JMenuItem getJMenuItemCopyAll() { if (ivjJMenuItemCopyAll == null) { try { ivjJMenuItemCopyAll = new javax.swing.JMenuItem(); ivjJMenuItemCopyAll.setName("JMenuItemCopyAll"); ivjJMenuItemCopyAll.setText("Copy All"); - // user code begin {1} - // user code end } catch (java.lang.Throwable ivjExc) { - // user code begin {2} - // user code end handleException(ivjExc); } } @@ -501,11 +451,7 @@ private javax.swing.JMenuItem getJMenuItemExportHDF5() { ivjJMenuItemExportHDF5 = new javax.swing.JMenuItem(); ivjJMenuItemExportHDF5.setName("JMenuItemExportHDF5"); ivjJMenuItemExportHDF5.setText("Export Selected cells as HDF5 file"); - // user code begin {1} - // user code end } catch (java.lang.Throwable ivjExc) { - // user code begin {2} - // user code end handleException(ivjExc); } } @@ -518,11 +464,7 @@ private javax.swing.JMenuItem getJMenuItemCopyRow() { ivjJMenuItemCopyRow = new javax.swing.JMenuItem(); ivjJMenuItemCopyRow.setName("JMenuItemCopyRow"); ivjJMenuItemCopyRow.setText("Copy Rows"); - // user code begin {1} - // user code end } catch (java.lang.Throwable ivjExc) { - // user code begin {2} - // user code end handleException(ivjExc); } } @@ -537,11 +479,7 @@ private javax.swing.JPopupMenu getJPopupMenu1() { ivjJPopupMenu1.add(getJMenuItemCopyRow()); ivjJPopupMenu1.add(getJMenuItemCopyAll()); ivjJPopupMenu1.add(getJMenuItemExportHDF5()); - // user code begin {1} - // user code end } catch (java.lang.Throwable ivjExc) { - // user code begin {2} - // user code end handleException(ivjExc); } } @@ -552,16 +490,11 @@ private javax.swing.JPopupMenu getJPopupMenu1() { * Return the NonEditableDefaultTableModel1 property value. * @return cbit.gui.NonEditableDefaultTableModel */ -/* WARNING: THIS METHOD WILL BE REGENERATED. */ private org.vcell.util.gui.NonEditableDefaultTableModel getNonEditableDefaultTableModel1() { if (ivjNonEditableDefaultTableModel1 == null) { try { ivjNonEditableDefaultTableModel1 = new org.vcell.util.gui.NonEditableDefaultTableModel(); - // user code begin {1} - // user code end } catch (java.lang.Throwable ivjExc) { - // user code begin {2} - // user code end handleException(ivjExc); } } @@ -583,10 +516,7 @@ public Plot2D getPlot2D() { * Return the plot2D1 property value. * @return cbit.plot.Plot2D */ -/* WARNING: THIS METHOD WILL BE REGENERATED. */ private Plot2D getplot2D1() { - // user code begin {1} - // user code end return ivjplot2D1; } @@ -595,7 +525,6 @@ private Plot2D getplot2D1() { * Return the ScrollPaneTable property value. * @return javax.swing.JTable */ -/* WARNING: THIS METHOD WILL BE REGENERATED. */ private ScrollTable getScrollPaneTable() { if (ivjScrollPaneTable == null) { try { @@ -609,11 +538,7 @@ private ScrollTable getScrollPaneTable() { ivjScrollPaneTable.setDefaultRenderer(Object.class,rdwtt); ivjScrollPaneTable.setDefaultRenderer(Number.class,rdwtt); */ - // user code begin {1} - // user code end } catch (java.lang.Throwable ivjExc) { - // user code begin {2} - // user code end handleException(ivjExc); } } @@ -636,10 +561,7 @@ private void handleException(java.lang.Throwable exception) { * Initializes connections * @exception java.lang.Exception The exception description. */ -/* WARNING: THIS METHOD WILL BE REGENERATED. */ private void initConnections() throws java.lang.Exception { - // user code begin {1} - // user code end this.addPropertyChangeListener(ivjEventHandler); getScrollPaneTable().addMouseListener(ivjEventHandler); getJMenuItemCopy().addActionListener(ivjEventHandler); @@ -653,11 +575,8 @@ private void initConnections() throws java.lang.Exception { /** * Initialize the class. */ -/* WARNING: THIS METHOD WILL BE REGENERATED. */ private void initialize() { try { - // user code begin {1} - // user code end setName("Plot2DDataPanel"); setLayout(new java.awt.BorderLayout()); setSize(541, 348); @@ -670,8 +589,6 @@ private void initialize() { } catch (java.lang.Throwable ivjExc) { handleException(ivjExc); } - // user code begin {2} - // user code end } /** @@ -735,16 +652,10 @@ private void setplot2D1(Plot2D newValue) { connPtoP2SetSource(); connEtoM1(ivjplot2D1); firePropertyChange("plot2D", oldValue, newValue); - // user code begin {1} - // user code end } catch (java.lang.Throwable ivjExc) { - // user code begin {2} - // user code end handleException(ivjExc); } }; - // user code begin {3} - // user code end } /** diff --git a/vcell-client/src/main/java/cbit/plot/gui/PlotPane.java b/vcell-client/src/main/java/cbit/plot/gui/PlotPane.java index 7f7bcc8c81..44f999e93c 100644 --- a/vcell-client/src/main/java/cbit/plot/gui/PlotPane.java +++ b/vcell-client/src/main/java/cbit/plot/gui/PlotPane.java @@ -52,7 +52,7 @@ */ public class PlotPane extends JPanel { -class LineIcon implements Icon { +public class LineIcon implements Icon { private Paint lineColor; private LineIcon(Paint paint) { lineColor = paint; diff --git a/vcell-client/src/main/java/cbit/vcell/client/data/ODEDataViewer.java b/vcell-client/src/main/java/cbit/vcell/client/data/ODEDataViewer.java index a267f7208a..f14e341042 100644 --- a/vcell-client/src/main/java/cbit/vcell/client/data/ODEDataViewer.java +++ b/vcell-client/src/main/java/cbit/vcell/client/data/ODEDataViewer.java @@ -9,15 +9,15 @@ */ package cbit.vcell.client.data; -import java.awt.event.ActionEvent; -import java.awt.event.ActionListener; import java.util.HashMap; import java.util.Hashtable; -import javax.swing.JTabbedPane; +import javax.swing.*; import javax.swing.event.ChangeEvent; import javax.swing.event.ChangeListener; +import cbit.vcell.simdata.LangevinSolverResultSet; +import cbit.vcell.solver.ode.gui.*; import org.vcell.solver.nfsim.NFSimMolecularConfigurations; import org.vcell.util.document.VCDataIdentifier; import org.vcell.util.gui.DialogUtils; @@ -37,10 +37,7 @@ import cbit.vcell.solver.DataSymbolMetadata; import cbit.vcell.solver.Simulation; import cbit.vcell.solver.SimulationModelInfo; -import cbit.vcell.solver.ode.ODESimData; import cbit.vcell.solver.ode.ODESolverResultSet; -import cbit.vcell.solver.ode.gui.ODESolverPlotSpecificationPanel; -import cbit.vcell.solver.ode.gui.OutputSpeciesResultsPanel; import cbit.vcell.util.ColumnDescription; /** * Insert the type's description here. @@ -51,14 +48,25 @@ public class ODEDataViewer extends DataViewer { IvjEventHandler ivjEventHandler = new IvjEventHandler(); private ODESolverPlotSpecificationPanel ivjODESolverPlotSpecificationPanel1 = null; private PlotPane ivjPlotPane1 = null; + + private MoleculeSpecificationPanel moleculeSpecificationPanel = null; + private MoleculeVisualizationPanel moleculeVisualizationPanel = null; + private ClusterSpecificationPanel clusterSpecificationPanel = null; + private ClusterVisualizationPanel clusterVisualizationPanel = null; + private JTabbedPane ivjJTabbedPane = null; private ODESolverResultSet fieldOdeSolverResultSet = null; + public boolean hasLangevinBatchResults = false; + private LangevinSolverResultSet fieldLangevinSolverResultSet = null; private NFSimMolecularConfigurations nFSimMolecularConfigurations = null; - private javax.swing.JPanel ivjViewData = null; + private javax.swing.JPanel viewData = null; + private JPanel viewMultiClustersPanel = null; + private JPanel viewMultiDataPanel = null; private OutputSpeciesResultsPanel outputSpeciesResultsPanel = null; private VCDataIdentifier fieldVcDataIdentifier = null; private static final String OUTPUT_SPECIES_TABNAME = "Output Species"; + private static final String LANGEVIN_CLUSTER_RESULTS_TABNAME = "Langevin Clusters"; class IvjEventHandler implements java.beans.PropertyChangeListener { public void propertyChange(java.beans.PropertyChangeEvent evt) { @@ -217,6 +225,9 @@ public ODESolverPlotSpecificationPanel getODESolverPlotSpecificationPanel1() { public ODESolverResultSet getOdeSolverResultSet() { return fieldOdeSolverResultSet; } +public LangevinSolverResultSet getLangevinSolverResultSet() { + return fieldLangevinSolverResultSet; +} public NFSimMolecularConfigurations getNFSimMolecularConfigurations() { return nFSimMolecularConfigurations; } @@ -265,7 +276,9 @@ public VCDataIdentifier getVcDataIdentifier() { public void stateChanged(ChangeEvent e) { if(ivjJTabbedPane.getSelectedIndex() == ivjJTabbedPane.indexOfTab(OUTPUT_SPECIES_TABNAME)){ // TODO: here - }else { + } else if(ivjJTabbedPane.getSelectedIndex() == ivjJTabbedPane.indexOfTab(LANGEVIN_CLUSTER_RESULTS_TABNAME)) { + + } else { } } @@ -276,10 +289,18 @@ private javax.swing.JTabbedPane getJTabbedPane() { try { ivjJTabbedPane = new javax.swing.JTabbedPane(); ivjJTabbedPane.setName("JTabbedPane1"); - ivjJTabbedPane.insertTab("View Data", null, getViewData(), null, 0); + if(getSimulation() != null && hasLangevinBatchResults) { + ivjJTabbedPane.insertTab("View Data", null, getViewMultiData(), null, 0); + } else { + ivjJTabbedPane.insertTab("View Data", null, getViewData(), null, 0); + } + + ivjJTabbedPane.addTab(LANGEVIN_CLUSTER_RESULTS_TABNAME, getViewMultiClusters()); + outputSpeciesResultsPanel = new OutputSpeciesResultsPanel(this); outputSpeciesResultsPanel.addPropertyChangeListener(ivjEventHandler); ivjJTabbedPane.addTab(OUTPUT_SPECIES_TABNAME, outputSpeciesResultsPanel); + ivjJTabbedPane.addChangeListener(mainTabChangeListener); } catch (java.lang.Throwable ivjExc) { handleException(ivjExc); @@ -287,23 +308,105 @@ private javax.swing.JTabbedPane getJTabbedPane() { } return ivjJTabbedPane; } -/** - * Return the ViewData property value. - * @return javax.swing.JPanel - */ -private javax.swing.JPanel getViewData() { - if (ivjViewData == null) { +public void replaceViewDataTab() { + if (getSimulation() != null && ivjJTabbedPane != null) { + int selected = ivjJTabbedPane.getSelectedIndex(); + ivjJTabbedPane.removeTabAt(0); + ivjJTabbedPane.insertTab("View Data", null, getViewMultiData(), null, 0); + ivjJTabbedPane.setSelectedIndex(selected); + } +} + +private JPanel getViewMultiData() { + if (viewMultiDataPanel == null) { + try { + viewMultiDataPanel = new JPanel(); + viewMultiDataPanel.setName("ViewMultiData"); + viewMultiDataPanel.setLayout(new java.awt.BorderLayout()); + viewMultiDataPanel.add(getMoleculeSpecificationPanel(), "West"); + viewMultiDataPanel.add(getMoleculeVisualizationPanel(), "Center"); + } catch (java.lang.Throwable ivjExc) { + handleException(ivjExc); + } + } + return viewMultiDataPanel; +} +public MoleculeSpecificationPanel getMoleculeSpecificationPanel() { + if (moleculeSpecificationPanel == null) { + try { + moleculeSpecificationPanel = new MoleculeSpecificationPanel(this); + moleculeSpecificationPanel.setName("MoleculeSpecificationPanel"); + } catch (java.lang.Throwable ivjExc) { + handleException(ivjExc); + } + } + return moleculeSpecificationPanel; +} +private MoleculeVisualizationPanel getMoleculeVisualizationPanel() { + if (moleculeVisualizationPanel == null) { + try { + moleculeVisualizationPanel = new MoleculeVisualizationPanel(this); + moleculeVisualizationPanel.setName("DataVisualizationPanel"); + SpecialtyTableRenderer str = new RenderDataViewerDoubleWithTooltip(); + moleculeVisualizationPanel.setSpecialityRenderer(str); + } catch (java.lang.Throwable ivjExc) { + handleException(ivjExc); + } + } + return moleculeVisualizationPanel; +} +private JPanel getViewMultiClusters() { + if (viewMultiClustersPanel == null) { + try { + viewMultiClustersPanel = new JPanel(); + viewMultiClustersPanel.setName("ViewMultiClusters"); + viewMultiClustersPanel.setLayout(new java.awt.BorderLayout()); + viewMultiClustersPanel.add(getClusterSpecificationPanel(), "West"); + viewMultiClustersPanel.add(getClusterVisualizationPanel(), "Center"); + } catch (java.lang.Throwable ivjExc) { + handleException(ivjExc); + } + } + return viewMultiClustersPanel; +} +public ClusterSpecificationPanel getClusterSpecificationPanel() { + if (clusterSpecificationPanel == null) { + try { + clusterSpecificationPanel = new ClusterSpecificationPanel(this); + clusterSpecificationPanel.setName("ClusterSpecificationPanel"); + } catch (java.lang.Throwable ivjExc) { + handleException(ivjExc); + } + } + return clusterSpecificationPanel; +} +public ClusterVisualizationPanel getClusterVisualizationPanel() { + if (clusterVisualizationPanel == null) { + try { + clusterVisualizationPanel = new ClusterVisualizationPanel(this); + clusterVisualizationPanel.setName("ClusterVisualizationPanel"); + SpecialtyTableRenderer str = new RenderDataViewerDoubleWithTooltip(); + clusterVisualizationPanel.setSpecialityRenderer(str); + } catch (java.lang.Throwable ivjExc) { + handleException(ivjExc); + } + } + return clusterVisualizationPanel; +} + +private JPanel getViewData() { + if (viewData == null) { try { - ivjViewData = new javax.swing.JPanel(); - ivjViewData.setName("ViewData"); - ivjViewData.setLayout(new java.awt.BorderLayout()); - getViewData().add(getODESolverPlotSpecificationPanel1(), "West"); - getViewData().add(getPlotPane1(), "Center"); + viewData = new JPanel(); + viewData.setName("ViewData"); + viewData.setLayout(new java.awt.BorderLayout()); + viewData.add(getODESolverPlotSpecificationPanel1(), "West"); + viewData.add(getPlotPane1(), "Center"); } catch (java.lang.Throwable ivjExc) { handleException(ivjExc); } } - return ivjViewData; + return viewData; } @@ -390,6 +493,11 @@ public void setOdeSolverResultSet(ODESolverResultSet odeSolverResultSet) { fieldOdeSolverResultSet = odeSolverResultSet; firePropertyChange("odeSolverResultSet", oldValue, odeSolverResultSet); } +public void setLangevinSolverResultSet(LangevinSolverResultSet langevinSolverResultSet) { + LangevinSolverResultSet oldValue = fieldLangevinSolverResultSet; + fieldLangevinSolverResultSet = langevinSolverResultSet; + firePropertyChange("langevinSolverResultSet", oldValue, langevinSolverResultSet); +} public void setNFSimMolecularConfigurations(NFSimMolecularConfigurations nFSimMolecularConfigurations) { NFSimMolecularConfigurations oldValue = nFSimMolecularConfigurations; this.nFSimMolecularConfigurations = nFSimMolecularConfigurations; @@ -426,14 +534,28 @@ public void setVcDataIdentifier(VCDataIdentifier vcDataIdentifier) { fieldVcDataIdentifier = vcDataIdentifier; setOdeDataContext(); firePropertyChange("vcDataIdentifier", oldValue, vcDataIdentifier); - outputSpeciesResultsPanel.refreshData(); + if(hasLangevinBatchResults) { + getClusterVisualizationPanel().refreshData(); + getMoleculeVisualizationPanel().refreshData(); + // order is important, the visualization panels must refresh first so that necessary data will be present when + // the specification panels start sending events to the visualization panels + getClusterSpecificationPanel().refreshData(); + getMoleculeSpecificationPanel().refreshData(); + } else { + outputSpeciesResultsPanel.refreshData(); + } } public void setOdeDataContext() { - if(getSimulation() != null && getNFSimMolecularConfigurations() != null) { + if(getSimulation() != null && hasLangevinBatchResults) { + getJTabbedPane().setEnabledAt(getJTabbedPane().indexOfTab(OUTPUT_SPECIES_TABNAME), false); + getJTabbedPane().setEnabledAt(getJTabbedPane().indexOfTab(LANGEVIN_CLUSTER_RESULTS_TABNAME), true); + } else if(getSimulation() != null && getNFSimMolecularConfigurations() != null) { getJTabbedPane().setEnabledAt(getJTabbedPane().indexOfTab(OUTPUT_SPECIES_TABNAME), true); + getJTabbedPane().setEnabledAt(getJTabbedPane().indexOfTab(LANGEVIN_CLUSTER_RESULTS_TABNAME), false); } else { getJTabbedPane().setEnabledAt(getJTabbedPane().indexOfTab(OUTPUT_SPECIES_TABNAME), false); + getJTabbedPane().setEnabledAt(getJTabbedPane().indexOfTab(LANGEVIN_CLUSTER_RESULTS_TABNAME), false); } } diff --git a/vcell-client/src/main/java/cbit/vcell/client/data/SimResultsViewer.java b/vcell-client/src/main/java/cbit/vcell/client/data/SimResultsViewer.java index e4382369fc..ad46b55daf 100644 --- a/vcell-client/src/main/java/cbit/vcell/client/data/SimResultsViewer.java +++ b/vcell-client/src/main/java/cbit/vcell/client/data/SimResultsViewer.java @@ -79,6 +79,14 @@ private DataViewer createODEDataViewer() throws DataAccessException { odeDataViewer = new ODEDataViewer(); odeDataViewer.setSimulation(getSimulation()); ODESolverResultSet odesrs = ((ODEDataManager)dataManager).getODESolverResultSet(); + LangevinSolverResultSet langevinSolverResultSet = ((ODEDataManager)dataManager).getLangevinSolverResultSet(); + if(getSimulation() != null && langevinSolverResultSet != null) { + if (getSimulation().getSimulationOwner().getMathDescription().isLangevin() && langevinSolverResultSet.isAverageDataAvailable() && langevinSolverResultSet.isClusterDataAvailable()) { + odeDataViewer.setLangevinSolverResultSet(langevinSolverResultSet); + odeDataViewer.hasLangevinBatchResults = true; + odeDataViewer.replaceViewDataTab(); + } + } odeDataViewer.setOdeSolverResultSet(odesrs); odeDataViewer.setNFSimMolecularConfigurations(((ODEDataManager)dataManager).getNFSimMolecularConfigurations()); odeDataViewer.setVcDataIdentifier(dataManager.getVCDataIdentifier()); diff --git a/vcell-client/src/main/java/cbit/vcell/solver/ode/gui/AbstractSpecificationPanel.java b/vcell-client/src/main/java/cbit/vcell/solver/ode/gui/AbstractSpecificationPanel.java new file mode 100644 index 0000000000..bb38fecf0a --- /dev/null +++ b/vcell-client/src/main/java/cbit/vcell/solver/ode/gui/AbstractSpecificationPanel.java @@ -0,0 +1,187 @@ +package cbit.vcell.solver.ode.gui; + +import cbit.vcell.client.data.ODEDataViewer; +import cbit.vcell.client.desktop.biomodel.DocumentEditorSubPanel; +import cbit.vcell.math.ODESolverResultSetColumnDescription; +import cbit.vcell.simdata.LangevinSolverResultSet; +import cbit.vcell.solver.SimulationModelInfo; +import cbit.vcell.solver.ode.ODESolverResultSet; +import cbit.vcell.util.ColumnDescription; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.vcell.util.gui.CollapsiblePanel; + +import java.awt.*; +import java.beans.PropertyChangeListener; +import javax.swing.*; + +@SuppressWarnings("serial") +public abstract class AbstractSpecificationPanel extends DocumentEditorSubPanel { + + private static final Logger lg = LogManager.getLogger(AbstractSpecificationPanel.class); + + // ------------------------------ + // Shared UI components + // ------------------------------ + protected CollapsiblePanel displayOptionsCollapsiblePanel = null; + + protected JScrollPane jScrollPaneYAxis = null; + protected static final String YAxisLabelText = "Y Axis: "; + protected JLabel yAxisLabel = null; + + protected JList yAxisChoiceList = null; + protected DefaultListModel defaultListModelY = null; + + // ------------------------------ + // Constructor + // ------------------------------ + public AbstractSpecificationPanel() { + super(); + initialize(); + } + + // ------------------------------ + // Initialization (shared layout) + // ------------------------------ + private void initialize() { + lg.debug("initialize() called"); + + setPreferredSize(new Dimension(213, 600)); + setLayout(new GridBagLayout()); + setSize(248, 604); + setMinimumSize(new Dimension(125, 300)); + + // X-axis label + JLabel xAxisLabel = new JLabel("X Axis: "); + GridBagConstraints gbc = new GridBagConstraints(); + gbc.anchor = GridBagConstraints.WEST; + gbc.gridx = 0; gbc.gridy = 0; + gbc.insets = new Insets(4, 4, 0, 4); + add(xAxisLabel, gbc); + + // X-axis textbox + JTextField xAxisTextBox = new JTextField("time [seconds]"); + xAxisTextBox.setEnabled(false); + xAxisTextBox.setEditable(false); + gbc = new GridBagConstraints(); + gbc.gridx = 0; gbc.gridy = 1; + gbc.fill = GridBagConstraints.HORIZONTAL; + gbc.weightx = 1.0; + gbc.insets = new Insets(0, 4, 4, 4); + add(xAxisTextBox, gbc); + + // Y-axis label + gbc = new GridBagConstraints(); + gbc.anchor = GridBagConstraints.WEST; + gbc.insets = new Insets(4, 4, 0, 4); + gbc.gridx = 0; gbc.gridy = 2; + add(getYAxisLabel(), gbc); + + // Display options panel (content added by subclasses) + gbc = new GridBagConstraints(); + gbc.fill = GridBagConstraints.BOTH; + gbc.insets = new Insets(4, 4, 5, 4); + gbc.gridx = 0; + gbc.gridy = 3; + add(getDisplayOptionsPanel(), gbc); + + // Y-axis list + gbc = new GridBagConstraints(); + gbc.gridx = 0; gbc.gridy = 4; + gbc.fill = GridBagConstraints.BOTH; + gbc.weightx = 1.0; + gbc.weighty = 1.0; + gbc.insets = new Insets(0, 4, 4, 4); + add(getJScrollPaneYAxis(), gbc); + } + + + // ------------------------------ + // Shared Y-axis label + // ------------------------------ + protected JLabel getYAxisLabel() { + if (yAxisLabel == null) { + yAxisLabel = new JLabel(); + yAxisLabel.setName("YAxisLabel"); + String text = "" + YAxisLabelText + ""; + yAxisLabel.setText(text); + } + return yAxisLabel; + } + + // ------------------------------ + // Shared Display Options container + // (subclasses populate content) + // ------------------------------ + protected CollapsiblePanel getDisplayOptionsPanel() { + if (displayOptionsCollapsiblePanel == null) { + displayOptionsCollapsiblePanel = new CollapsiblePanel("Display Options", true); + JPanel content = displayOptionsCollapsiblePanel.getContentPanel(); + content.setLayout(new GridBagLayout()); + } + return displayOptionsCollapsiblePanel; + } + + // ------------------------------ + // Shared Y-axis scroll pane + // ------------------------------ + protected JScrollPane getJScrollPaneYAxis() { + if (jScrollPaneYAxis == null) { + jScrollPaneYAxis = new JScrollPane(); + jScrollPaneYAxis.setName("JScrollPaneYAxis"); + jScrollPaneYAxis.setViewportView(getYAxisChoice()); + jScrollPaneYAxis.setMinimumSize(new Dimension(100, 120)); + jScrollPaneYAxis.setPreferredSize(new Dimension(100, 120)); + } + return jScrollPaneYAxis; + } + + // ------------------------------ + // Shared Y-axis list + // ------------------------------ + protected JList getYAxisChoice() { + if (yAxisChoiceList == null) { + yAxisChoiceList = new JList(); + yAxisChoiceList.setName("YAxisChoice"); + yAxisChoiceList.setBounds(0, 0, 160, 120); + yAxisChoiceList.setSelectionMode( + ListSelectionModel.MULTIPLE_INTERVAL_SELECTION + ); + yAxisChoiceList.setCellRenderer(new DefaultListCellRenderer() { + @Override + public Component getListCellRendererComponent( + JList list, Object value, int index, + boolean isSelected, boolean cellHasFocus) { + + JLabel label = (JLabel) super.getListCellRendererComponent( + list, value, index, isSelected, cellHasFocus + ); + if (value instanceof ColumnDescription cd) { + label.setText(cd.getName()); + } + return label; + } + }); + } + return yAxisChoiceList; + } + + // ------------------------------ + // Shared Y-axis model + // ------------------------------ + protected DefaultListModel getDefaultListModelY() { + if (defaultListModelY == null) { + defaultListModelY = new DefaultListModel<>(); + } + return defaultListModelY; + } + + // ------------------------------ + // Shared exception handler + // ------------------------------ + protected void handleException(Throwable exception) { + lg.error("Uncaught exception in AbstractSpecificationPanel", exception); + System.out.println("--------- UNCAUGHT EXCEPTION ---------"); + exception.printStackTrace(System.out); + } +} \ No newline at end of file diff --git a/vcell-client/src/main/java/cbit/vcell/solver/ode/gui/AbstractVisualizationPanel.java b/vcell-client/src/main/java/cbit/vcell/solver/ode/gui/AbstractVisualizationPanel.java new file mode 100644 index 0000000000..d556f702cb --- /dev/null +++ b/vcell-client/src/main/java/cbit/vcell/solver/ode/gui/AbstractVisualizationPanel.java @@ -0,0 +1,338 @@ +package cbit.vcell.solver.ode.gui; + +import cbit.vcell.client.desktop.biomodel.DocumentEditorSubPanel; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.vcell.util.gui.JToolBarToggleButton; +import org.vcell.util.gui.VCellIcons; + + +import java.awt.*; +import java.text.DecimalFormat; +import java.text.DecimalFormatSymbols; +import java.util.Locale; +import javax.swing.*; +import javax.swing.border.EmptyBorder; + +public abstract class AbstractVisualizationPanel extends DocumentEditorSubPanel { + + private static final Logger lg = LogManager.getLogger(AbstractVisualizationPanel.class); + + protected class LineIcon implements Icon { + private final Color color; + public LineIcon(Color color) { + this.color = color; + } + @Override + public void paintIcon(Component c, Graphics g, int x, int y) { + Graphics2D g2 = (Graphics2D)g; + g2.setStroke(new BasicStroke(4.0f)); + g2.setPaint(color); + int midY = y + getIconHeight() / 2; + g2.drawLine(x, midY, x + getIconWidth(), midY); + } + @Override + public int getIconWidth() { return 50; } + @Override + public int getIconHeight() { + return 4; // more vertical room for a wider stroke + } + } + + protected JPanel cardPanel; + protected JPanel plotPanelContainer; + protected JPanel dataPanelContainer; + protected JPanel legendPanel; + protected JPanel legendContentPanel; + protected JPanel bottomRightPanel; + + protected JLabel bottomLabel; // info about the current simulation and # of jobs + private JLabel timeLabelBottom; // "time" under the X-axis in the plot + protected JCheckBox crosshairCheckBox; + protected JLabel crosshairCoordLabel; + + protected JToolBarToggleButton plotButton; + protected JToolBarToggleButton dataButton; + + protected CardLayout cardLayout; + + + public AbstractVisualizationPanel() { + super(); + // do NOT call initialize here !!! there are overriden calls to abstract methods that rely on subclass fields + } + + // initialize calls ovverriden methods that rely on subclass fields, so we can't call it from the constructor. + // Instead, each subclass must call initialize() after its own constructor has run and its own fields are initialized. + protected void initialize() { + setPreferredSize(new Dimension(420, 400)); + setLayout(new BorderLayout()); + setSize(513, 457); + setBackground(Color.white); + + add(getCardPanel(), BorderLayout.CENTER); + add(getBottomRightPanel(), BorderLayout.SOUTH); + add(getLegendPanel(), BorderLayout.EAST); + } + + // ------------------------- + // Abstract hooks + // ------------------------- + // Subclass must provide the plot panel (MoleculePlotPanel or ClusterPlotPanel) + protected abstract JPanel createPlotPanel(); + // Subclass must provide the data panel (MoleculeDataPanel or ClusterDataPanel) + protected abstract JPanel createDataPanel(); + // Subclass must implement crosshair enabling logic + protected abstract void setCrosshairEnabled(boolean enabled); + + // ------------------------- + // Card panel + // ------------------------- + protected JPanel getCardPanel() { + if (cardPanel == null) { + cardPanel = new JPanel(); + cardPanel.setName("CardPanel"); + cardLayout = new CardLayout(); + cardPanel.setLayout(cardLayout); + cardPanel.add(getPlotPanelContainer(), "PlotPanelContainer"); + cardPanel.add(getDataPanelContainer(), "DataPanelContainer"); + } + return cardPanel; + } + + private JPanel getPlotPanelContainer() { + if (plotPanelContainer == null) { + plotPanelContainer = new JPanel(); + plotPanelContainer.setName("PlotPanelContainer"); + plotPanelContainer.setLayout(new BorderLayout()); + plotPanelContainer.add(createPlotPanel(), BorderLayout.CENTER); // Subclass provides the actual plot panel + plotPanelContainer.add(getTimeLabelBottom(), BorderLayout.SOUTH); // Bottom label (e.g., "time") + } + return plotPanelContainer; + } + + private JLabel getTimeLabelBottom() { + if (timeLabelBottom == null) { + timeLabelBottom = new JLabel(); + timeLabelBottom.setName("TimeLabelBottom"); + timeLabelBottom.setText("time"); + timeLabelBottom.setForeground(Color.black); + timeLabelBottom.setHorizontalTextPosition(SwingConstants.CENTER); + timeLabelBottom.setHorizontalAlignment(SwingConstants.CENTER); + } + return timeLabelBottom; + } + + private JPanel getDataPanelContainer() { + if (dataPanelContainer == null) { + dataPanelContainer = new JPanel(); + dataPanelContainer.setName("DataPanelContainer"); + dataPanelContainer.setLayout(new BorderLayout()); + dataPanelContainer.add(createDataPanel(), BorderLayout.CENTER); + } + return dataPanelContainer; + } + // ------------------------- + // Legend panel + // ------------------------- + protected JPanel getLegendPanel() { + if (legendPanel == null) { + legendPanel = new JPanel(); + legendPanel.setName("LegendPanel"); + legendPanel.setLayout(new BorderLayout()); + legendPanel.add(new JLabel(" "), BorderLayout.SOUTH); // South spacer (keeps layout stable) + + JLabel labelLegendTitle = new JLabel("Plot Legend:"); // Title label with border + labelLegendTitle.setBorder(new EmptyBorder(10, 4, 10, 4)); + legendPanel.add(labelLegendTitle, BorderLayout.NORTH); + + JScrollPane scrollPane = new JScrollPane(getLegendContentPanel()); // Scrollpane containing the legend content panel + scrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_NEVER); + scrollPane.setBorder(new EmptyBorder(0, 0, 0, 0)); + legendPanel.add(scrollPane, BorderLayout.CENTER); + } + return legendPanel; + } + protected JPanel getLegendContentPanel() { + if (legendContentPanel == null) { + legendContentPanel = new JPanel() { + @Override + public Dimension getPreferredSize() { + // Reserve space for vertical scrollbar from the start + Dimension d = super.getPreferredSize(); + int scrollbarWidth = UIManager.getInt("ScrollBar.width"); + return new Dimension(d.width + scrollbarWidth, d.height); + } + }; + legendContentPanel.setName("LegendContentPanel"); + legendContentPanel.setLayout(new BoxLayout(legendContentPanel, BoxLayout.Y_AXIS)); + } + return legendContentPanel; + } + // ------------------------- + // Bottom-right panel + // ------------------------- + private JPanel getBottomRightPanel() { + if (bottomRightPanel == null) { + bottomRightPanel = new JPanel(); + bottomRightPanel.setName("BottomRightPanel"); + bottomRightPanel.setLayout(new GridBagLayout()); + + GridBagConstraints gbc; + gbc = new GridBagConstraints(); + gbc.gridx = 1; + gbc.gridy = 0; + gbc.fill = GridBagConstraints.HORIZONTAL; + gbc.weightx = 1.0; + gbc.insets = new Insets(4, 4, 4, 4); + bottomRightPanel.add(getBottomLabel(), gbc); + + gbc = new GridBagConstraints(); + gbc.gridx = 2; + gbc.gridy = 0; + gbc.insets = new Insets(4, 4, 4, 2); + bottomRightPanel.add(getCrosshairCheckBox(), gbc); + + gbc = new GridBagConstraints(); + gbc.gridx = 3; + gbc.gridy = 0; + gbc.insets = new Insets(4, 2, 4, 2); + bottomRightPanel.add(getCrosshairCoordLabel(), gbc); + + gbc = new GridBagConstraints(); + gbc.gridx = 4; + gbc.gridy = 0; + gbc.insets = new Insets(4, 4, 4, 4); + bottomRightPanel.add(getPlotButton(), gbc); + + gbc = new GridBagConstraints(); + gbc.gridx = 5; + gbc.gridy = 0; + gbc.insets = new Insets(4, 4, 4, 4); + bottomRightPanel.add(getDataButton(), gbc); + } + return bottomRightPanel; + } + + protected JLabel getBottomLabel() { + if (bottomLabel == null) { + bottomLabel = new JLabel(); + bottomLabel.setName("BottomLabel"); + bottomLabel.setText(" "); + bottomLabel.setForeground(Color.blue); + bottomLabel.setPreferredSize(new Dimension(44, 20)); + bottomLabel.setMinimumSize(new Dimension(44, 20)); + bottomLabel.setFont(new Font("dialog", Font.PLAIN, 12)); + } + return bottomLabel; + } + + protected JCheckBox getCrosshairCheckBox() { + if (crosshairCheckBox == null) { + crosshairCheckBox = new JCheckBox("Show Crosshair"); + crosshairCheckBox.setName("CrosshairCheckBox"); + crosshairCheckBox.setSelected(true); // default ON + } + return crosshairCheckBox; + } + + protected JLabel getCrosshairCoordLabel() { + if (crosshairCoordLabel == null) { + crosshairCoordLabel = new JLabel(emptyCoordText); + crosshairCoordLabel.setName("CrosshairCoordLabel"); + // no fixed width — dynamic sizing will handle it but we DO want a stable height + int height = crosshairCoordLabel.getFontMetrics(crosshairCoordLabel.getFont()).getHeight(); + crosshairCoordLabel.setPreferredSize(new Dimension(1, height)); + } + return crosshairCoordLabel; + } + + protected JToolBarToggleButton getPlotButton() { + if (plotButton == null) { + plotButton = new JToolBarToggleButton(); + plotButton.setName("PlotButton"); + plotButton.setToolTipText("Show plot(s)"); + plotButton.setText(""); + plotButton.setMaximumSize(new Dimension(28, 28)); + plotButton.setMinimumSize(new Dimension(28, 28)); + plotButton.setPreferredSize(new Dimension(28, 28)); + plotButton.setActionCommand("PlotPanelContainer"); + plotButton.setSelected(true); + plotButton.setIcon(VCellIcons.dataExporterIcon); + } + return plotButton; + } + protected JToolBarToggleButton getDataButton() { + if (dataButton == null) { + dataButton = new JToolBarToggleButton(); + dataButton.setName("DataButton"); + dataButton.setToolTipText("Show data table"); + dataButton.setText(""); + dataButton.setMaximumSize(new Dimension(28, 28)); + dataButton.setMinimumSize(new Dimension(28, 28)); + dataButton.setPreferredSize(new Dimension(28, 28)); + dataButton.setActionCommand("DataPanelContainer"); + dataButton.setIcon(VCellIcons.dataSetsIcon); + } + return dataButton; + } + + protected void initConnections() { + // empty; override in the subclasses + } + + public void setVisualizationBackground(Color color) { + super.setBackground(color); + getBottomRightPanel().setBackground(color); + getBottomLabel().setBackground(color); + getCrosshairCheckBox().setBackground(color); + getCrosshairCoordLabel().setBackground(color); + getLegendPanel().setBackground(color); + getLegendContentPanel().setBackground(color); + getCardPanel().setBackground(color); + getDataPanelContainer().setBackground(color); + getPlotPanelContainer().setBackground(color); + } + + + @Override + protected void onSelectedObjectsChange(Object[] selectedObjects) { + + } + + // crosshair coordinates and coordinates label management + public void updateCrosshairCoordinates(double xVal, double yVal) { + String text = formatCoord(xVal) + ", " + formatCoord(yVal); + getCrosshairCoordLabel().setText(text); + adjustCoordLabelWidth(text); + } + public void clearCrosshairCoordinates() { + getCrosshairCoordLabel().setText(emptyCoordText); + adjustCoordLabelWidth(emptyCoordText); + } + private int lastCoordCharCount = emptyCoordText.length(); + private static final String emptyCoordText = " "; // enough spaces to reduce jitter when switching between no coord and coord + private static final DecimalFormat sci = new DecimalFormat("0.000E0", DecimalFormatSymbols.getInstance(Locale.US)); + private static final DecimalFormat fix = new DecimalFormat("0.000", DecimalFormatSymbols.getInstance(Locale.US)); + private static String formatCoord(double v) { + double av = Math.abs(v); + return (av >= 0.001) + ? fix.format(v) + : sci.format(v); + } + private void adjustCoordLabelWidth(String text) { + int charCount = text.length(); + // Only resize if the number of characters changed + if (charCount == lastCoordCharCount) { + return; + } + lastCoordCharCount = charCount; + FontMetrics fm = getCrosshairCoordLabel().getFontMetrics(getCrosshairCoordLabel().getFont()); + int charWidth = fm.charWidth('8'); // good representative width + int width = charWidth * charCount + 4; // +4 px padding + Dimension d = getCrosshairCoordLabel().getPreferredSize(); + getCrosshairCoordLabel().setPreferredSize(new Dimension(width, d.height)); + getCrosshairCoordLabel().revalidate(); // required so GridBagLayout recalculates layout + } + +} diff --git a/vcell-client/src/main/java/cbit/vcell/solver/ode/gui/ClusterSpecificationPanel.java b/vcell-client/src/main/java/cbit/vcell/solver/ode/gui/ClusterSpecificationPanel.java new file mode 100644 index 0000000000..87c17b801f --- /dev/null +++ b/vcell-client/src/main/java/cbit/vcell/solver/ode/gui/ClusterSpecificationPanel.java @@ -0,0 +1,461 @@ +package cbit.vcell.solver.ode.gui; + +import cbit.vcell.client.data.ODEDataViewer; +import cbit.vcell.client.desktop.biomodel.DocumentEditorSubPanel; +import cbit.vcell.math.ODESolverResultSetColumnDescription; +import cbit.vcell.simdata.LangevinSolverResultSet; +import cbit.vcell.solver.SimulationModelInfo; +import cbit.vcell.solver.ode.ODESolverResultSet; +import cbit.vcell.util.ColumnDescription; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.vcell.util.gui.CollapsiblePanel; + +import javax.swing.*; +import javax.swing.border.EmptyBorder; +import javax.swing.event.ChangeEvent; +import javax.swing.event.ChangeListener; +import javax.swing.event.ListSelectionEvent; +import javax.swing.event.ListSelectionListener; +import java.awt.*; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.beans.PropertyChangeEvent; +import java.beans.PropertyChangeListener; +import java.util.LinkedHashMap; +import java.util.Map; + +public class ClusterSpecificationPanel extends AbstractSpecificationPanel { + + private static final Logger lg = LogManager.getLogger(ClusterSpecificationPanel.class); + + public enum DisplayMode { + COUNTS( + "COUNTS", + "Cluster Counts", + "Show the number of clusters of each size" + ), + MEAN( + "MEAN", + "Cluster Mean", + "At each timepoint: ACS/ACO averaged over runs; SD = SD of ACS across runs" + ), + OVERALL( + "OVERALL", + "Cluster Overall", + "At each timepoint: all runs pooled; ACS, SD, ACO computed from the combined sample" + ); + private final String actionCommand; + private final String uiLabel; + private final String tooltip; + DisplayMode(String actionCommand, String uiLabel, String tooltip) { + this.actionCommand = actionCommand; + this.uiLabel = uiLabel; + this.tooltip = tooltip; + } + public String actionCommand() { return actionCommand; } + public String uiLabel() { return uiLabel; } + public String tooltip() { return tooltip; } + public static DisplayMode fromActionCommand(String cmd) { + for (DisplayMode m : values()) { + if (m.actionCommand.equals(cmd)) { + return m; + } + } + throw new IllegalArgumentException("Unknown DisplayMode: " + cmd); + } + } + + public enum ClusterStatistic { + ACS( + "Avg. Cluster Size", + "Average number of molecules per cluster", + "molecules" + ), + ACO( + "Avg. Cluster Occupancy", + "Average size of the cluster that a molecule belongs to (molecule‑centric cluster size)", + "molecules" + ), + SD( + "SD of Cluster Size", + "Variability of cluster sizes around the average cluster size (ACS)", + "molecules" + ); + private final String fullName; + private final String description; + private final String unit; + ClusterStatistic(String fullName, String description, String unit) { + this.fullName = fullName; + this.description = description; + this.unit = unit; + } + public String fullName() { + return fullName; + } + public String description() { + return description; + } + public String unit() { + return unit; + } + // like valueOf() but returns null instead of throwing exception if not found + public static ClusterStatistic fromString(String s) { + if (s == null) { + return null; + } + for (ClusterStatistic stat : ClusterStatistic.values()) { + if (stat.name().equals(s)) { + return stat; + } + } + return null; + } + } + + public static class ClusterSelection { // used to communicate y-list selection to the ClusterVisualizationPanel + public final DisplayMode mode; + public final java.util.List columns; + public final ODESolverResultSet resultSet; + public ClusterSelection(DisplayMode mode, java.util.List columns, ODESolverResultSet resultSet) { + this.mode = mode; + this.columns = columns; + this.resultSet = resultSet; + } + } + + private static class ClusterYAxisRenderer extends DefaultListCellRenderer { + @Override + public Component getListCellRendererComponent(JList list, Object value, int index, + boolean isSelected, boolean cellHasFocus) { + JLabel label = (JLabel) super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus); + + if (value instanceof ODESolverResultSetColumnDescription cd) { + label.setText(cd.getName()); + if (cd.isTrivial()) { + label.setForeground(Color.GRAY); + } else { + label.setForeground(isSelected + ? list.getSelectionForeground() + : list.getForeground()); + } + DisplayMode mode = (DisplayMode) + ((JComponent) list).getClientProperty("ClusterDisplayMode"); + label.setToolTipText(buildTooltip(cd, mode)); + } + return label; + } + private String buildTooltip(ODESolverResultSetColumnDescription cd, DisplayMode mode) { + if (mode == null) { + return null; + } + String name = cd.getName(); + return switch (mode) { + case COUNTS -> + "Number of clusters of size " + name + + " [molecules]"; + case MEAN, OVERALL -> { + ClusterStatistic stat = ClusterStatistic.valueOf(name); + yield "" + stat.description + + " [" + stat.unit + "]"; + } + }; + } + } + + class IvjEventHandler implements ActionListener, PropertyChangeListener, ListSelectionListener { + @Override + public void actionPerformed(ActionEvent e) { + String cmd = e.getActionCommand(); + if (e.getSource() instanceof JRadioButton rb && SwingUtilities.isDescendingFrom(rb, ClusterSpecificationPanel.this)) { + lg.debug("actionPerformed() called. Source is JRadioButton: {}", rb.getText()); + DisplayMode mode = DisplayMode.fromActionCommand(cmd); + populateYAxisChoices(mode); + } + } + @Override + public void propertyChange(PropertyChangeEvent evt) { + if(evt.getSource() == ClusterSpecificationPanel.this) { + lg.debug("propertyChange() called. Source is ClusterSpecificationPanel. Property name: {}, old value: {}, new value: {}", + evt.getPropertyName(), evt.getOldValue(), evt.getNewValue()); + } + } + @Override + public void valueChanged(ListSelectionEvent e) { + if (e.getSource() == ClusterSpecificationPanel.this.getYAxisChoice()) { + if(suppressEvents || e.getValueIsAdjusting()) { + return; // ignore events triggered during initialization + } + lg.debug("valueChanged() called. Source is YAxisChoice JList. Selected values: {}", getYAxisChoice().getSelectedValuesList()); + enforceAcsSdAcoRule(); + // extract selected ColumnDescriptions + java.util.List selected = getYAxisChoice().getSelectedValuesList(); + DisplayMode mode = getCurrentDisplayMode(); + ODESolverResultSet srs = getResultSetForMode(mode); + // set property to inform the list about current mode (needed for renderer) + yAxisChoiceList.putClientProperty("ClusterDisplayMode", mode); + // fire the event upward + firePropertyChange("ClusterSelection", null, new ClusterSelection(mode, selected, srs)); + } + } + }; + + private final ODEDataViewer owner; + private LangevinSolverResultSet langevinSolverResultSet = null; + private SimulationModelInfo simulationModelInfo = null; + + private final Map yAxisCounts = new LinkedHashMap<>(); + ClusterSpecificationPanel.IvjEventHandler ivjEventHandler = new ClusterSpecificationPanel.IvjEventHandler(); + + private boolean suppressEvents = false; // to prevent event firing during programmatic changes to the UI + + public ClusterSpecificationPanel(ODEDataViewer odeDataViewer) { + super(); + this.owner = odeDataViewer; + getYAxisChoice().setCellRenderer(new ClusterYAxisRenderer()); + initConnections(); + } + + private void initConnections() { + JPanel content = getDisplayOptionsPanel().getContentPanel(); + for (Component c : content.getComponents()) { + if (c instanceof JRadioButton rb) { + rb.addActionListener(ivjEventHandler); + } + } + getYAxisChoice().addListSelectionListener(ivjEventHandler); + getYAxisChoice().setModel(getDefaultListModelY()); + this.addPropertyChangeListener(ivjEventHandler); + } + + // -------------------------------------------------------------- + + private void populateYAxisChoices(DisplayMode mode) { + DefaultListModel model = getDefaultListModelY(); + suppressEvents = true; // prevent firing events while we update the list model + try { + model.clear(); + getYAxisChoice().setEnabled(false); + updateYAxisLabel(mode); + ColumnDescription[] cds = getColumnDescriptionsForMode(mode); + if (cds == null || cds.length <= 1) { + return; + } + for (ColumnDescription cd : cds) { + if (!"t".equals(cd.getName())) { + model.addElement(cd); + } + } + } finally { + suppressEvents = false; + } + if (!model.isEmpty()) { + getYAxisChoice().setEnabled(true); + getYAxisChoice().setSelectedIndex(0); // triggers valueChanged() + } + + } + + private void updateYAxisLabel(DisplayMode mode) { + int count = yAxisCounts.getOrDefault(mode, 0); + String text = "" + YAxisLabelText + "(" + count + " entries)"; + yAxisLabel.setText(text); + } + + private void enforceAcsSdAcoRule() { + DisplayMode mode = getCurrentDisplayMode(); + if (mode != DisplayMode.MEAN && mode != DisplayMode.OVERALL) { + return; // rule applies only in these modes + } + java.util.List selected = getYAxisChoice().getSelectedValuesList(); + boolean acs = contains(selected, "ACS"); + boolean sd = contains(selected, "SD"); + boolean aco = contains(selected, "ACO"); + if (!acs && sd && aco) { + // conflict: SD + ACO without ACS + int anchor = getYAxisChoice().getAnchorSelectionIndex(); + ColumnDescription clicked = (ColumnDescription) getYAxisChoice().getModel().getElementAt(anchor); + String name = clicked.getName(); + if (name.equals("SD")) { + deselect("ACO"); + } else if (name.equals("ACO")) { + deselect("SD"); + } else if (name.equals("ACS")) { + // Ctrl-click on ACS caused conflict -> SD must go + deselect("SD"); + } + } + } + + private boolean contains(java.util.List list, String name) { + for (ColumnDescription cd : list) { + if (cd.getName().equals(name)) return true; + } + return false; + } + + private void deselect(String name) { + DefaultListModel model = getDefaultListModelY(); + int size = model.getSize(); + for (int i = 0; i < size; i++) { + ColumnDescription cd = model.get(i); + if (cd.getName().equals(name)) { + getYAxisChoice().removeSelectionInterval(i, i); + return; + } + } + } + + @Override + protected CollapsiblePanel getDisplayOptionsPanel() { + CollapsiblePanel cp = super.getDisplayOptionsPanel(); + JPanel content = cp.getContentPanel(); + + if (content.getComponentCount() == 0) { // Only populate once + ButtonGroup group = new ButtonGroup(); + + JRadioButton rbCounts = new JRadioButton(DisplayMode.COUNTS.uiLabel()); + JRadioButton rbMean = new JRadioButton(DisplayMode.MEAN.uiLabel()); + JRadioButton rbOverall = new JRadioButton(DisplayMode.OVERALL.uiLabel()); + + rbCounts.setActionCommand(DisplayMode.COUNTS.actionCommand()); + rbMean.setActionCommand(DisplayMode.MEAN.actionCommand()); + rbOverall.setActionCommand(DisplayMode.OVERALL.actionCommand()); + + rbCounts.setToolTipText(DisplayMode.COUNTS.tooltip()); + rbMean.setToolTipText(DisplayMode.MEAN.tooltip()); + rbOverall.setToolTipText(DisplayMode.OVERALL.tooltip()); + + group.add(rbCounts); + group.add(rbMean); + group.add(rbOverall); + + rbCounts.setSelected(true); // select the first option by default, which will populate the y-axis choices + + GridBagConstraints gbc = new GridBagConstraints(); + gbc.gridx = 0; + gbc.anchor = GridBagConstraints.WEST; + gbc.insets = new Insets(2, 4, 2, 4); + gbc.gridy = 0; + content.add(rbCounts, gbc); + gbc.gridy = 1; + content.add(rbMean, gbc); + gbc.gridy = 2; + content.add(rbOverall, gbc); + } + return cp; + } + +// @Override +// protected JList getYAxisChoice() { +// if ((yAxisChoiceList == null)) { +// yAxisChoiceList = new JList(); +// yAxisChoiceList.setName("YAxisChoice"); +// yAxisChoiceList.setBounds(0, 0, 160, 120); +// yAxisChoiceList.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION); +// yAxisChoiceList.setCellRenderer(new DefaultListCellRenderer() { +// @Override +// public Component getListCellRendererComponent(JList list, Object value, int index, +// boolean isSelected, boolean cellHasFocus) { +// JLabel label = (JLabel) super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus); +// +// if (value instanceof ODESolverResultSetColumnDescription cd) { +// String name = cd.getName(); +// label.setText(name); +// +// if (cd.isTrivial()) { // gray out trivial entries +// label.setForeground(Color.GRAY); +// } else { +// label.setForeground(isSelected +// ? list.getSelectionForeground() +// : list.getForeground()); +// } +// +// DisplayMode mode = (DisplayMode) // determine tooltip based on DisplayMode +// ((JComponent) list).getClientProperty("ClusterDisplayMode"); +// if (mode == null) { +// label.setToolTipText(null); +// return label; +// } +// switch (mode) { +// case COUNTS: +// // cluster size X molecules +// label.setText(name); +// label.setToolTipText( +// "Number of Clusters of size: " + name + " " + "" + "[molecules] " +// ); +// break; +// case MEAN: +// case OVERALL: +// ClusterStatistic stat = ClusterStatistic.valueOf(name); +// label.setText(stat.fullName); +// String tooltip = "" + stat.description + "" + " [" + stat.unit + "] "; +// label.setToolTipText(tooltip); +// break; +// } +// } +// return label; +// } +// }); +// } +// return yAxisChoiceList; +// } + + @Override + protected void onSelectedObjectsChange(Object[] selectedObjects) { + lg.debug("onSelectedObjectsChange() called. Number of selected objects: {}", selectedObjects.length); + } + + public void refreshData() { + simulationModelInfo = owner.getSimulationModelInfo(); + langevinSolverResultSet = owner.getLangevinSolverResultSet(); + yAxisCounts.clear(); + if (langevinSolverResultSet != null) { + yAxisCounts.put(DisplayMode.COUNTS, countColumns(langevinSolverResultSet.getClusterCounts())); + yAxisCounts.put(DisplayMode.MEAN, countColumns(langevinSolverResultSet.getClusterMean())); + yAxisCounts.put(DisplayMode.OVERALL, countColumns(langevinSolverResultSet.getClusterOverall())); + } + lg.debug("refreshData() called. SimulationModelInfo: {}, LangevinSolverResultSet: {}, YAxisCounts: {}", + simulationModelInfo, langevinSolverResultSet, yAxisCounts); + + // find the selected radio button inside the collapsible panel and fire event as if it were just selected by mouse click + // which will populate the y-axis choices based on the new data + JPanel content = displayOptionsCollapsiblePanel.getContentPanel(); + for (Component c : content.getComponents()) { + if (c instanceof JRadioButton rb && rb.isSelected()) { + ivjEventHandler.actionPerformed(new ActionEvent(rb, ActionEvent.ACTION_PERFORMED, rb.getActionCommand())); + break; + } + } + } + private int countColumns(ODESolverResultSet srs) { + if (srs == null) return 0; + ColumnDescription[] cds = srs.getColumnDescriptions(); + if (cds == null) return 0; + return cds.length > 1 ? cds.length-1 : 0; // subtract one for time column, but don't return negative if no column at all + } + private ODESolverResultSet getResultSetForMode(DisplayMode mode) { + if (langevinSolverResultSet == null) { + return null; + } + return switch (mode) { + case COUNTS -> langevinSolverResultSet.getClusterCounts(); + case MEAN -> langevinSolverResultSet.getClusterMean(); + case OVERALL-> langevinSolverResultSet.getClusterOverall(); + }; + } + + private ColumnDescription[] getColumnDescriptionsForMode(DisplayMode mode) { + ODESolverResultSet srs = getResultSetForMode(mode); + return (srs == null ? null : srs.getColumnDescriptions()); + } + private DisplayMode getCurrentDisplayMode() { + JPanel content = displayOptionsCollapsiblePanel.getContentPanel(); + for (Component c : content.getComponents()) { + if (c instanceof JRadioButton rb && rb.isSelected()) { + return DisplayMode.valueOf(rb.getActionCommand()); + } + } + return DisplayMode.COUNTS; // default fallback + } +} diff --git a/vcell-client/src/main/java/cbit/vcell/solver/ode/gui/ClusterVisualizationPanel.java b/vcell-client/src/main/java/cbit/vcell/solver/ode/gui/ClusterVisualizationPanel.java new file mode 100644 index 0000000000..1d597935b8 --- /dev/null +++ b/vcell-client/src/main/java/cbit/vcell/solver/ode/gui/ClusterVisualizationPanel.java @@ -0,0 +1,465 @@ +package cbit.vcell.solver.ode.gui; + +import cbit.plot.gui.ClusterDataPanel; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import cbit.plot.gui.ClusterPlotPanel; +import cbit.vcell.client.data.ODEDataViewer; +import cbit.vcell.client.desktop.biomodel.DocumentEditorSubPanel; +import cbit.vcell.parser.ExpressionException; +import cbit.vcell.solver.ode.ODESolverResultSet; +import cbit.vcell.util.ColumnDescription; +import org.vcell.util.ColorUtil; +import org.vcell.util.gui.JToolBarToggleButton; +import org.vcell.util.gui.SpecialtyTableRenderer; +import org.vcell.util.gui.VCellIcons; + +import javax.swing.*; +import javax.swing.border.EmptyBorder; +import javax.swing.event.ChangeEvent; +import javax.swing.event.ChangeListener; +import javax.swing.event.ListSelectionEvent; +import javax.swing.event.ListSelectionListener; +import java.awt.*; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.event.ComponentAdapter; +import java.awt.event.ComponentEvent; +import java.awt.geom.Path2D; +import java.beans.PropertyChangeEvent; +import java.beans.PropertyChangeListener; +import java.text.DecimalFormat; +import java.text.DecimalFormatSymbols; +import java.util.*; +import java.util.List; + + +public class ClusterVisualizationPanel extends AbstractVisualizationPanel { + + private static final Logger lg = LogManager.getLogger(ClusterVisualizationPanel.class); + + ODEDataViewer owner; + IvjEventHandler ivjEventHandler = new IvjEventHandler(); + + private final Map persistentColorMap = new LinkedHashMap<>(); + private final java.util.List globalPalette = new ArrayList<>(); + private int nextColorIndex = 0; + + private ClusterPlotPanel clusterPlotPanel = null; // here are the plots being drawn + private ClusterDataPanel clusterDataPanel = null; // here resides the data table + + class IvjEventHandler implements ActionListener, PropertyChangeListener, ChangeListener, ListSelectionListener { + @Override + public void actionPerformed(ActionEvent e) { + if (e.getSource() instanceof Component c && SwingUtilities.isDescendingFrom(c, ClusterVisualizationPanel.this)) { + String cmd = e.getActionCommand(); + // --- Card switching (plot <-> data) --- + if (cmd.equals("PlotPanelContainer") || cmd.equals("DataPanelContainer")) { + CardLayout cl = (CardLayout) getCardPanel().getLayout(); + cl.show(getCardPanel(), cmd); // show the plot or the data panel + getPlotButton().setSelected(cmd.equals("PlotPanelContainer")); // update button selection state + getDataButton().setSelected(cmd.equals("DataPanelContainer")); + getLegendPanel().setVisible(cmd.equals("PlotPanelContainer")); // show legend only in plot mode + getCrosshairCheckBox().setVisible(cmd.equals("PlotPanelContainer")); // show/hide crosshair checkbox only in plot mode + setCrosshairEnabled(cmd.equals("PlotPanelContainer") && getCrosshairCheckBox().isSelected()); // enable/disable crosshair logic + return; + } + // --- Crosshair checkbox toggled --- + if (e.getSource() == getCrosshairCheckBox()) { + boolean enabled = getCrosshairCheckBox().isSelected(); + setCrosshairEnabled(enabled); + return; + } + } + } + @Override + public void propertyChange(PropertyChangeEvent evt) { + if (evt.getSource() == owner.getClusterSpecificationPanel() && "ClusterSelection".equals(evt.getPropertyName())) { + ClusterSpecificationPanel.ClusterSelection sel = (ClusterSpecificationPanel.ClusterSelection) evt.getNewValue(); + ensureColorsAssigned(sel.columns); + try { + redrawLegend(sel); // redraw legend (one plot, multiple curves) + redrawPlot(sel); // redraw plot (one plot, multiple curves) + redrawDataTable(sel); // redraw data table + } catch (ExpressionException e) { + throw new RuntimeException(e); + } + return; + } + } + @Override + public void stateChanged(ChangeEvent e) { + if (e.getSource() instanceof Component c && SwingUtilities.isDescendingFrom(c, ClusterVisualizationPanel.this)) { + lg.debug("stateChanged() called, source = " + e.getSource().getClass().getSimpleName()); + } + } + @Override + public void valueChanged(ListSelectionEvent e) { + if (e.getSource() instanceof Component c && SwingUtilities.isDescendingFrom(c, ClusterVisualizationPanel.this)) { + lg.debug("valueChanged() called, source = " + e.getSource().getClass().getSimpleName()); + } + } + }; + + public ClusterVisualizationPanel(ODEDataViewer odeDataViewer) { + super(); + this.owner = odeDataViewer; + initialize(); + initConnections(); + setVisualizationBackground(Color.WHITE); + } + + // --------------------the abstract class hooks + @Override + protected JPanel createPlotPanel() { + return getClusterPlotPanel(); + } + @Override + protected JPanel createDataPanel() { + return getClusterDataPanel(); + } + @Override + protected void setCrosshairEnabled(boolean enabled) { + getClusterPlotPanel().setCrosshairEnabled(enabled); + } + + private ClusterPlotPanel getClusterPlotPanel() { // actual plotting is shown here + if (clusterPlotPanel == null) { + try { + clusterPlotPanel = new ClusterPlotPanel(); + clusterPlotPanel.setName("ClusterPlotPanel"); + clusterPlotPanel.setCoordinateCallback(coords -> { + if (coords == null) { + clearCrosshairCoordinates(); + } else { + updateCrosshairCoordinates(coords[0], coords[1]); + } + }); + clusterPlotPanel.addComponentListener(new ComponentAdapter() { + @Override + public void componentShown(ComponentEvent e) { + lg.debug("componentShown() called, height = " + clusterPlotPanel.getHeight()); + } + }); + } catch (java.lang.Throwable ivjExc) { + handleException(ivjExc); + } + } + return clusterPlotPanel; + } + public ClusterDataPanel getClusterDataPanel() { // actual table shown here + if (clusterDataPanel == null) { + try { + clusterDataPanel = new ClusterDataPanel(); + } catch (java.lang.Throwable ivjExc) { + handleException(ivjExc); + } + } + return clusterDataPanel; + } + + + public void setVisualizationBackground(Color color) { + super.setVisualizationBackground(color); + getClusterPlotPanel().setBackground(color); + getClusterDataPanel().setBackground(color); + } + + @Override + protected void initConnections() { + initializeGlobalPalette(); // get a stable, high contrast palette + // group the two buttons so only one stays selected + ButtonGroup bg = new ButtonGroup(); + bg.add(getPlotButton()); + bg.add(getDataButton()); + + // add the shared handler + getPlotButton().addActionListener(ivjEventHandler); + getDataButton().addActionListener(ivjEventHandler); + + // crosshair checkbox is plot-specific, so subclass handles it + getCrosshairCheckBox().addActionListener(ivjEventHandler); + + // listen to the left panel + owner.getClusterSpecificationPanel().addPropertyChangeListener(ivjEventHandler); + } + + + private void handleException(java.lang.Throwable exception) { + lg.error("Uncaught exception in " + this.getClass().getSimpleName(), exception); + System.out.println("--------- UNCAUGHT EXCEPTION ---------"); + exception.printStackTrace(System.out); + } + + @Override + protected void onSelectedObjectsChange(Object[] selectedObjects) { + lg.debug("onSelectedObjectsChange() called with " + selectedObjects.length + " objects"); + } + + public void refreshData() { + if(owner != null && owner.getSimulation() != null) { + int jobs = owner.getSimulation().getSolverTaskDescription().getLangevinSimulationOptions().getTotalNumberOfJobs(); + String name = owner.getSimulation().getName(); + String str = "" + name + " [" + jobs + " job" + (jobs != 1 ? "s" : "") + "]"; + getBottomLabel().setText(str); + } else { + getBottomLabel().setText(" "); + } + // These are not being used here, which is inconsistent with MoleculeVisualizationPanel + // Instead, we receive them indirectly via the ClusterSelection object from the ClusterSpecificationPanel +// simulationModelInfo = owner.getSimulationModelInfo(); +// langevinSolverResultSet = owner.getLangevinSolverResultSet(); + lg.debug("refreshData() called, simulation = " + owner.getSimulation()); + } + // --------------------------------------------------------------------- + + private void initializeGlobalPalette() { + // Use a curated palette from ColorUtil + globalPalette.clear(); + globalPalette.addAll(Arrays.asList(ColorUtil.TABLEAU20)); + + // Reserve ACS and ACO immediately + ensureColorsAssigned("ACS"); + ensureColorsAssigned("ACO"); + + // SD derives from ACS (does NOT consume a palette slot) + Color acsColor = persistentColorMap.get("ACS"); + Color sdColor = deriveEnvelopeColor(acsColor); + persistentColorMap.put("SD", sdColor); + } + private void ensureColorsAssigned(List columns) { + // assign colors only when needed, and keep them consistent across updates + for (ColumnDescription cd : columns) { + String name = cd.getName(); + ensureColorsAssigned(name); + } + } + private void ensureColorsAssigned(String name) { + if (!persistentColorMap.containsKey(name)) { + Color c = globalPalette.get(nextColorIndex % globalPalette.size()); + persistentColorMap.put(name, c); + nextColorIndex++; + } + } + private Color deriveEnvelopeColor(Color base) { + return new Color( + base.getRed(), + base.getGreen(), + base.getBlue(), + 80 // smaller number means lighter color (more transparent) + ); + } + /* + ACS = Average Cluster Size + ACS answers: "If I pick a cluster at random, how many molecules does it contain on average?" + ACS is the mean size of clusters, computed as: ACS = Math.sumOver(i, n_i * size_i) / Math.sumOver(i, n_i); + Unit: molecules per cluster (molecules) + + SD = Standard Deviation of Cluster Size + SD answers: "How much variability is there in cluster sizes?" + SD = Math.sqrt( Math.sumOver(i, n_i * Math.pow(size_i - ACS, 2)) / Math.sumOver(i, n_i) ) + Unit: molecules per cluster (molecules) + + ACO = Average Cluster Occupancy + ACO answers: "If I pick a random molecule from the entire system, how many other molecules are in its cluster on average?" + ACO = Math.sumOver(i, n_i * size_i * size_i) / Math.sumOver(i, n_i * size_i) + Unit: molecules per molecule (molecules) + + Example: if clusters are: + - {5, 5, 5, 5} -> ACS = 5, SD = 0 + - {3, 4, 5, 6} -> ACS = 4.5, SD ≈ 1.12 + - {1, 1, 1, 10} -> ACS = 3.25, SD is large + SD tells whether the system is: + - homogeneous (low SD) + - heterogeneous (high SD) + ACS alone cannot tell that - SD is the variability measure + */ + private JComponent createLegendEntry(String name, Color color, ClusterSpecificationPanel.DisplayMode mode) { + JPanel p = new JPanel(); + p.setName("JPanelClusterColorLegends"); + p.setLayout(new BoxLayout(p, BoxLayout.Y_AXIS)); + p.setOpaque(false); + + String unitSymbol; + String tooltip; + switch (mode) { + case COUNTS: + // name is the cluster size (e.g., "3") + unitSymbol = "molecules"; + tooltip = "cluster size " + name + " molecules"; + break; + case MEAN: + case OVERALL: + // name is ACS / SD / ACO + ClusterSpecificationPanel.ClusterStatistic stat = ClusterSpecificationPanel.ClusterStatistic.valueOf(name); + unitSymbol = stat.unit(); + tooltip = "" + stat.fullName() + "
" + stat.description() + ""; + break; + default: + unitSymbol = ""; + tooltip = null; + } + + // Visible label + String shortLabel = "" + name + "" + " [" + unitSymbol + "] " + ""; + JLabel line = new JLabel(new LineIcon(color)); + JLabel text = new JLabel(shortLabel); + line.setBorder(new EmptyBorder(6, 0, 1, 0)); + text.setBorder(new EmptyBorder(1, 8, 6, 0)); + line.setToolTipText(tooltip); + text.setToolTipText(tooltip); + p.setToolTipText(tooltip); + p.add(line); + p.add(text); + return p; + } + + private void redrawPlot(ClusterSpecificationPanel.ClusterSelection sel) throws ExpressionException { + lg.debug("redrawPlot() called, current selection: " + sel); + + ClusterPlotPanel plot = getClusterPlotPanel(); + plot.clearAllRenderers(); + + if (sel == null || sel.resultSet == null) { + plot.repaint(); + return; + } + List columns = sel.columns; + ODESolverResultSet srs = sel.resultSet; + int indexTime = srs.findColumn("t"); + double[] times = srs.extractColumn(indexTime); + + // --------------------------------------------------------------------- + // FIRST PASS: load all selected columns normally (except SD curve) + // --------------------------------------------------------------------- + Map yMap = new LinkedHashMap<>(); + + double globalMin = 0.0; // old behavior baseline + double globalMax = Double.NEGATIVE_INFINITY; + + for (ColumnDescription cd : columns) { + String name = cd.getName(); + + int idx = srs.findColumn(name); + if (idx < 0) continue; + + double[] y = srs.extractColumn(idx); + yMap.put(name, y); + + // SD does not draw a line, but its raw values still matter for globalMax + if (!name.equals("SD")) { + for (double v : y) { + if (v > globalMax) globalMax = v; + } + } + } + + // --------------------------------------------------------------------- + // SECOND PASS: ACS/SD special logic + // --------------------------------------------------------------------- + boolean acsSelected = yMap.containsKey("ACS"); + boolean sdSelected = yMap.containsKey("SD"); + + double[] acs = null; + double[] sd = null; + + int idxACS = srs.findColumn("ACS"); + int idxSD = srs.findColumn("SD"); + + // We need ACS if either ACS or SD is selected + if (idxACS >= 0) { + acs = (acsSelected ? yMap.get("ACS") : srs.extractColumn(idxACS)); + } + + // We need SD if either ACS or SD is selected + if (idxSD >= 0) { + sd = (sdSelected ? yMap.get("SD") : srs.extractColumn(idxSD)); + } + + // If either ACS or SD is selected, scale using ACS±SD + if ((acsSelected || sdSelected) && acs != null && sd != null) { + for (int i = 0; i < acs.length; i++) { + double upper = acs[i] + sd[i]; + double lower = acs[i] - sd[i]; + + if (upper > globalMax) globalMax = upper; + if (lower < globalMin) globalMin = lower; + } + } + + // --- AVG ----------------------------------------------------- + for (ColumnDescription cd : columns) { + String name = cd.getName(); + if (name.equals("SD")) continue; + double[] y = yMap.get(name); + if (y == null) continue; + Color c = persistentColorMap.get(name); + // Cluster curves are AVG curves in the new API + plot.addAvgRenderer(times, y, c, name, /*statTag*/ "AVG"); + } + + // --- SD ------------------------------------------------- + if (sdSelected && acs != null && sd != null) { + int n = acs.length; + double[] upper = new double[n]; + double[] lower = new double[n]; + for (int i = 0; i < n; i++) { + upper[i] = acs[i] + sd[i]; + lower[i] = acs[i] - sd[i]; + } + Color sdColor = persistentColorMap.get("SD"); + // SD is a band renderer in the new API + plot.addSDRenderer(times, lower, upper, sdColor, "SD", /*statTag*/ "SD"); + } + + // --------------------------------------------------------------------- + // FINALIZE + // --------------------------------------------------------------------- + if (globalMin > 0) globalMin = 0; + plot.setGlobalMinMax(globalMin, globalMax); + if (times.length > 1) { + plot.setDt(times[1]); // times[0] == 0 + } + plot.repaint(); + } + + + private void redrawLegend(ClusterSpecificationPanel.ClusterSelection sel) { + lg.debug("redrawLegend() called, current selection: " + sel); + + getLegendContentPanel().removeAll(); + + for (ColumnDescription cd : sel.columns) { + String name = cd.getName(); + Color c; + if (name.equals("SD")) { + // SD uses a translucent version of ACS color + Color cACS = persistentColorMap.get("ACS"); + c = new Color(cACS.getRed(), cACS.getGreen(), cACS.getBlue(), 80); // alpha 0–255 -> ~30% opacity + } else { + // ACS, ACO, COUNTS all use their assigned colors + c = persistentColorMap.get(name); + } + getLegendContentPanel().add(createLegendEntry(name, c, sel.mode)); + } + getLegendContentPanel().revalidate(); + getLegendContentPanel().repaint(); + } + + private void redrawDataTable(ClusterSpecificationPanel.ClusterSelection sel) throws ExpressionException { + lg.debug("redrawDataTable() called, current selection: " + sel); + getClusterDataPanel().updateData(sel); + } + public void setSpecialityRenderer(SpecialtyTableRenderer str) { + getClusterDataPanel().setSpecialityRenderer(str); + } + + private boolean contains(List list, String name) { + for (ColumnDescription cd : list) { + if (cd.getName().equals(name)) return true; + } + return false; + } + +} diff --git a/vcell-client/src/main/java/cbit/vcell/solver/ode/gui/LangevinClustersResultsPanel.java b/vcell-client/src/main/java/cbit/vcell/solver/ode/gui/LangevinClustersResultsPanel.java new file mode 100644 index 0000000000..d52b29a746 --- /dev/null +++ b/vcell-client/src/main/java/cbit/vcell/solver/ode/gui/LangevinClustersResultsPanel.java @@ -0,0 +1,516 @@ +package cbit.vcell.solver.ode.gui; + +import cbit.plot.gui.Plot2DDataPanel; +import cbit.plot.gui.Plot2DPanel; +import cbit.vcell.client.data.ODEDataViewer; +import cbit.vcell.client.desktop.biomodel.DocumentEditorSubPanel; +import cbit.vcell.simdata.LangevinSolverResultSet; +import cbit.vcell.solver.SimulationModelInfo; +import cbit.vcell.solver.ode.ODESolverResultSet; +import cbit.vcell.util.ColumnDescription; +import org.vcell.util.gui.CollapsiblePanel; +import org.vcell.util.gui.JToolBarToggleButton; +import org.vcell.util.gui.VCellIcons; + +import javax.swing.*; +import javax.swing.border.EmptyBorder; +import javax.swing.event.ChangeEvent; +import javax.swing.event.ChangeListener; +import javax.swing.event.ListSelectionEvent; +import javax.swing.event.ListSelectionListener; +import java.awt.*; +import java.awt.event.ActionEvent; +import java.beans.PropertyChangeEvent; +import java.beans.PropertyChangeListener; + +@Deprecated +public class LangevinClustersResultsPanel extends DocumentEditorSubPanel { + + private final ODEDataViewer owner; + IvjEventHandler ivjEventHandler = new IvjEventHandler(); + + private DefaultListModel defaultListModelY = null; + + private JPanel clusterSpecificationPanel = null; + private JPanel clusterVisualizationPanel = null; + + private CollapsiblePanel displayOptionsCollapsiblePanel = null; + private JScrollPane jScrollPaneYAxis = null; + private JList yAxisChoiceList = null; + + private JPanel ivjJPanel1 = null; + private JPanel ivjJPanelPlot = null; + private JPanel ivjPlot2DPanel1 = null; // here + private JLabel ivjJLabelBottom = null; + private JPanel ivjJPanelData = null; + private JPanel ivjPlot2DDataPanel1 = null; // here + private JPanel bottomRightPanel = null; + private JPanel ivjJPanelLegend = null; + private JScrollPane ivjPlotLegendsScrollPane = null; + private JPanel ivjJPanelPlotLegends = null; + private JLabel bottomLabel = null; + private JToolBarToggleButton ivjPlotButton = null; + private JToolBarToggleButton ivjDataButton = null; + + + private enum DisplayMode { COUNTS, MEAN, OVERALL } + LangevinSolverResultSet langevinSolverResultSet = null; + SimulationModelInfo simulationModelInfo = null; + + + class IvjEventHandler implements java.awt.event.ActionListener, PropertyChangeListener, ChangeListener, ListSelectionListener { + @Override + public void actionPerformed(ActionEvent e) { + System.out.println("LangevinClustersResultsPanel.IvjEventHandler.actionPerformed() called"); + System.out.println(" Source: " + e.getSource() + ", Action Command: " + e.getActionCommand()); + Object source = e.getSource(); + String cmd = e.getActionCommand(); + + if (cmd.equals("JPanelPlot") || cmd.equals("JPanelData")) { + CardLayout cl = (CardLayout) ivjJPanel1.getLayout(); + cl.show(ivjJPanel1, cmd); // show the plot or the data panel + ivjPlotButton.setSelected(cmd.equals("JPanelPlot")); // update button selection state + ivjDataButton.setSelected(cmd.equals("JPanelData")); + ivjJPanelLegend.setVisible(cmd.equals("JPanelPlot")); // show legend only in plot mode + return; + } + + if (source instanceof JRadioButton) { + JRadioButton rb = (JRadioButton) source; + System.out.println(" Source is JRadioButton with text: " + rb.getText()); + switch (cmd) { + case "COUNTS": + populateYAxisChoices(DisplayMode.COUNTS); + break; + case "MEAN": + populateYAxisChoices(DisplayMode.MEAN); + break; + case "OVERALL": + populateYAxisChoices(DisplayMode.OVERALL); + break; + } + } + } + @Override + public void propertyChange(PropertyChangeEvent evt) { + System.out.println("LangevinClustersResultsPanel.IvjEventHandler.propertyChange() called"); + System.out.println(" Source: " + evt.getSource() + ", Property Name: " + evt.getPropertyName() + ", Old Value: " + evt.getOldValue() + ", New Value: " + evt.getNewValue()); + } + @Override + public void stateChanged(ChangeEvent e) { + System.out.println("LangevinClustersResultsPanel.IvjEventHandler.stateChanged() called"); + System.out.println(" Source: " + e.getSource() + ", Class: " + e.getSource().getClass().getName()); + } + + @Override + public void valueChanged(ListSelectionEvent e) { + System.out.println("LangevinClustersResultsPanel.IvjEventHandler.valueChanged() called"); + System.out.println(" Source: " + e.getSource() + ", Class: " + e.getSource().getClass().getName()); + Object source = e.getSource(); + + } + }; + + private void populateYAxisChoices(DisplayMode mode) { + DefaultListModel model = getDefaultListModelY(); + model.clear(); + getYAxisChoice().setEnabled(false); + + ODESolverResultSet srs = null; + ColumnDescription[] cd = null; + + if (langevinSolverResultSet == null) { + model.addElement(""); + return; + } + switch (mode) { + case COUNTS: + // we may never get non-trivial clusters if there's no binding reaction + srs = langevinSolverResultSet.getClusterCounts(); + cd = srs.getColumnDescriptions(); + break; + case MEAN: + srs = langevinSolverResultSet.getClusterMean(); + cd = srs.getColumnDescriptions(); + break; + case OVERALL: + srs = langevinSolverResultSet.getClusterOverall(); + cd = srs.getColumnDescriptions(); + break; + } + if(cd == null || cd.length == 1) { + model.addElement(""); // if only "t" column is present, treat as no data + return; + } + for (ColumnDescription columnDescription : cd) { + if(columnDescription.getName().equals("t")) { + continue; // skip time column + } + model.addElement(columnDescription.getName()); + getYAxisChoice().setEnabled(true); + } + } + + public LangevinClustersResultsPanel(ODEDataViewer odeDataViewer) { + super(); + this.owner = odeDataViewer; + initialize(); + } + + private void initialize() { + System.out.println("LangevinClustersResultsPanel.initialize() called"); + setName("LangevinClustersResultsPanel"); + setLayout(new BorderLayout()); + add(getClusterSpecificationPanel(), BorderLayout.WEST); + add(getClusterVisualizationPanel(), BorderLayout.CENTER); + } + + private JPanel getClusterSpecificationPanel() { + if (clusterSpecificationPanel == null) { + clusterSpecificationPanel = new JPanel(); + clusterSpecificationPanel.setName("ClusterSpecificationPanel"); + clusterSpecificationPanel.setPreferredSize(new Dimension(213, 600)); + clusterSpecificationPanel.setLayout(new java.awt.GridBagLayout()); + clusterSpecificationPanel.setSize(248, 604); + clusterSpecificationPanel.setMinimumSize(new java.awt.Dimension(125, 300)); + + JLabel xAxisLabel = new JLabel("X Axis:"); + java.awt.GridBagConstraints constraintsXAxisLabel = new java.awt.GridBagConstraints(); + constraintsXAxisLabel.anchor = GridBagConstraints.WEST; + constraintsXAxisLabel.gridx = 0; constraintsXAxisLabel.gridy = 0; + constraintsXAxisLabel.insets = new Insets(4, 4, 0, 4); + clusterSpecificationPanel.add(xAxisLabel, constraintsXAxisLabel); + + JTextField xAxisTextBox = new JTextField("t"); + xAxisTextBox.setEnabled(false); + xAxisTextBox.setEditable(false); + GridBagConstraints constraintsXAxisTextBox = new java.awt.GridBagConstraints(); + constraintsXAxisTextBox.gridx = 0; constraintsXAxisTextBox.gridy = 1; + constraintsXAxisTextBox.fill = GridBagConstraints.HORIZONTAL; + constraintsXAxisTextBox.weightx = 1.0; + constraintsXAxisTextBox.insets = new Insets(0, 4, 4, 4); + clusterSpecificationPanel.add(xAxisTextBox, constraintsXAxisTextBox); + + JLabel yAxisLabel = new JLabel("Y Axis:"); + GridBagConstraints gbc_YAxisLabel = new GridBagConstraints(); + gbc_YAxisLabel.anchor = GridBagConstraints.WEST; + gbc_YAxisLabel.insets = new Insets(4, 4, 0, 4); + gbc_YAxisLabel.gridx = 0; gbc_YAxisLabel.gridy = 2; + clusterSpecificationPanel.add(yAxisLabel, gbc_YAxisLabel); + + GridBagConstraints gbc_panel = new GridBagConstraints(); + gbc_panel.fill = GridBagConstraints.BOTH; + gbc_panel.insets = new Insets(4, 4, 5, 4); + gbc_panel.gridx = 0; + gbc_panel.gridy = 3; + clusterSpecificationPanel.add(getDisplayOptionsPanel(), gbc_panel); + + java.awt.GridBagConstraints constraintsJScrollPaneYAxis = new java.awt.GridBagConstraints(); + constraintsJScrollPaneYAxis.gridx = 0; constraintsJScrollPaneYAxis.gridy = 4; + constraintsJScrollPaneYAxis.fill = GridBagConstraints.BOTH; + constraintsJScrollPaneYAxis.weightx = 1.0; + constraintsJScrollPaneYAxis.weighty = 1.0; + constraintsJScrollPaneYAxis.insets = new Insets(0, 4, 4, 4); + clusterSpecificationPanel.add(getJScrollPaneYAxis(), constraintsJScrollPaneYAxis); + + initConnectionsLeft(); + } + return clusterSpecificationPanel; + } + + private CollapsiblePanel getDisplayOptionsPanel() { + if(displayOptionsCollapsiblePanel == null) { + displayOptionsCollapsiblePanel = new CollapsiblePanel("Display Options", true); + JPanel content = displayOptionsCollapsiblePanel.getContentPanel(); + content.setLayout(new GridBagLayout()); + + ButtonGroup group = new ButtonGroup(); + JRadioButton rbCounts = new JRadioButton("Cluster Counts"); + JRadioButton rbMean = new JRadioButton("Cluster Mean"); + JRadioButton rbOverall = new JRadioButton("Cluster Overall"); + + rbCounts.setActionCommand("COUNTS"); + rbMean.setActionCommand("MEAN"); + rbOverall.setActionCommand("OVERALL"); + + rbCounts.addActionListener(ivjEventHandler); + rbMean.addActionListener(ivjEventHandler); + rbOverall.addActionListener(ivjEventHandler); + + group.add(rbCounts); + group.add(rbMean); + group.add(rbOverall); + + rbCounts.setSelected(true); + + GridBagConstraints gbc = new GridBagConstraints(); + gbc.gridx = 0; + gbc.anchor = GridBagConstraints.WEST; + gbc.insets = new Insets(2, 4, 2, 4); + gbc.gridy = 0; + content.add(rbCounts, gbc); + gbc.gridy = 1; + content.add(rbMean, gbc); + gbc.gridy = 2; + content.add(rbOverall, gbc); + } + return displayOptionsCollapsiblePanel; + } + + private JScrollPane getJScrollPaneYAxis() { + if(jScrollPaneYAxis == null) { + jScrollPaneYAxis = new JScrollPane(); + jScrollPaneYAxis.setName("JScrollPaneYAxis"); + jScrollPaneYAxis.setViewportView(getYAxisChoice()); + } + return jScrollPaneYAxis; + } + private JList getYAxisChoice() { + if ((yAxisChoiceList == null)) { + yAxisChoiceList = new JList(); + yAxisChoiceList.setName("YAxisChoice"); + yAxisChoiceList.setBounds(0, 0, 160, 120); + yAxisChoiceList.setSelectionMode(javax.swing.ListSelectionModel.MULTIPLE_INTERVAL_SELECTION); + } + return yAxisChoiceList; + } + + private void initConnectionsLeft() { + + getDisplayOptionsPanel().addPropertyChangeListener(ivjEventHandler); + getYAxisChoice().addListSelectionListener(ivjEventHandler); + this.addPropertyChangeListener(ivjEventHandler); + getYAxisChoice().setModel(getDefaultListModelY()); + + } + private javax.swing.DefaultListModel getDefaultListModelY() { + if (defaultListModelY == null) { + defaultListModelY = new javax.swing.DefaultListModel(); + } + return defaultListModelY; + } + + // ================================================================================================ + + private JPanel getClusterVisualizationPanel() { + if (clusterVisualizationPanel == null) { + clusterVisualizationPanel = new JPanel(); + clusterVisualizationPanel.setName("ClusterVisualizationPanel"); + clusterVisualizationPanel.setPreferredSize(new java.awt.Dimension(420, 400)); + clusterVisualizationPanel.setLayout(new BorderLayout()); + clusterVisualizationPanel.setSize(513, 457); + clusterVisualizationPanel.add(getJPanel1(), "Center"); + clusterVisualizationPanel.add(getBottomRightPanel(), "South"); + clusterVisualizationPanel.add(getJPanelLegend(), "East"); + initConnectionsRight(); + + } + return clusterVisualizationPanel; + } + + private JPanel getJPanel1() { + if (ivjJPanel1 == null) { + ivjJPanel1 = new JPanel(); + ivjJPanel1.setName("JPanel1"); + ivjJPanel1.setLayout(new CardLayout()); + ivjJPanel1.add(getJPanelPlot(), getJPanelPlot().getName()); + ivjJPanel1.add(getJPanelData(), getJPanelData().getName()); + } + return ivjJPanel1; + } + private javax.swing.JPanel getJPanelPlot() { + if (ivjJPanelPlot == null) { + ivjJPanelPlot = new javax.swing.JPanel(); + ivjJPanelPlot.setName("JPanelPlot"); + ivjJPanelPlot.setLayout(new java.awt.BorderLayout()); + ivjJPanelPlot.add(getPlot2DPanel1(), "Center"); + ivjJPanelPlot.add(getJLabelBottom(), "South"); + } + return ivjJPanelPlot; + } + private javax.swing.JPanel getJPanelData() { + if (ivjJPanelData == null) { + ivjJPanelData = new javax.swing.JPanel(); + ivjJPanelData.setName("JPanelData"); + ivjJPanelData.setLayout(new java.awt.BorderLayout()); + ivjJPanelData.add(getPlot2DDataPanel1(), "Center"); + } + return ivjJPanelData; + } + + private javax.swing.JLabel getJLabelBottom() { + if (ivjJLabelBottom == null) { + ivjJLabelBottom = new javax.swing.JLabel(); + ivjJLabelBottom.setName("JLabelBottom"); + ivjJLabelBottom.setText("t"); + ivjJLabelBottom.setForeground(java.awt.Color.black); + ivjJLabelBottom.setHorizontalTextPosition(javax.swing.SwingConstants.CENTER); + ivjJLabelBottom.setHorizontalAlignment(javax.swing.SwingConstants.CENTER); + } + return ivjJLabelBottom; + } + private JPanel getPlot2DPanel1() { + if (ivjPlot2DPanel1 == null) { + try { + ivjPlot2DPanel1 = new JPanel(); + ivjPlot2DPanel1.setName("Plot2DPanel1"); + } catch (java.lang.Throwable ivjExc) { + handleException(ivjExc); + } + } + return ivjPlot2DPanel1; + } + public JPanel getPlot2DDataPanel1() { + if (ivjPlot2DDataPanel1 == null) { + try { + ivjPlot2DDataPanel1 = new JPanel(); + ivjPlot2DDataPanel1.setName("Plot2DDataPanel1"); + } catch (java.lang.Throwable ivjExc) { + handleException(ivjExc); + } + } + return ivjPlot2DDataPanel1; + } + + + // --------------------------------------------------------------------- + private JPanel getBottomRightPanel() { + if (bottomRightPanel == null) { + bottomRightPanel = new JPanel(); + bottomRightPanel.setName("JPanelBottom"); + bottomRightPanel.setLayout(new GridBagLayout()); + + java.awt.GridBagConstraints gbc = new GridBagConstraints(); + gbc.gridx = 1; gbc.gridy = 0; + gbc.fill = GridBagConstraints.HORIZONTAL; + gbc.weightx = 1.0; + gbc.insets = new java.awt.Insets(4, 4, 4, 4); + bottomRightPanel.add(getJBottomLabel(), gbc); + + gbc = new GridBagConstraints(); + gbc.gridx = 2; gbc.gridy = 0; + gbc.insets = new Insets(4, 4, 4, 4); + bottomRightPanel.add(getPlotButton(), gbc); + + gbc = new GridBagConstraints(); + gbc.gridx = 3; gbc.gridy = 0; + gbc.insets = new Insets(4, 4, 4, 4); + bottomRightPanel.add(getDataButton(), gbc); + } + return bottomRightPanel; + } + private JLabel getJBottomLabel() { + if (bottomLabel == null) { + bottomLabel = new JLabel(); + bottomLabel.setName("JBottomLabel"); + bottomLabel.setText(" "); + bottomLabel.setForeground(Color.blue); + bottomLabel.setPreferredSize(new Dimension(44, 20)); + bottomLabel.setFont(new Font("dialog", 0, 12)); + bottomLabel.setMinimumSize(new Dimension(44, 20)); + } + return bottomLabel; + } + private JToolBarToggleButton getPlotButton() { + if (ivjPlotButton == null) { + ivjPlotButton = new JToolBarToggleButton(); + ivjPlotButton.setName("PlotButton"); + ivjPlotButton.setToolTipText("Show plot(s)"); + ivjPlotButton.setText(""); + ivjPlotButton.setMaximumSize(new Dimension(28, 28)); + ivjPlotButton.setActionCommand("JPanelPlot"); + ivjPlotButton.setSelected(true); + ivjPlotButton.setPreferredSize(new Dimension(28, 28)); + ivjPlotButton.setIcon(VCellIcons.dataExporterIcon); + ivjPlotButton.setMinimumSize(new Dimension(28, 28)); + } + return ivjPlotButton; + } + private JToolBarToggleButton getDataButton() { + if (ivjDataButton == null) { + ivjDataButton = new JToolBarToggleButton(); + ivjDataButton.setName("DataButton"); + ivjDataButton.setToolTipText("Show data"); + ivjDataButton.setText(""); + ivjDataButton.setMaximumSize(new Dimension(28, 28)); + ivjDataButton.setActionCommand("JPanelData"); + ivjDataButton.setIcon(VCellIcons.dataSetsIcon); + ivjDataButton.setPreferredSize(new Dimension(28, 28)); + ivjDataButton.setMinimumSize(new Dimension(28, 28)); + } + return ivjDataButton; + } + + private javax.swing.JPanel getJPanelLegend() { + if (ivjJPanelLegend == null) { + ivjJPanelLegend = new javax.swing.JPanel(); + ivjJPanelLegend.setName("JPanelLegend"); + ivjJPanelLegend.setLayout(new java.awt.BorderLayout()); + getJPanelLegend().add(new JLabel(" "), "South"); + getJPanelLegend().add(new JLabel("Plot Legend:"), "North"); + getJPanelLegend().add(getPlotLegendsScrollPane(), "Center"); + } + return ivjJPanelLegend; + } + private JScrollPane getPlotLegendsScrollPane() { + if (ivjPlotLegendsScrollPane == null) { + ivjPlotLegendsScrollPane = new javax.swing.JScrollPane(); + ivjPlotLegendsScrollPane.setName("PlotLegendsScrollPane"); + ivjPlotLegendsScrollPane.setHorizontalScrollBarPolicy(javax.swing.JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED); + ivjPlotLegendsScrollPane.setBorder(new EmptyBorder(0, 0, 0, 0)); + getPlotLegendsScrollPane().setViewportView(getJPanelPlotLegends()); + } + return ivjPlotLegendsScrollPane; + } + private JPanel getJPanelPlotLegends() { + if (ivjJPanelPlotLegends == null) { + ivjJPanelPlotLegends = new javax.swing.JPanel(); + ivjJPanelPlotLegends.setName("JPanelPlotLegends"); + ivjJPanelPlotLegends.setLayout(new BoxLayout(ivjJPanelPlotLegends, javax.swing.BoxLayout.Y_AXIS)); + ivjJPanelPlotLegends.setBounds(0, 0, 72, 360); + } + return ivjJPanelPlotLegends; + } + + + private void initConnectionsRight() { + // Group the two buttons so only one stays selected + ButtonGroup bg = new ButtonGroup(); + bg.add(getPlotButton()); + bg.add(getDataButton()); + + // Add the shared handler + getPlotButton().addActionListener(ivjEventHandler); + getDataButton().addActionListener(ivjEventHandler); + } + + // ================================================================================================= + + private void handleException(java.lang.Throwable exception) { + System.out.println("--------- UNCAUGHT EXCEPTION ---------"); + exception.printStackTrace(System.out); + } + + @Override + protected void onSelectedObjectsChange(Object[] selectedObjects) { + System.out.println("LangevinClustersResultsPanel.onSelectedObjectsChange() called with " + selectedObjects.length + " objects"); + } + + public void refreshData() { + simulationModelInfo = owner.getSimulationModelInfo(); + langevinSolverResultSet = owner.getLangevinSolverResultSet(); + System.out.println("LangevinClustersResultsPanel.refreshData() called"); + + // find the selected radio button inside the collapsible panel + JPanel content = displayOptionsCollapsiblePanel.getContentPanel(); + for (Component c : content.getComponents()) { + if (c instanceof JRadioButton rb && rb.isSelected()) { + ivjEventHandler.actionPerformed(new ActionEvent(rb, ActionEvent.ACTION_PERFORMED, rb.getActionCommand())); + break; + } + } + + } + + +} diff --git a/vcell-client/src/main/java/cbit/vcell/solver/ode/gui/MoleculeSpecificationPanel.java b/vcell-client/src/main/java/cbit/vcell/solver/ode/gui/MoleculeSpecificationPanel.java new file mode 100644 index 0000000000..d608c6141c --- /dev/null +++ b/vcell-client/src/main/java/cbit/vcell/solver/ode/gui/MoleculeSpecificationPanel.java @@ -0,0 +1,428 @@ +package cbit.vcell.solver.ode.gui; + +import cbit.vcell.client.data.ODEDataViewer; +import cbit.vcell.client.desktop.biomodel.DocumentEditorSubPanel; +import cbit.vcell.math.ODESolverResultSetColumnDescription; +import cbit.vcell.simdata.LangevinSolverResultSet; +import cbit.vcell.solver.SimulationModelInfo; +import cbit.vcell.solver.ode.ODESolverResultSet; +import cbit.vcell.util.ColumnDescription; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.vcell.model.ssld.SsldUtils; +import org.vcell.util.gui.CollapsiblePanel; + +import javax.swing.*; +import javax.swing.event.ChangeEvent; +import javax.swing.event.ChangeListener; +import javax.swing.event.ListSelectionEvent; +import javax.swing.event.ListSelectionListener; +import java.awt.Container; +import java.awt.Component; +import java.awt.*; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.beans.PropertyChangeEvent; +import java.beans.PropertyChangeListener; +import java.util.ArrayList; +import java.util.Map; + +public class MoleculeSpecificationPanel extends AbstractSpecificationPanel { + + private static final Logger lg = LogManager.getLogger(MoleculeSpecificationPanel.class); + + public enum DisplayMode { + MOLECULES("MOLECULES", "Molecules", "Display molecule counts over time"), + BOUND_SITES("BOUND_SITES", "Bound Sites", "Display number of bound sites over time"), + FREE_SITES("FREE_SITES", "Free Sites", "Display number of free sites over time"), + TOTAL_SITES("TOTAL_SITES", "Total Sites", "Display total number of sites over time"); + + private final String actionCommand; + private final String uiLabel; + private final String tooltip; + DisplayMode(String actionCommand, String uiLabel, String tooltip) { + this.actionCommand = actionCommand; + this.uiLabel = uiLabel; + this.tooltip = tooltip; + } + public String actionCommand() { return actionCommand; } + public String uiLabel() { return uiLabel; } + public String tooltip() { return tooltip; } + public static boolean isDisplayModeActionCommand(String cmd) { + for (MoleculeSpecificationPanel.DisplayMode m : values()) { + if (m.actionCommand().equals(cmd)) { + return true; + } + } + return false; + } + public static MoleculeSpecificationPanel.DisplayMode fromActionCommand(String cmd) { + for (MoleculeSpecificationPanel.DisplayMode m : values()) { + if (m.actionCommand.equals(cmd)) { + return m; + } + } + throw new IllegalArgumentException("Unknown DisplayMode: " + cmd); + } + } + public enum StatisticSelection { + + AVG("STAT_AVG", "Average", "Display the average value over time"), + MIN_MAX("STAT_MINMAX", "Min / Max", "Display min/max envelope over time"), + SD("STAT_SD", "Standard Deviation", "Display standard deviation around the mean"); + + private final String actionCommand; + private final String uiLabel; + private final String tooltip; + StatisticSelection(String actionCommand, String uiLabel, String tooltip) { + this.actionCommand = actionCommand; + this.uiLabel = uiLabel; + this.tooltip = tooltip; + } + public String actionCommand() { return actionCommand; } + public String uiLabel() { return uiLabel; } + public String tooltip() { return tooltip; } + public static boolean isStatisticSelectionActionCommand(String cmd) { + for (MoleculeSpecificationPanel.StatisticSelection s : values()) { + if (s.actionCommand().equals(cmd)) { + return true; + } + } + return false; + } + public static StatisticSelection fromActionCommand(String cmd) { + for (MoleculeSpecificationPanel.StatisticSelection s : values()) { + if (s.actionCommand.equals(cmd)) { + return s; + } + } + throw new IllegalArgumentException("Unknown StatisticSelection: " + cmd); + } + } + + public static class MoleculeSelection { + public final java.util.List selectedColumns; + public final java.util.List selectedStatistics; + public final java.util.List selectedDisplayModes; + public MoleculeSelection( + java.util.List selectedColumns, + java.util.List selectedStatistics, + java.util.List selectedDisplayModes) { + this.selectedColumns = selectedColumns; + this.selectedStatistics = selectedStatistics; + this.selectedDisplayModes = selectedDisplayModes; + } + } + + private static class MoleculeYAxisRenderer extends DefaultListCellRenderer { + @Override + public Component getListCellRendererComponent(JList list, Object value, int index, + boolean isSelected, boolean cellHasFocus) { + JLabel label = (JLabel) super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus); + + if (value instanceof ODESolverResultSetColumnDescription cd) { + label.setText(cd.getName()); + if (cd.isTrivial()) { + label.setForeground(Color.GRAY); + } else { + // restore normal selection / non-selection colors + label.setForeground(isSelected + ? list.getSelectionForeground() + : list.getForeground()); + } + label.setToolTipText(buildTooltip(cd)); + } + return label; + } + private String buildTooltip(ColumnDescription cd) { + StringBuilder sb = new StringBuilder(""); + sb.append("").append(cd.getName()).append(" "); + if (cd instanceof ODESolverResultSetColumnDescription mcd) { + sb.append("Solver-generated Observable"); + } + sb.append(""); + return sb.toString(); + } + } + + class IvjEventHandler implements ActionListener, PropertyChangeListener, ListSelectionListener { + @Override + public void actionPerformed(ActionEvent e) { + if (e.getSource() instanceof JCheckBox cb && SwingUtilities.isDescendingFrom(cb, MoleculeSpecificationPanel.this)) { + boolean selected = cb.isSelected(); + String cmd = e.getActionCommand(); +// System.out.println(this.getClass().getName() + ".actionPerformed() called with action command: " + e.getActionCommand() + " = " + selected); + lg.debug("actionPerformed() called with action command: {} = {}", cmd, selected); + + if (DisplayMode.isDisplayModeActionCommand(cmd)) { + java.util.List displayModes = getSelectedDisplayModes(); + populateYAxisChoices(displayModes); + } else if (StatisticSelection.isStatisticSelectionActionCommand(cmd)) { + MoleculeSelection sel = new MoleculeSelection( + getYAxisChoice().getSelectedValuesList(), + getSelectedStatistics(), + getSelectedDisplayModes() + ); + lg.debug("MoleculeSelection changed: {} columns, {} statistics, {} display modes", + sel.selectedColumns.size(), sel.selectedStatistics.size(), sel.selectedDisplayModes.size()); + firePropertyChange("MoleculeSelectionChanged", null, sel); + } + } + } + @Override + public void propertyChange(PropertyChangeEvent evt) { + if (evt.getSource() == owner.getMoleculeSpecificationPanel()) { + lg.debug("propertyChange() called with property name: {}", evt.getPropertyName()); + } + } + @Override + public void valueChanged(ListSelectionEvent e) { + if (e.getSource() == MoleculeSpecificationPanel.this.getYAxisChoice()) { + if (supressEvents || e.getValueIsAdjusting()) { + return; + } + lg.debug("valueChanged() called with selected indices: {}", getYAxisChoice().getSelectedIndices()); + MoleculeSelection sel = new MoleculeSelection( + getYAxisChoice().getSelectedValuesList(), + getSelectedStatistics(), + getSelectedDisplayModes() + ); + lg.debug("MoleculeSelection changed: {} columns, {} statistics, {} display modes", + sel.selectedColumns.size(), sel.selectedStatistics.size(), sel.selectedDisplayModes.size()); + firePropertyChange("MoleculeSelectionChanged", null, sel); + } + } + } + MoleculeSpecificationPanel.IvjEventHandler ivjEventHandler = new MoleculeSpecificationPanel.IvjEventHandler(); + + private final ODEDataViewer owner; + private LangevinSolverResultSet langevinSolverResultSet = null; + private SimulationModelInfo simulationModelInfo = null; + + private boolean supressEvents = true; // flag to suppress events during initialization + + public MoleculeSpecificationPanel(ODEDataViewer owner) { + super(); + this.owner = owner; + getYAxisChoice().setCellRenderer(new MoleculeYAxisRenderer()); + initConnections(); + } + + protected void initConnections() { + JPanel content = getDisplayOptionsPanel().getContentPanel(); + for (Component c : content.getComponents()) { + if (c instanceof JCheckBox) { + ((JCheckBox) c).addActionListener(ivjEventHandler); // wire checkbox listeners now that ivjEventHandler is constructed + } + } + getYAxisChoice().addListSelectionListener(ivjEventHandler); + getYAxisChoice().setModel(getDefaultListModelY()); + this.addPropertyChangeListener(ivjEventHandler); + } + + // ----------------------------------------------------------- + + @Override + protected CollapsiblePanel getDisplayOptionsPanel() { + CollapsiblePanel cp = super.getDisplayOptionsPanel(); + JPanel content = cp.getContentPanel(); + + if (content.getComponentCount() == 0) { // Only populate once + GridBagConstraints gbc = new GridBagConstraints(); + gbc.gridx = 0; + gbc.gridy = 0; + gbc.anchor = GridBagConstraints.WEST; + gbc.insets = new Insets(2, 4, 2, 4); + + JCheckBox firstDisplayModeCheckBox = null; // GROUP 1: DisplayMode checkboxes + for (DisplayMode mode : DisplayMode.values()) { + JCheckBox cb = new JCheckBox(mode.uiLabel()); + cb.setActionCommand(mode.actionCommand()); + cb.setToolTipText(mode.tooltip()); + // can't add action listeners here, the event handler object is stil null + // will add them in initConnections() after the whole panel is constructed + if (firstDisplayModeCheckBox == null) { + firstDisplayModeCheckBox = cb; // remember first + } + content.add(cb, gbc); + gbc.gridy++; + } + if (firstDisplayModeCheckBox != null) { // Select default DisplayMode = first enum entry (MOLECULES) + firstDisplayModeCheckBox.setSelected(true); + } + + gbc.insets = new Insets(8, 4, 2, 4); // Spacer + label for statistics group + JLabel statsLabel = new JLabel("Statistics:"); + content.add(statsLabel, gbc); + gbc.gridy++; + gbc.insets = new Insets(2, 16, 2, 4); + + JCheckBox firstStatisticCheckBox = null; // GROUP 2: StatisticSelection checkboxes + for (StatisticSelection stat : StatisticSelection.values()) { + JCheckBox cb = new JCheckBox(stat.uiLabel()); + cb.setActionCommand(stat.actionCommand()); + cb.setToolTipText(stat.tooltip()); + if (firstStatisticCheckBox == null) { + firstStatisticCheckBox = cb; // remember first + } + content.add(cb, gbc); + gbc.gridy++; + } + if (firstStatisticCheckBox != null) { // Select default StatisticSelection = first enum entry (AVG) + firstStatisticCheckBox.setSelected(true); + } + } + return cp; + } + + private void populateYAxisChoices(java.util.List modes) { + JList list = getYAxisChoice(); + DefaultListModel model = getDefaultListModelY(); + // Remember what was selected before we touch the model + java.util.List previouslySelected = list.getSelectedValuesList(); + + // addElement() calls will fire ListDataEvents which will trigger list selection events, which will eventually + // result in multiple calls to firePropertyChange("MoleculeSelectionChanged", ...) and multiple redraws of + // the right panels (plot and data table). To avoid firing these events while we're still populating the model, + // we'll set a flag to suppress events temporarily. The event handler will check this flag and skip firing + // property changes if it's set. + supressEvents = true; + try { + model.clear(); + list.setEnabled(false); + for (DisplayMode mode : modes) { + java.util.List cds = getColumnDescriptionsForMode(mode); + if (cds == null || cds.size() <= 1) { + continue; + } + for (ColumnDescription cd : cds) { + if (!"t".equals(cd.getName())) { + model.addElement(cd); + } + } + } + } finally { + supressEvents = false; // re-enable events after we're done populating the model, even if an exception occurs + } + updateYAxisLabel(model); + if (!model.isEmpty()) { + list.setEnabled(true); + // Re-apply previous selection where possible + if (!previouslySelected.isEmpty()) { + java.util.List indicesToSelect = new ArrayList<>(); + for (int i = 0; i < model.size(); i++) { + ColumnDescription cd = model.get(i); + if (previouslySelected.contains(cd)) { + indicesToSelect.add(i); + } + } + if (!indicesToSelect.isEmpty()) { + int[] idx = indicesToSelect.stream().mapToInt(Integer::intValue).toArray(); + list.setSelectedIndices(idx); + return; // do NOT force-select index 0 + } + } + // Fallback: nothing from previous selection exists anymore + list.setSelectedIndex(0); + } + } + + private java.util.List getColumnDescriptionsForMode(DisplayMode mode) { + java.util.List list = new ArrayList<>(); + if (langevinSolverResultSet == null || langevinSolverResultSet.metadataMap == null) { + return list; + } + for (Map.Entry entry : langevinSolverResultSet.metadataMap.entrySet()) { + String columnName = entry.getKey(); + SsldUtils.LangevinResult lr = entry.getValue(); + if (lr.qualifier == SsldUtils.Qualifier.NONE) { // Qualifier.NONE is illegal for all modes + throw new IllegalArgumentException("Invalid qualifier NONE for column: " + columnName); + } + boolean include = false; + switch (mode) { + case MOLECULES: + include = (lr.site == null || lr.site.isEmpty()); // Molecules = entries with no site + break; + case BOUND_SITES: + include = (!lr.site.isEmpty() && lr.qualifier == SsldUtils.Qualifier.BOUND); + break; + case FREE_SITES: + include = (!lr.site.isEmpty() && lr.qualifier == SsldUtils.Qualifier.FREE); + break; + case TOTAL_SITES: + include = (!lr.site.isEmpty() && lr.qualifier == SsldUtils.Qualifier.TOTAL); + break; + } + if (include) { + ColumnDescription cd = langevinSolverResultSet.getColumnDescriptionByName(columnName); + if (cd != null) { + list.add(cd); + } + } + } + return list; + } + + // ----------------------------------------------------------- + + private void updateYAxisLabel(DefaultListModel model) { + int count = (model == null) ? 0 : model.getSize(); + String text = "" + YAxisLabelText + "(" + count + " entries)"; + yAxisLabel.setText(text); + } + + public java.util.List getSelectedDisplayModes() { + java.util.List list = new ArrayList<>(); + JPanel content = getDisplayOptionsPanel().getContentPanel(); + java.awt.Component[] components = content.getComponents(); + for (Component c : components) { + if (c instanceof JCheckBox cb && cb.isSelected()) { + String actionCommand = cb.getActionCommand(); + if (DisplayMode.isDisplayModeActionCommand(actionCommand)) { + list.add(DisplayMode.fromActionCommand(actionCommand)); + } + } + } + return list; + } + public java.util.List getSelectedStatistics() { + java.util.List list = new ArrayList<>(); + JPanel content = getDisplayOptionsPanel().getContentPanel(); + java.awt.Component[] components = content.getComponents(); + for (Component c : components) { + if (c instanceof JCheckBox cb && cb.isSelected()) { + String actionCommand = cb.getActionCommand(); + if (StatisticSelection.isStatisticSelectionActionCommand(actionCommand)) { + list.add(StatisticSelection.fromActionCommand(actionCommand)); + } + } + } + return list; + } + + @Override + protected void onSelectedObjectsChange(Object[] selectedObjects) { + lg.debug("onSelectedObjectsChange() called with {} objects", selectedObjects.length); + + } + public void refreshData() { + lg.debug("refreshData() called"); + simulationModelInfo = owner.getSimulationModelInfo(); + langevinSolverResultSet = owner.getLangevinSolverResultSet(); + if(displayOptionsCollapsiblePanel == null) { // the whole panel should exist already at this point, + // lazy inilization here may be a bad idea but we'll do it anyway + lg.warn("displayOptionsCollapsiblePanel is null during refreshData()"); + } + JPanel content = getDisplayOptionsPanel().getContentPanel(); + for (Component c : content.getComponents()) { + if (c instanceof JCheckBox cb && cb.isSelected()) { + // we know that during the initial construction of displayOptionsCollapsiblePanel we select the + // first DisplayMode and first StatisticSelection checkboxes, so this code will fire once for sure + // and so we'll properly populate the yAxisChoiceList + ivjEventHandler.actionPerformed(new ActionEvent(cb, ActionEvent.ACTION_PERFORMED, cb.getActionCommand())); + break; + } + } + } + +} diff --git a/vcell-client/src/main/java/cbit/vcell/solver/ode/gui/MoleculeVisualizationPanel.java b/vcell-client/src/main/java/cbit/vcell/solver/ode/gui/MoleculeVisualizationPanel.java new file mode 100644 index 0000000000..22dd6b25fb --- /dev/null +++ b/vcell-client/src/main/java/cbit/vcell/solver/ode/gui/MoleculeVisualizationPanel.java @@ -0,0 +1,452 @@ +package cbit.vcell.solver.ode.gui; + +import cbit.plot.gui.ClusterDataPanel; +import cbit.plot.gui.ClusterPlotPanel; +import cbit.plot.gui.MoleculeDataPanel; +import cbit.plot.gui.MoleculePlotPanel; +import cbit.vcell.client.data.ODEDataViewer; +import cbit.vcell.parser.ExpressionException; +import cbit.vcell.simdata.LangevinSolverResultSet; +import cbit.vcell.solver.OutputTimeSpec; +import cbit.vcell.solver.SimulationModelInfo; +import cbit.vcell.solver.TimeStep; +import cbit.vcell.solver.UniformOutputTimeSpec; +import cbit.vcell.solver.ode.ODESimData; +import cbit.vcell.util.ColumnDescription; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.vcell.util.ColorUtil; +import org.vcell.util.gui.SpecialtyTableRenderer; + +import javax.swing.*; +import javax.swing.border.EmptyBorder; +import javax.swing.event.ListSelectionEvent; +import javax.swing.event.ListSelectionListener; +import java.awt.*; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.event.ComponentAdapter; +import java.awt.event.ComponentEvent; +import java.beans.PropertyChangeEvent; +import java.beans.PropertyChangeListener; +import java.util.*; +import java.util.List; + +public class MoleculeVisualizationPanel extends AbstractVisualizationPanel { + + private static final Logger lg = LogManager.getLogger(MoleculeVisualizationPanel.class); + + private static final int MINMAX_ALPHA = 90; // stronger envelope + private static final int SD_ALPHA = 60; // lighter envelope + + private final ODEDataViewer owner; + LangevinSolverResultSet langevinSolverResultSet = null; + SimulationModelInfo simulationModelInfo = null; + MoleculeVisualizationPanel.IvjEventHandler ivjEventHandler = new MoleculeVisualizationPanel.IvjEventHandler(); + + private final Map persistentColorMap = new LinkedHashMap<>(); + private final java.util.List globalPalette = new ArrayList<>(); + private int nextColorIndex = 0; + + private MoleculePlotPanel moleculePlotPanel = null; // here are the plots being drawn + private MoleculeDataPanel moleculeDataPanel = null; // here resides the data table + + class IvjEventHandler implements ActionListener, PropertyChangeListener, ListSelectionListener { + @Override + public void actionPerformed(ActionEvent e) { + if (e.getSource() instanceof Component c && SwingUtilities.isDescendingFrom(c, MoleculeVisualizationPanel.this)) { +// System.out.println(this.getClass().getName() + ".actionPerformed() called with " + e.getActionCommand()); + lg.debug("actionPerformed() called with command: " + e.getActionCommand()); + // switch selection between plot panel and data panel (located in a JCardLayout) + String cmd = e.getActionCommand(); + // --- Card switching (plot <-> data) --- + if (cmd.equals("PlotPanelContainer") || cmd.equals("DataPanelContainer")) { + CardLayout cl = (CardLayout) getCardPanel().getLayout(); + cl.show(getCardPanel(), cmd); // show the plot or the data panel + getPlotButton().setSelected(cmd.equals("PlotPanelContainer")); // update button selection state + getDataButton().setSelected(cmd.equals("DataPanelContainer")); + getLegendPanel().setVisible(cmd.equals("PlotPanelContainer")); // show legend only in plot mode + getCrosshairCheckBox().setVisible(cmd.equals("PlotPanelContainer")); // show/hide crosshair checkbox only in plot mode + setCrosshairEnabled(cmd.equals("PlotPanelContainer") && getCrosshairCheckBox().isSelected()); // enable/disable crosshair logic + return; + } + // --- Crosshair checkbox toggled --- + if (e.getSource() == getCrosshairCheckBox()) { + boolean enabled = getCrosshairCheckBox().isSelected(); + setCrosshairEnabled(enabled); + return; + } + } + } + @Override + public void propertyChange(PropertyChangeEvent evt) { // listens to changes in the MoleculeSpecificationPanel + if (evt.getSource() == owner.getMoleculeSpecificationPanel() && "MoleculeSelectionChanged".equals(evt.getPropertyName())) { + lg.debug("propertyChange() called with property: " + evt.getPropertyName()); + // redraw everything based on the new selections + MoleculeSpecificationPanel.MoleculeSelection sel = (MoleculeSpecificationPanel.MoleculeSelection) evt.getNewValue(); + ensureColorsAssigned(sel.selectedColumns); + try { + redrawLegend(sel); // redraw legend (one plot, multiple curves) + redrawPlot(sel); // redraw plot (one plot, multiple curves) + redrawDataTable(sel); // redraw data table + } catch (ExpressionException e) { + throw new RuntimeException(e); + } + } + } + @Override + public void valueChanged(ListSelectionEvent e) { + if (e.getSource() instanceof Component c && SwingUtilities.isDescendingFrom(c, MoleculeVisualizationPanel.this)) { + lg.debug("valueChanged() called"); + } + } + } + + public MoleculeVisualizationPanel(ODEDataViewer owner) { + super(); + this.owner = owner; + initialize(); + initConnections(); + setVisualizationBackground(Color.WHITE); + } + + @Override + protected JPanel createPlotPanel() { + return getMoleculePlotPanel(); + } + + @Override + protected JPanel createDataPanel() { + return getMoleculeDataPanel(); + } + + @Override + protected void setCrosshairEnabled(boolean enabled) { + getMoleculePlotPanel().setCrosshairEnabled(enabled); + } + + public void setVisualizationBackground(Color color) { + super.setVisualizationBackground(color); + getMoleculePlotPanel().setBackground(color); + getMoleculeDataPanel().setBackground(color); + } + + private MoleculePlotPanel getMoleculePlotPanel() { // actual plotting is shown here + if (moleculePlotPanel == null) { + try { + moleculePlotPanel = new MoleculePlotPanel(); + moleculePlotPanel.setName("ClusterPlotPanel"); + moleculePlotPanel.setCoordinateCallback(coords -> { + if (coords == null) { + clearCrosshairCoordinates(); + } else { + updateCrosshairCoordinates(coords[0], coords[1]); + } + }); + moleculePlotPanel.addComponentListener(new ComponentAdapter() { + @Override + public void componentShown(ComponentEvent e) { +// System.out.println(this.getClass().getSimpleName() + ".componentShown() called, height = " + moleculePlotPanel.getHeight()); + lg.debug("componentShown() called, height = " + moleculePlotPanel.getHeight()); + } + }); + } catch (java.lang.Throwable ivjExc) { + handleException(ivjExc); + } + } + return moleculePlotPanel; + } + public MoleculeDataPanel getMoleculeDataPanel() { // actual table shown here + if (moleculeDataPanel == null) { + try { + moleculeDataPanel = new MoleculeDataPanel(owner); + } catch (java.lang.Throwable ivjExc) { + handleException(ivjExc); + } + } + return moleculeDataPanel; + } + + // --------------------------------------------------------------- + + protected void initConnections() { + initializeGlobalPalette(); // get a stable, high contrast palette + // group the two buttons so only one stays selected + ButtonGroup bg = new ButtonGroup(); + bg.add(getPlotButton()); + bg.add(getDataButton()); + + // add the shared handler + getPlotButton().addActionListener(ivjEventHandler); + getDataButton().addActionListener(ivjEventHandler); + + // crosshair checkbox is plot-specific, so subclass handles it + getCrosshairCheckBox().addActionListener(ivjEventHandler); + + // listen to the left panel + owner.getMoleculeSpecificationPanel().addPropertyChangeListener(ivjEventHandler); + } + + // ---------------------------------------------------------------------- + + private void redrawPlot(MoleculeSpecificationPanel.MoleculeSelection sel) throws ExpressionException { + lg.debug("redrawPlot() called, current selection: " + sel); + + MoleculePlotPanel plot = getMoleculePlotPanel(); + plot.clearAllRenderers(); + if (sel == null || langevinSolverResultSet == null || !langevinSolverResultSet.isAverageDataAvailable()) { + plot.repaint(); + return; + } + + ODESimData avgData = langevinSolverResultSet.getAvg(); + ODESimData minData = langevinSolverResultSet.getMin(); + ODESimData maxData = langevinSolverResultSet.getMax(); + ODESimData stdData = langevinSolverResultSet.getStd(); + + int indexTime = avgData.findColumn("t"); + double[] time = avgData.extractColumn(indexTime); + // time may have suffered systematic numerical precision issues, so we reconstruct it based on the + // uniform output time step and the number of rows in the data + OutputTimeSpec outputTimeSpec = owner.getSimulation().getSolverTaskDescription().getOutputTimeSpec(); + double dt = ((UniformOutputTimeSpec)outputTimeSpec).getOutputTimeStep(); // uniform output time step + double endingTime = owner.getSimulation().getSolverTaskDescription().getTimeBounds().getEndingTime(); + for (int i = 0; i < time.length; i++) { + time[i] = i * dt; + } + + // ------------------------------------------------------------ + // FIRST PASS: collect all Y arrays and compute global min/max + // ------------------------------------------------------------ + double globalMin = 0.0; // same baseline as clusters + double globalMax = Double.NEGATIVE_INFINITY; + + // Store arrays so we don’t re-extract later + class Series { + double[] avg; + double[] min; + double[] max; + double[] sd; + } + Map map = new LinkedHashMap<>(); + + for (ColumnDescription cd : sel.selectedColumns) { + String name = cd.getName(); + Series s = new Series(); + + s.avg = LangevinSolverResultSet.getSeries(avgData, name); + s.min = LangevinSolverResultSet.getSeries(minData, name); + s.max = LangevinSolverResultSet.getSeries(maxData, name); + s.sd = LangevinSolverResultSet.getSeries(stdData, name); + + map.put(name, s); + + if (s.avg != null) { // AVG contributes to globalMax + for (double v : s.avg) { + if (v > globalMax) globalMax = v; + } + } + if (s.min != null) { // MIN/MAX contributes to globalMin/globalMax + for (double v : s.min) { + if (v < globalMin) globalMin = v; + } + } + if (s.max != null) { + for (double v : s.max) { + if (v > globalMax) globalMax = v; + } + } + if (s.avg != null && s.sd != null) { // SD envelope contributes to globalMin/globalMax + for (int i = 0; i < s.avg.length; i++) { + double upper = s.avg[i] + s.sd[i]; + double lower = s.avg[i] - s.sd[i]; + if (upper > globalMax) globalMax = upper; + if (lower < globalMin) globalMin = lower; + } + } + } + + // ------------------------------------------------------------ + // SECOND PASS: add renderers in correct order + // SD → MINMAX → AVG + // ------------------------------------------------------------ + for (ColumnDescription cd : sel.selectedColumns) { + String name = cd.getName(); + Series s = map.get(name); + + Color baseColor = persistentColorMap.get(name); + Color mMColor = deriveMinMaxColor(baseColor); + Color sDColor = deriveSDColor(baseColor); + + // --- SD band --- + if (sel.selectedStatistics.contains(MoleculeSpecificationPanel.StatisticSelection.SD) + && s.avg != null && s.sd != null) { + int n = s.avg.length; + double[] low = new double[n]; + double[] high = new double[n]; + for (int i = 0; i < n; i++) { + low[i] = s.avg[i] - s.sd[i]; + high[i] = s.avg[i] + s.sd[i]; + } + plot.addSDRenderer(time, low, high, sDColor, name, MoleculeSpecificationPanel.StatisticSelection.SD); + } + + // --- MIN/MAX band --- + if (sel.selectedStatistics.contains(MoleculeSpecificationPanel.StatisticSelection.MIN_MAX) + && s.min != null && s.max != null) { + plot.addMinMaxRenderer(time, s.min, s.max, mMColor, name, MoleculeSpecificationPanel.StatisticSelection.MIN_MAX); + } + + // --- AVG line --- + if (sel.selectedStatistics.contains(MoleculeSpecificationPanel.StatisticSelection.AVG) + && s.avg != null) { + plot.addAvgRenderer(time, s.avg, baseColor, name, MoleculeSpecificationPanel.StatisticSelection.AVG); + } + } + + // ------------------------------------------------------------ + // FINALIZE + // ------------------------------------------------------------ + if (globalMin > 0) globalMin = 0; + plot.setGlobalMinMax(globalMin, globalMax); + if (time.length > 1) { + plot.setDt(time[1] - time[0]); // times[0] == 0 + } + plot.repaint(); + } + + public Color deriveMinMaxColor(Color base) { + return new Color( + base.getRed(), + base.getGreen(), + base.getBlue(), + MINMAX_ALPHA + ); + } + + public Color deriveSDColor(Color base) { + return new Color( + base.getRed(), + base.getGreen(), + base.getBlue(), + SD_ALPHA + ); + } + + private void redrawLegend(MoleculeSpecificationPanel.MoleculeSelection sel) throws ExpressionException { + lg.debug("redrawLegend() called with selection: " + sel); + JPanel legend = getLegendContentPanel(); + legend.removeAll(); + for (ColumnDescription cd : sel.selectedColumns) { + String name = cd.getName(); + Color c = persistentColorMap.get(name); // // assigned color for this molecule + if (c == null) { + // fallback or auto-assign if needed + c = Color.GRAY; + } + legend.add(createLegendEntry(name, c)); + } + legend.revalidate(); + legend.repaint(); + } + private JComponent createLegendEntry(String name, Color color) { + JPanel p = new JPanel(); + p.setLayout(new BoxLayout(p, BoxLayout.Y_AXIS)); + p.setOpaque(false); + + String unitSymbol = "molecules"; + String tooltip = "" + name + "
" + unitSymbol + ""; + JLabel line = new JLabel(new LineIcon(color)); + +// JLabel text = new JLabel("" + name + " [" + unitSymbol + "]"); + JLabel text = new JLabel("" + name + " "); + line.setBorder(new EmptyBorder(6, 0, 1, 0)); + text.setBorder(new EmptyBorder(1, 8, 6, 0)); + line.setToolTipText(tooltip); + text.setToolTipText(tooltip); + p.setToolTipText(tooltip); + p.add(line); + p.add(text); + return p; + } + + private void redrawDataTable(MoleculeSpecificationPanel.MoleculeSelection sel) throws ExpressionException { + lg.debug("redrawDataTable() called with selection: " + sel); + getMoleculeDataPanel().updateData(sel, langevinSolverResultSet); + } + + private void handleException(java.lang.Throwable exception) { + lg.error("Uncaught exception: " + exception.getMessage(), exception); + System.out.println("--------- UNCAUGHT EXCEPTION ---------"); + exception.printStackTrace(System.out); + } + + @Override + protected void onSelectedObjectsChange(Object[] selectedObjects) { + lg.debug("onSelectedObjectsChange() called with " + selectedObjects.length + " objects"); + } + + // called directly from ODEDataViewer when a new value for vcDataIdentifier is set + // TODO: this is a little unreliable since it depends on the order of refreshData() calls in ODEDataViewer + // the right way to do it would be to add the simulationModelInfo and langevinSolverResultSet to MoleculeSelection object, + // the way it's done in ClusterVisualizationPanel, so that they are always in sync with the selections in the left panel + public void refreshData() { + lg.debug("refreshData() called"); + simulationModelInfo = owner.getSimulationModelInfo(); + langevinSolverResultSet = owner.getLangevinSolverResultSet(); + + if(owner != null && owner.getSimulation() != null) { + int jobs = owner.getSimulation().getSolverTaskDescription().getLangevinSimulationOptions().getTotalNumberOfJobs(); + String name = owner.getSimulation().getName(); + String str = "" + name + " [" + jobs + " job" + (jobs != 1 ? "s" : "") + "]"; + getBottomLabel().setText(str); + } else { + getBottomLabel().setText(" "); + } + } + + // ------------------------------------------------------------------------------- + + private void initializeGlobalPalette() { + // Use a curated palette from ColorUtil + globalPalette.clear(); + globalPalette.addAll(Arrays.asList(ColorUtil.TABLEAU20)); + + // Reserve ACS and ACO immediately + ensureColorsAssigned("ACS"); + ensureColorsAssigned("ACO"); + + // SD derives from ACS (does NOT consume a palette slot) + Color acsColor = persistentColorMap.get("ACS"); + Color sdColor = deriveEnvelopeColor(acsColor); + persistentColorMap.put("SD", sdColor); + } + private void ensureColorsAssigned(List columns) { + // assign colors only when needed, and keep them consistent across updates + for (ColumnDescription cd : columns) { + String name = cd.getName(); + ensureColorsAssigned(name); + } + } + private void ensureColorsAssigned(String name) { + if (!persistentColorMap.containsKey(name)) { + Color c = globalPalette.get(nextColorIndex % globalPalette.size()); + persistentColorMap.put(name, c); + nextColorIndex++; + } + } + private Color deriveEnvelopeColor(Color base) { + return new Color( + base.getRed(), + base.getGreen(), + base.getBlue(), + 80 // smaller number means lighter color (more transparent) + ); + } + + public void setSpecialityRenderer(SpecialtyTableRenderer str) { +// getMoleculeDataPanel().setSpecialityRenderer(str); + } + + +} diff --git a/vcell-client/src/main/java/cbit/vcell/solver/ode/gui/OutputSpeciesResultsPanel.java b/vcell-client/src/main/java/cbit/vcell/solver/ode/gui/OutputSpeciesResultsPanel.java index b9ddf0bbf4..c564a2f9eb 100644 --- a/vcell-client/src/main/java/cbit/vcell/solver/ode/gui/OutputSpeciesResultsPanel.java +++ b/vcell-client/src/main/java/cbit/vcell/solver/ode/gui/OutputSpeciesResultsPanel.java @@ -72,7 +72,6 @@ import org.vcell.model.rbm.RbmUtils; import org.vcell.model.rbm.SpeciesPattern; import org.vcell.model.rbm.RbmUtils.BnglObjectConstructionVisitor; -import org.vcell.model.rbm.gui.GeneratedSpeciesTableRow; import org.vcell.solver.nfsim.NFSimMolecularConfigurations; import org.vcell.solver.nfsim.NFSimSolver; import org.vcell.util.Pair; @@ -134,7 +133,7 @@ public class OutputSpeciesResultsPanel extends DocumentEditorSubPanel { private SpeciesPatternLargeShape spls; // =================================================================================================== - private class OutputSpeciesResultsTableModel extends VCellSortTableModel + private class OutputSpeciesResultsTableModel extends VCellSortTableModel implements PropertyChangeListener, AutoCompleteTableModel { public final int colCount = 4; @@ -147,7 +146,7 @@ private class OutputSpeciesResultsTableModel extends VCellSortTableModel allGeneratedSpeciesList; + private ArrayList allGeneratedSpeciesList; public OutputSpeciesResultsTableModel() { super(table, new String[] {"Count", "Structure", "Depiction", "BioNetGen Definition"}); @@ -168,7 +167,7 @@ public Class getColumnClass(int iCol) { return Object.class; } - public ArrayList getTableRows() { + public ArrayList getTableRows() { return allGeneratedSpeciesList; } @@ -186,7 +185,7 @@ public void setValueAt(Object valueNew, int iRow, int iCol) { } @Override public Object getValueAt(int iRow, int iCol) { - GeneratedSpeciesTableRow speciesTableRow = getValueAt(iRow); + LocalGeneratedSpeciesTableRow speciesTableRow = getValueAt(iRow); switch(iCol) { case iColCount: return speciesTableRow.count; @@ -227,10 +226,10 @@ public Set getAutoCompletionWords(int row, int column) { public void propertyChange(PropertyChangeEvent arg0) { } @Override - protected Comparator getComparator(int col, boolean ascending) { + protected Comparator getComparator(int col, boolean ascending) { final int scale = ascending ? 1 : -1; - return new Comparator() { - public int compare(GeneratedSpeciesTableRow o1, GeneratedSpeciesTableRow o2) { + return new Comparator() { + public int compare(LocalGeneratedSpeciesTableRow o1, LocalGeneratedSpeciesTableRow o2) { switch (col) { case iColCount: return scale * o1.count.compareTo(o2.count); @@ -299,7 +298,7 @@ public void refreshData() { Map molecularConfigurations = nfsmc.getMolecularConfigurations(); for(String pattern : molecularConfigurations.keySet()) { Integer number = molecularConfigurations.get(pattern); - GeneratedSpeciesTableRow newRow = createTableRow(pattern, number); + LocalGeneratedSpeciesTableRow newRow = createTableRow(pattern, number); allGeneratedSpeciesList.add(newRow); } } else { @@ -310,12 +309,12 @@ public void refreshData() { System.out.println("Simulation: " + sim.getName()); // apply text search function for particular columns - List speciesObjectList = new ArrayList<>(); + List speciesObjectList = new ArrayList<>(); if (searchText == null || searchText.length() == 0) { speciesObjectList.addAll(allGeneratedSpeciesList); } else { String lowerCaseSearchText = searchText.toLowerCase(); - for (GeneratedSpeciesTableRow rs : allGeneratedSpeciesList) { + for (LocalGeneratedSpeciesTableRow rs : allGeneratedSpeciesList) { if (rs.expression.toLowerCase().contains(lowerCaseSearchText) ) { speciesObjectList.add(rs); // search for pure expression continue; // found already, no point to keep searching for another match for this row @@ -352,7 +351,7 @@ public void refreshData() { } // end OutputSpeciesResultsTableModel class // ====================================================================================== - private class GeneratedSpeciesTableRow { + private class LocalGeneratedSpeciesTableRow { private Integer count = 0; // used only in relation to an observable private String expression = "?"; private String structure = "?"; @@ -360,7 +359,7 @@ private class GeneratedSpeciesTableRow { // never call this directly, the object is not fully constructed // always call createTableRow() instead - protected GeneratedSpeciesTableRow(Integer count) { + protected LocalGeneratedSpeciesTableRow(Integer count) { if(count != null) { this.count = count; } @@ -412,7 +411,7 @@ private void deriveSpecies(String originalExpression) { species = null; } } - } // end GeneratedSpeciesTableRow class + } // end LocalGeneratedSpeciesTableRow class // ====================================================================================== private class EventHandler implements ActionListener, DocumentListener, ListSelectionListener, TableModelListener { @@ -476,8 +475,8 @@ public OutputSpeciesResultsPanel(ODEDataViewer owner) { initialize(); } -private GeneratedSpeciesTableRow createTableRow(String expression, Integer count) { - GeneratedSpeciesTableRow row = new GeneratedSpeciesTableRow(count); +private LocalGeneratedSpeciesTableRow createTableRow(String expression, Integer count) { + LocalGeneratedSpeciesTableRow row = new LocalGeneratedSpeciesTableRow(count); try { row.deriveSpecies(expression); }catch (Exception e) { @@ -488,7 +487,7 @@ private GeneratedSpeciesTableRow createTableRow(String expression, Integer count } -public ArrayList getTableRows() { +public ArrayList getTableRows() { return tableModel.getTableRows(); } @@ -726,8 +725,8 @@ public Component getTableCellRendererComponent(JTable table, Object value, selectedObject = tableModel.getValueAt(row); } if (selectedObject != null) { - if(selectedObject instanceof GeneratedSpeciesTableRow) { - SpeciesContext sc = ((GeneratedSpeciesTableRow)selectedObject).species; + if(selectedObject instanceof LocalGeneratedSpeciesTableRow) { + SpeciesContext sc = ((LocalGeneratedSpeciesTableRow)selectedObject).species; if(sc == null || sc.getSpeciesPattern() == null) { spss = null; } else { @@ -764,8 +763,8 @@ public void paintComponent(Graphics g) { } public void updateShape(int selectedRow) { - - GeneratedSpeciesTableRow speciesTableRow = tableModel.getValueAt(selectedRow); + + LocalGeneratedSpeciesTableRow speciesTableRow = tableModel.getValueAt(selectedRow); SpeciesContext sc = speciesTableRow.species; if(sc == null || sc.getSpeciesPattern() == null) { spls = new SpeciesPatternLargeShape(20, 20, -1, shapePanel, true, issueManager); // error (red circle) diff --git a/vcell-core/src/main/java/cbit/vcell/simdata/LangevinSolverResultSet.java b/vcell-core/src/main/java/cbit/vcell/simdata/LangevinSolverResultSet.java index edf1b5a234..ec0ac99a0f 100644 --- a/vcell-core/src/main/java/cbit/vcell/simdata/LangevinSolverResultSet.java +++ b/vcell-core/src/main/java/cbit/vcell/simdata/LangevinSolverResultSet.java @@ -1,16 +1,31 @@ package cbit.vcell.simdata; +import cbit.vcell.math.ODESolverResultSetColumnDescription; +import cbit.vcell.parser.ExpressionException; +import cbit.vcell.solver.DataSymbolMetadata; +import cbit.vcell.solver.SimulationModelInfo; import cbit.vcell.solver.ode.ODESimData; +import cbit.vcell.units.VCUnitDefinition; +import cbit.vcell.util.ColumnDescription; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.vcell.model.ssld.SsldUtils; import java.io.*; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.Map; public class LangevinSolverResultSet implements Serializable { - private final LangevinBatchResultSet raw; + private static final Logger lg = LogManager.getLogger(LangevinSolverResultSet.class); + private final LangevinBatchResultSet raw; public LangevinSolverResultSet(LangevinBatchResultSet raw) { this.raw = raw; } + public final Map metadataMap = new LinkedHashMap<>(); + // // safe getter that returns a deep copy, but I don't think we need it // public LangevinBatchResultSet getLangevinBatchResultSetSafe() { @@ -40,6 +55,15 @@ public ODESimData getClusterOverall() { return raw == null ? null : raw.getOdeSimDataClusterOverall(); } + public ColumnDescription getColumnDescriptionByName(String columnName) { + if(raw == null || raw.getOdeSimDataAvg() == null) { + return null; + } + int index = raw.getOdeSimDataAvg().findColumn(columnName); + ColumnDescription cd = raw.getOdeSimDataAvg().getColumnDescriptions(index); + return cd; + } + // helper functions public boolean isAverageDataAvailable() { return getAvg() != null && @@ -71,4 +95,73 @@ private static LangevinBatchResultSet deepCopy(LangevinBatchResultSet original) } } + public void postProcess() { + if(isClusterDataAvailable()) { + ODESimData co = getClusterOverall(); + checkTrivial(co); + co = getClusterMean(); + checkTrivial(co); + co = getClusterCounts(); + checkTrivial(co); + } + if(isAverageDataAvailable()) { + ODESimData co = getAvg(); + populateMetadata(co); + checkTrivial(co); + co = getMin(); + checkTrivial(co); + co = getMax(); + checkTrivial(co); + co = getStd(); + checkTrivial(co); + } + } + private void populateMetadata(ODESimData co) { + // from solver-generated observable (ColumnDescription.name) extract molecule, site and state names + ColumnDescription[] cds = co.getColumnDescriptions(); + for(ColumnDescription cd : cds) { + String columnName = cd.getName(); + SimulationModelInfo.ModelCategoryType filterCategory = null; // parse name to find the filter category + SsldUtils.LangevinResult lr = SsldUtils.LangevinResult.fromString(columnName); + if(lr.qualifier.equals(SsldUtils.Qualifier.NONE)) { + lg.warn("Ignoring LangevinResult token: " + columnName + ", qualifier missing"); + continue; + } + metadataMap.put(columnName, lr); + } + } + private static void checkTrivial(ODESimData co) { + ColumnDescription[] cds = co.getColumnDescriptions(); + for(ColumnDescription columnDescription : cds) { + if (columnDescription instanceof ODESolverResultSetColumnDescription cd) { + double[] data = null; + int index = co.findColumn(cd.getName()); + try { + data = co.extractColumn(index); + } catch (ExpressionException e) { + lg.warn("Failed to extract column: " + e.getMessage()); + continue; + } + if(data == null || data.length == 0) { + continue; + } + double initial = data[0]; + boolean isTrivial = true; + for(double d : data) { + if(initial != d) { + isTrivial = false; + break; // one mismatch is enough to know it's not trivial + } + } + cd.setIsTrivial(isTrivial); + } + } + } + + public static double[] getSeries(ODESimData data, String columnName) throws ExpressionException { + int idx = data.findColumn(columnName); + if (idx < 0) return null; + return data.extractColumn(idx); + } + } diff --git a/vcell-core/src/main/java/cbit/vcell/simdata/ODEDataManager.java b/vcell-core/src/main/java/cbit/vcell/simdata/ODEDataManager.java index 418d034b5c..9c853b1234 100644 --- a/vcell-core/src/main/java/cbit/vcell/simdata/ODEDataManager.java +++ b/vcell-core/src/main/java/cbit/vcell/simdata/ODEDataManager.java @@ -137,6 +137,9 @@ public ODESolverResultSet getODESolverResultSet() throws DataAccessException { public NFSimMolecularConfigurations getNFSimMolecularConfigurations() throws DataAccessException { return nFSimMolecularConfigurations; } +public LangevinSolverResultSet getLangevinSolverResultSet() throws DataAccessException { + return langevinSolverResultSet; +} /** * Gets the simulationInfo property (cbit.vcell.solver.SimulationInfo) value. @@ -182,7 +185,10 @@ private void connect() throws DataAccessException { nFSimMolecularConfigurations = getVCDataManager().getNFSimMolecularConfigurations(getVCDataIdentifier()); LangevinBatchResultSet raw = getVCDataManager().getLangevinBatchResultSet(getVCDataIdentifier()); langevinSolverResultSet = new LangevinSolverResultSet(raw); // may be null - if( langevinSolverResultSet.isAverageDataAvailable()) { + if(langevinSolverResultSet != null) { + langevinSolverResultSet.postProcess(); + } + if( langevinSolverResultSet != null && langevinSolverResultSet.isAverageDataAvailable()) { odeSolverResultSet = langevinSolverResultSet.getAvg(); } } diff --git a/vcell-core/src/main/java/org/vcell/model/ssld/SsldUtils.java b/vcell-core/src/main/java/org/vcell/model/ssld/SsldUtils.java index 6794b2a6ae..0e67ea7990 100644 --- a/vcell-core/src/main/java/org/vcell/model/ssld/SsldUtils.java +++ b/vcell-core/src/main/java/org/vcell/model/ssld/SsldUtils.java @@ -28,6 +28,8 @@ public class SsldUtils { + private static final Logger lg = LogManager.getLogger(SsldUtils.class); + public enum Qualifier { FREE, BOUND, TOTAL, NONE; public static Qualifier fromPrefix(String s) { @@ -40,8 +42,6 @@ public static Qualifier fromPrefix(String s) { } } - private static Logger lg = LogManager.getLogger(SsldUtils.class); - // result entry from langevin output public static class LangevinResult { public final Qualifier qualifier; @@ -56,34 +56,27 @@ public static LangevinResult fromString(String str) { // Step 1: split only on DOUBLE underscore String[] parts = str.split("__"); - String first = parts[0]; // Step 2: find first underscore int idx = first.indexOf('_'); - Qualifier qualifier = Qualifier.NONE; String molecule; - if (idx > 0) { String prefix = first.substring(0, idx); qualifier = Qualifier.fromPrefix(prefix); - if (qualifier != Qualifier.NONE) { - // Valid qualifier found + // valid qualifier found molecule = first.substring(idx + 1); } else { - // Not a valid qualifier → whole thing is molecule - molecule = first; + molecule = first; // not a valid qualifier -> whole thing is molecule } } else { - // No underscore at all → no qualifier - molecule = first; + molecule = first; // no underscore at all -> no qualifier } String site = ""; String state = ""; - if (parts.length >= 2) { site = parts[1]; } @@ -93,7 +86,6 @@ public static LangevinResult fromString(String str) { if (parts.length > 3) { throw new RuntimeException("Too many '__' sections in: " + str); } - return new LangevinResult(qualifier, molecule, site, state); } @@ -1075,7 +1067,6 @@ private void adjustTransitionReactionSites(TransitionReaction ssldReaction, Mapp // the other end of the bond (the molecule and site of the transition product) } else { transitionMoleculeIndexInSpeciesPattern = 0; - mtpConditionReactant = null; mcpConditionReactant = null; mtpConditionProduct = null; @@ -1104,7 +1095,6 @@ private void adjustTransitionReactionSites(TransitionReaction ssldReaction, Mapp mcpConditionReactant.setBond(SpeciesPattern.generateBond(mtpTransitionReactant, mcpTransitionReactant)); mcpTransitionReactant.setBond(SpeciesPattern.generateBond(mtpConditionReactant, mcpConditionReactant)); } - SpeciesPattern spProduct = m.getProductPatternOneFromMolecule(ssldTransitionMolecule).getSpeciesPattern(); MolecularTypePattern mtpTransitionProduct = spProduct.getMolecularTypePatterns(ssldTransitionMolecule.getName()).get(transitionMoleculeIndexInSpeciesPattern); MolecularComponentPattern mcpTransitionProduct = mtpTransitionProduct.getMolecularComponentPattern(ssldSiteType.getName()); @@ -1112,7 +1102,8 @@ private void adjustTransitionReactionSites(TransitionReaction ssldReaction, Mapp ComponentStateDefinition csdFinal = mcpTransitionProduct.getMolecularComponent().getComponentStateDefinition(ssldFinalStateName); ComponentStatePattern cspTransitionProduct = new ComponentStatePattern(csdFinal); mcpTransitionProduct.setComponentStatePattern(cspTransitionProduct); - System.out.println(" " + ssldReaction.getCondition()); + lg.debug(" " + ssldReaction.getCondition()); + if(ssldReaction.getCondition().equals(TransitionReaction.FREE_CONDITION)) { mcpTransitionProduct.setBondType(MolecularComponentPattern.BondType.None); } else if(ssldReaction.getCondition().equals(TransitionReaction.BOUND_CONDITION)) { diff --git a/vcell-util/src/main/java/org/vcell/util/ColorUtil.java b/vcell-util/src/main/java/org/vcell/util/ColorUtil.java index ca87a95aee..3bb8022ab7 100644 --- a/vcell-util/src/main/java/org/vcell/util/ColorUtil.java +++ b/vcell-util/src/main/java/org/vcell/util/ColorUtil.java @@ -124,4 +124,73 @@ public static int calcBrightness(int red,int grn,int blu){ return (red*299+grn*587+blu*114)/1000; } + public static final Color[] TABLEAU20 = { + new Color(214,39,40), // red + new Color(31,119,180), // deep blue + new Color(255,127,14), // orange + new Color(44,160,44), // green + new Color(148,103,189), // purple + new Color(23,190,207), // teal + new Color(140,86,75), // brown + new Color(227,119,194), // pink + new Color(127,127,127), // gray + new Color(188,189,34), // olive + new Color(174,199,232), // light blue + new Color(255,187,120), // light orange + new Color(152,223,138), // light green + new Color(255,152,150), // light red + new Color(197,176,213), // light purple + new Color(219,219,141), // light olive + new Color(196,156,148), // light brown + new Color(247,182,210), // light pink + new Color(199,199,199), // light gray + new Color(158,218,229) // light teal + }; + + public static final Color[] COLORBLIND20 = { + new Color(0,114,178), // deep blue + new Color(213,94,0), // orange + new Color(0,158,115), // green + new Color(170,51,119), // magenta + new Color(0,170,170), // teal + new Color(153,153,153), // gray + new Color(0,150,255), // azure + new Color(0,170,0), // emerald + new Color(200,55,0), // brick red + new Color(102,0,153), // purple + new Color(86,180,233), // sky blue + new Color(120,190,32), // lime green + new Color(230,159,0), // goldenrod + new Color(204,121,167), // rose + new Color(0,200,200), // cyan + new Color(102,102,102), // dark gray + new Color(0,90,160), // deep navy + new Color(40,120,40), // forest green + new Color(153,102,204), // lavender + new Color(136,34,85) // plum + }; + + public static final Color[] DARK20 = { + new Color(31,119,180), // dark blue + new Color(214,39,40), // dark red + new Color(44,160,44), // dark green + new Color(148,103,189), // dark purple + new Color(140,86,75), // brown + new Color(23,190,207), // dark cyan + new Color(188,189,34), // olive + new Color(127,127,127), // gray + new Color(57,59,121), // indigo + new Color(82,84,163), // muted violet + new Color(107,110,207), // periwinkle + new Color(156,158,222), // muted lavender + new Color(99,121,57), // moss green + new Color(140,162,82), // muted lime + new Color(181,207,107), // muted olive green + new Color(206,109,189), // muted magenta + new Color(140,109,49), // dark gold/brown + new Color(189,158,57), // mustard + new Color(231,186,82), // muted amber + new Color(231,203,148) // muted beige + }; + }