From 67e707eac89bea6fed59c8fecf3bda2e07c291ce Mon Sep 17 00:00:00 2001 From: rattatwinko Date: Tue, 10 Jun 2025 14:15:06 +0200 Subject: [PATCH] very good very nice, C++ working now! so you can test the app with a ESP32's serial port --- arduinosrc/main.ino | 201 +++++++++++++++- src/main/java/com/puchdyno/Main.java | 341 ++++++++++----------------- 2 files changed, 325 insertions(+), 217 deletions(-) diff --git a/arduinosrc/main.ino b/arduinosrc/main.ino index b7272f4..a58f07e 100644 --- a/arduinosrc/main.ino +++ b/arduinosrc/main.ino @@ -1,10 +1,201 @@ +#include + +// Mock dyno simulation parameters +const int SERIAL_BAUD_RATE = 9600; +const int DATA_SEND_INTERVAL_MS = 100; // Send data every 100ms +const int SIMULATION_STEP_MS = 50; // Update simulation every 50ms + +// Simulation state +enum SimulationState { + IDLE, + ACCELERATING, + HOLDING, + DECELERATING +}; + +SimulationState currentState = IDLE; +unsigned long lastDataSend = 0; +unsigned long lastSimulationUpdate = 0; +unsigned long stateStartTime = 0; + +// RPM simulation variables +float currentRPM = 0.0; +float targetRPM = 0.0; +float rpmAcceleration = 0.0; +float maxRPM = 11000.0; +float minRPM = 800.0; // Idle RPM +float idleRPM = 800.0; + +// Dyno run parameters +const float ACCELERATION_RATE = 25.0; // RPM per 100ms during acceleration +const float DECELERATION_RATE = 30.0; // RPM per 100ms during deceleration +const int HOLD_DURATION_MS = 2000; // Hold at max RPM for 2 seconds +const int IDLE_DURATION_MS = 3000; // Stay at idle for 3 seconds + +// Add some realistic noise and variations +float rpmNoise = 0.0; +int noiseCounter = 0; + void setup() { - Serial.begin(9600); // Ensure this matches the baud rate in Main.java + Serial.begin(SERIAL_BAUD_RATE); + + // Initialize random seed + randomSeed(analogRead(0)); + + // Wait for serial to initialize + delay(2000); + + Serial.println("ESP32 Mock Dyno Data Generator"); + Serial.println("Sending RPM data for dyno simulation..."); + Serial.println("=================================="); + + // Start at idle + currentRPM = idleRPM; + targetRPM = idleRPM; + currentState = IDLE; + stateStartTime = millis(); + + delay(1000); } void loop() { - // Replace this with your actual RPM measurement logic - double currentRPM = 1500.0; // Example RPM value - Serial.println(currentRPM); - delay(100); // Send data every 100ms, adjust as needed + unsigned long currentTime = millis(); + + // Update simulation state + if (currentTime - lastSimulationUpdate >= SIMULATION_STEP_MS) { + updateSimulation(); + lastSimulationUpdate = currentTime; + } + + // Send data to Java application + if (currentTime - lastDataSend >= DATA_SEND_INTERVAL_MS) { + sendRPMData(); + lastDataSend = currentTime; + } + + delay(10); // Small delay to prevent excessive CPU usage } + +void updateSimulation() { + unsigned long currentTime = millis(); + unsigned long stateElapsed = currentTime - stateStartTime; + + switch (currentState) { + case IDLE: + // Stay at idle RPM for a while, then start accelerating + targetRPM = idleRPM + random(-50, 51); // Add some idle variation + if (stateElapsed > IDLE_DURATION_MS) { + currentState = ACCELERATING; + targetRPM = maxRPM; + stateStartTime = currentTime; + Serial.println("Starting dyno run - ACCELERATING"); + } + break; + + case ACCELERATING: + // Accelerate towards max RPM + if (currentRPM < maxRPM - 100) { + currentRPM += ACCELERATION_RATE * (SIMULATION_STEP_MS / 100.0); + // Add some realistic acceleration curve (not perfectly linear) + float accelerationFactor = 1.0 - (currentRPM / maxRPM) * 0.3; + currentRPM = currentRPM * accelerationFactor + currentRPM * (1.0 - accelerationFactor); + } else { + currentState = HOLDING; + stateStartTime = currentTime; + Serial.println("Reached max RPM - HOLDING"); + } + break; + + case HOLDING: + // Hold at max RPM with some variation + targetRPM = maxRPM + random(-100, 101); + if (stateElapsed > HOLD_DURATION_MS) { + currentState = DECELERATING; + targetRPM = idleRPM; + stateStartTime = currentTime; + Serial.println("Starting deceleration - DECELERATING"); + } + break; + + case DECELERATING: + // Decelerate back to idle + if (currentRPM > idleRPM + 100) { + currentRPM -= DECELERATION_RATE * (SIMULATION_STEP_MS / 100.0); + // Add engine braking effect (faster deceleration at higher RPM) + float brakingFactor = 1.0 + (currentRPM / maxRPM) * 0.5; + currentRPM -= (DECELERATION_RATE * brakingFactor - DECELERATION_RATE) * (SIMULATION_STEP_MS / 100.0); + } else { + currentRPM = idleRPM; + currentState = IDLE; + stateStartTime = currentTime; + Serial.println("Returned to idle - IDLE"); + Serial.println("=================================="); + } + break; + } + + // Add realistic noise and fluctuations + addRealisticNoise(); + + // Ensure RPM doesn't go below 0 or above reasonable limits + currentRPM = constrain(currentRPM, 0, maxRPM + 500); +} + +void addRealisticNoise() { + // Add different types of noise based on RPM range + float baseNoise = random(-10, 11); // Basic noise + + // Engine vibration increases with RPM + float vibrationNoise = (currentRPM / 1000.0) * random(-5, 6); + + // Periodic fluctuations (simulating engine cycles) + noiseCounter++; + float periodicNoise = sin(noiseCounter * 0.1) * (currentRPM / 2000.0) * 3.0; + + // Combine all noise sources + rpmNoise = baseNoise + vibrationNoise + periodicNoise; + + // Apply noise with some smoothing + currentRPM += rpmNoise * 0.3; // Reduce noise impact +} + +void sendRPMData() { + // Send just the RPM value as your Java code expects + Serial.println(currentRPM, 0); // Send as integer (no decimal places) + + // Optional: Send debug info to Serial Monitor (comment out for production) + // Serial.print("DEBUG - State: "); + // Serial.print(getStateString()); + // Serial.print(", RPM: "); + // Serial.print(currentRPM, 1); + // Serial.print(", Noise: "); + // Serial.println(rpmNoise, 1); +} + +String getStateString() { + switch (currentState) { + case IDLE: return "IDLE"; + case ACCELERATING: return "ACCELERATING"; + case HOLDING: return "HOLDING"; + case DECELERATING: return "DECELERATING"; + default: return "UNKNOWN"; + } +} + +// Function to manually trigger a dyno run (call this if you want manual control) +void triggerDynoRun() { + if (currentState == IDLE) { + currentState = ACCELERATING; + stateStartTime = millis(); + Serial.println("Manual dyno run triggered!"); + } +} + +// Function to reset to idle (emergency stop) +void resetToIdle() { + currentState = IDLE; + currentRPM = idleRPM; + targetRPM = idleRPM; + stateStartTime = millis(); + Serial.println("Reset to idle!"); +} \ No newline at end of file diff --git a/src/main/java/com/puchdyno/Main.java b/src/main/java/com/puchdyno/Main.java index eb43705..390be14 100644 --- a/src/main/java/com/puchdyno/Main.java +++ b/src/main/java/com/puchdyno/Main.java @@ -29,66 +29,28 @@ public class Main { private static LeistungBerechner leistungBerechner; private static long lastTimestamp = 0; private static double lastOmega = 0.0; - private static final double WHEEL_RADIUS_METERS = 0.25; // Example: 25 cm radius for the dyno wheel + private static final double WHEEL_RADIUS_METERS = 0.25; private static boolean testMode = false; private static ScheduledExecutorService testDataExecutor; private static int testRpmCounter = 1000; - private static final double[] TEST_RPM_VALUES = { - 0, 500, 1000, 1500, 2000, 2500, 3000, 3500, - 4000, 4500, 5000, 5500, 6000, 6500, 7000, 7500, 8000, 8500, - 9000, 9500, 10000, 10500, 11000, 11500, 11000, 10500, 10000, 9500, - 9000, 8500, 8000, 7500, 7000, 6500, 6000, 5500, 5000, 4500, - 4000, 3500, 3000, 2500, 2000, 1500, 1000, 500, 0 - }; - private static int testRpmCurrentIndex = 0; - private static double testRpmCurrentValue = 0.0; - private static final double TEST_RPM_INCREMENT_STEP = 100.0; // Increased for faster simulation - - private static java.util.List smoothRpmData; - private static int smoothRpmDataIndex = 0; - private static final double TEST_SIMULATION_DELTA_TIME = 0.05; // 50 milliseconds - - private static TreeMap simulatedPowerCurve; - private static TreeMap simulatedTorqueCurve; + private static SerialPort currentSerialPort; + private static ScheduledExecutorService serialExecutor; + private static BufferedReader serialReader; public static void main(String[] args) { - // Initialize simulated curves - simulatedPowerCurve = new TreeMap<>(); - simulatedPowerCurve.put(0.0, 0.0); - simulatedPowerCurve.put(4200.0, 2.0); - simulatedPowerCurve.put(6000.0, 5.0); - simulatedPowerCurve.put(7000.0, 6.0); - simulatedPowerCurve.put(8000.0, 7.5); - simulatedPowerCurve.put(8945.0, 8.5); // Peak Power - simulatedPowerCurve.put(9500.0, 8.0); - simulatedPowerCurve.put(10500.0, 7.0); - simulatedPowerCurve.put(11400.0, 6.5); - simulatedPowerCurve.put(12000.0, 6.0); - - simulatedTorqueCurve = new TreeMap<>(); - simulatedTorqueCurve.put(0.0, 0.0); - simulatedTorqueCurve.put(4200.0, 3.0); - simulatedTorqueCurve.put(5000.0, 4.0); - simulatedTorqueCurve.put(6000.0, 5.0); - simulatedTorqueCurve.put(8379.0, 6.8); // Peak Torque - simulatedTorqueCurve.put(9000.0, 6.0); - simulatedTorqueCurve.put(10000.0, 5.0); - simulatedTorqueCurve.put(11400.0, 4.0); - simulatedTorqueCurve.put(12000.0, 3.5); - SwingUtilities.invokeLater(() -> { dynoGUI = new DynoGUI(); leistungBerechner = new LeistungBerechner(); addControlPanel(); - // Optionally start in test mode for development - // toggleTestMode(); + startSerialPortListener(); // Start listening for serial data immediately }); } private static void addControlPanel() { JPanel controlPanel = new JPanel(); + JButton toggleModeButton = new JButton("Toggle Test Mode"); toggleModeButton.addActionListener(e -> toggleTestMode()); controlPanel.add(toggleModeButton); @@ -105,11 +67,20 @@ public class Main { resetChartButton.addActionListener(e -> dynoGUI.resetChart()); controlPanel.add(resetChartButton); + JButton reconnectButton = new JButton("Reconnect Serial"); + reconnectButton.addActionListener(e -> reconnectSerial()); + controlPanel.add(reconnectButton); + dynoGUI.add(controlPanel, BorderLayout.SOUTH); dynoGUI.revalidate(); dynoGUI.repaint(); } + private static void reconnectSerial() { + stopSerialPortListener(); + startSerialPortListener(); + } + private static void toggleTestMode() { testMode = !testMode; if (testMode) { @@ -133,69 +104,38 @@ public class Main { } dynoGUI.resetChart(); - lastTimestamp = 0; // Reset for a new run + lastTimestamp = 0; lastOmega = 0.0; - testRpmCurrentIndex = 0; - testRpmCurrentValue = TEST_RPM_VALUES[0]; // Start at the first RPM value - - // Pre-generate smooth RPM data - smoothRpmData = new ArrayList<>(); - if (TEST_RPM_VALUES.length > 0) { - smoothRpmData.add(TEST_RPM_VALUES[0]); // Start with the first point - } - - for (int i = 0; i < TEST_RPM_VALUES.length - 1; i++) { - double startRpm = TEST_RPM_VALUES[i]; - double endRpm = TEST_RPM_VALUES[i+1]; - - if (startRpm == endRpm) { - // If start and end RPM are the same, just add it if it's not already the last element - if (smoothRpmData.isEmpty() || smoothRpmData.get(smoothRpmData.size() - 1).doubleValue() != endRpm) { - smoothRpmData.add(endRpm); - } - continue; // Move to the next segment - } - - // Determine the direction of change - int direction = (endRpm > startRpm) ? 1 : -1; - double currentRpm = startRpm; - - // Generate intermediate points - while (true) { - double nextRpm = currentRpm + direction * TEST_RPM_INCREMENT_STEP; - - // Check if we would overshoot the endRpm - if ((direction > 0 && nextRpm > endRpm) || (direction < 0 && nextRpm < endRpm)) { - // Add the exact endRpm if it's not already the last one - if (smoothRpmData.isEmpty() || smoothRpmData.get(smoothRpmData.size() - 1).doubleValue() != endRpm) { - smoothRpmData.add(endRpm); - } - break; // Segment finished - } else { - smoothRpmData.add(nextRpm); - currentRpm = nextRpm; - } - } - } - smoothRpmDataIndex = 0; testDataExecutor = Executors.newSingleThreadScheduledExecutor(); testDataExecutor.scheduleAtFixedRate(() -> { - simulateSmoothRpmRun(); - }, 0, 1, TimeUnit.MILLISECONDS); // Update RPM every 10ms for faster animation - } + // Simulate a realistic dyno run pattern + long currentTime = System.currentTimeMillis(); + double elapsedSeconds = (currentTime - lastTimestamp) / 1000.0; - private static void simulateSmoothRpmRun() { - if (smoothRpmDataIndex >= smoothRpmData.size()) { - stopTestRun(); - JOptionPane.showMessageDialog(null, "Test run finished.", "Info", JOptionPane.INFORMATION_MESSAGE); - showPrintableChart(); - return; - } + // Simulate acceleration, hold, then deceleration + double simulatedRpm; + if (elapsedSeconds < 5.0) { + // Acceleration phase (0-5 seconds) + simulatedRpm = 800 + (11000 - 800) * (elapsedSeconds / 5.0); + } else if (elapsedSeconds < 7.0) { + // Hold at max RPM (5-7 seconds) + simulatedRpm = 11000; + } else if (elapsedSeconds < 12.0) { + // Deceleration phase (7-12 seconds) + simulatedRpm = 11000 - (11000 - 800) * ((elapsedSeconds - 7.0) / 5.0); + } else { + // Return to idle after 12 seconds + simulatedRpm = 800; + stopTestRun(); + } - double currentSimulatedRpm = smoothRpmData.get(smoothRpmDataIndex); - processRPMData(currentSimulatedRpm, TEST_SIMULATION_DELTA_TIME); - smoothRpmDataIndex++; + // Add some realistic noise + simulatedRpm += (Math.random() * 200 - 100); + simulatedRpm = Math.max(0, simulatedRpm); + + processRPMData(simulatedRpm, 0.05); // Use 50ms delta time for test mode + }, 0, 50, TimeUnit.MILLISECONDS); // Update every 50ms } private static void stopTestRun() { @@ -208,9 +148,6 @@ public class Main { } } - private static SerialPort currentSerialPort; - private static ScheduledExecutorService serialExecutor; - private static void startSerialPortListener() { SerialPort[] comPorts = SerialPort.getCommPorts(); if (comPorts.length == 0) { @@ -250,23 +187,29 @@ public class Main { return; } - final SerialPort finalChosenPort = chosenPort; - currentSerialPort = finalChosenPort; // Store for later closing + currentSerialPort = chosenPort; + chosenPort.setBaudRate(9600); + chosenPort.setComPortTimeouts(SerialPort.TIMEOUT_READ_SEMI_BLOCKING, 100, 0); - finalChosenPort.setBaudRate(9600); // Common baud rate, can be adjusted - if (finalChosenPort.openPort()) { - System.out.println("Serial port " + finalChosenPort.getSystemPortName() + " opened successfully."); - BufferedReader reader = new BufferedReader(new InputStreamReader(finalChosenPort.getInputStream())); + if (chosenPort.openPort()) { + System.out.println("Serial port " + chosenPort.getSystemPortName() + " opened successfully."); + serialReader = new BufferedReader(new InputStreamReader(chosenPort.getInputStream())); serialExecutor = Executors.newSingleThreadScheduledExecutor(); serialExecutor.scheduleAtFixedRate(() -> { try { - if (reader.ready()) { - String line = reader.readLine(); + if (serialReader.ready()) { + String line = serialReader.readLine(); if (line != null && !line.trim().isEmpty()) { try { - double rpm = Double.parseDouble(line.trim()); - processRPMData(rpm); // Call the main processRPMData for live data + // Handle potential incomplete lines or multiple values + String[] values = line.trim().split("[\\s,]+"); + for (String value : values) { + if (!value.isEmpty()) { + double rpm = Double.parseDouble(value); + processRPMData(rpm); // Process each valid RPM value + } + } } catch (NumberFormatException e) { System.err.println("Received non-numeric data: " + line); } @@ -276,124 +219,93 @@ public class Main { System.err.println("Error reading from serial port: " + e.getMessage()); e.printStackTrace(); stopSerialPortListener(); - JOptionPane.showMessageDialog(null, "Serial port error: " + e.getMessage(), "Error", JOptionPane.ERROR_MESSAGE); + SwingUtilities.invokeLater(() -> { + JOptionPane.showMessageDialog(null, + "Serial port error: " + e.getMessage(), + "Error", JOptionPane.ERROR_MESSAGE); + }); } - }, 0, 100, TimeUnit.MILLISECONDS); // Read every 100ms + }, 0, 10, TimeUnit.MILLISECONDS); // Check more frequently for data } else { - JOptionPane.showMessageDialog(null, "Failed to open serial port " + finalChosenPort.getSystemPortName(), "Error", JOptionPane.ERROR_MESSAGE); + JOptionPane.showMessageDialog(null, + "Failed to open serial port " + chosenPort.getSystemPortName(), + "Error", JOptionPane.ERROR_MESSAGE); } } private static void stopSerialPortListener() { if (serialExecutor != null) { serialExecutor.shutdownNow(); + try { + if (!serialExecutor.awaitTermination(500, TimeUnit.MILLISECONDS)) { + serialExecutor.shutdownNow(); + } + } catch (InterruptedException e) { + serialExecutor.shutdownNow(); + Thread.currentThread().interrupt(); + } serialExecutor = null; } + + if (serialReader != null) { + try { + serialReader.close(); + } catch (Exception e) { + System.err.println("Error closing serial reader: " + e.getMessage()); + } + serialReader = null; + } + if (currentSerialPort != null && currentSerialPort.isOpen()) { currentSerialPort.closePort(); System.out.println("Serial port " + currentSerialPort.getSystemPortName() + " closed."); currentSerialPort = null; } + SwingUtilities.invokeLater(() -> { dynoGUI.updateWerte(0, 0, 0); // Reset gauges }); } - // Overloaded processRPMData for live mode, calculates deltaZeit from system time private static void processRPMData(double rpm) { - processRPMData(rpm, 0.0); // Pass 0.0 to indicate no forced delta time, use system time - } - - // Main processRPMData for both live and test mode, uses forcedDeltaTime if provided - private static void processRPMData(double rpm, double forcedDeltaTime) { long currentTimestamp = System.currentTimeMillis(); - double effectiveDeltaZeit; - double leistung; - double drehmoment; - - if (forcedDeltaTime > 0) { - // Test mode: Use simulated curves with linear interpolation - effectiveDeltaZeit = forcedDeltaTime; - - leistung = interpolate(simulatedPowerCurve, rpm); - drehmoment = interpolate(simulatedTorqueCurve, rpm); - - SwingUtilities.invokeLater(() -> { - dynoGUI.updateWerte(leistung, drehmoment, rpm); - }); - - // For test mode, we still need to update lastOmega for the next iteration - // in case the next simulated RPM is used for an actual calculation (e.g. if we switch modes). - lastOmega = rpmToOmega(rpm); - lastTimestamp = currentTimestamp; // Also update timestamp for consistency, though not used in test mode calculation - return; - - } else { - // Live mode: Calculate from actual sensor data - if (lastTimestamp == 0) { - lastTimestamp = currentTimestamp; - lastOmega = rpmToOmega(rpm); - SwingUtilities.invokeLater(() -> { - dynoGUI.updateWerte(0, 0, rpm); // Update RPM, but no power/torque yet for the first point - }); - return; // Skip calculations for the very first data point - } - effectiveDeltaZeit = (currentTimestamp - lastTimestamp) / 1000.0; - // Ensure effectiveDeltaZeit is always positive and non-zero for live mode - if (effectiveDeltaZeit <= 0) { - effectiveDeltaZeit = 0.001; // Assign a small positive value to avoid division by zero - } - - double currentOmega = rpmToOmega(rpm); - - // If there's no change in RPM, then acceleration is zero. Display zero power/torque. - if (currentOmega == lastOmega) { - SwingUtilities.invokeLater(() -> { - dynoGUI.updateWerte(0, 0, rpm); // Update RPM, but output zero power/torque - }); - lastTimestamp = currentTimestamp; // Crucially update for the next iteration - lastOmega = currentOmega; // Crucially update for the next iteration - return; - } - - drehmoment = leistungBerechner.berechneDrehmoment(lastOmega, currentOmega, effectiveDeltaZeit); - leistung = leistungBerechner.berechneLeistung(drehmoment, currentOmega); - - SwingUtilities.invokeLater(() -> { - dynoGUI.updateWerte(leistung, drehmoment, rpm); - }); - + if (lastTimestamp == 0) { lastTimestamp = currentTimestamp; - lastOmega = currentOmega; + lastOmega = rpmToOmega(rpm); + SwingUtilities.invokeLater(() -> { + dynoGUI.updateWerte(0, 0, rpm); + }); + return; } + + double deltaTime = (currentTimestamp - lastTimestamp) / 1000.0; + if (deltaTime <= 0) deltaTime = 0.001; + + double currentOmega = rpmToOmega(rpm); + double drehmoment = leistungBerechner.berechneDrehmoment(lastOmega, currentOmega, deltaTime); + double leistung = leistungBerechner.berechneLeistung(drehmoment, currentOmega); + + SwingUtilities.invokeLater(() -> { + dynoGUI.updateWerte(leistung, drehmoment, rpm); + }); + + lastTimestamp = currentTimestamp; + lastOmega = currentOmega; } - // Linear interpolation method - private static double interpolate(TreeMap curve, double rpm) { - if (curve.containsKey(rpm)) { - return curve.get(rpm); - } - Map.Entry lower = curve.floorEntry(rpm); - Map.Entry upper = curve.ceilingEntry(rpm); + private static void processRPMData(double rpm, double forcedDeltaTime) { + // For test mode with forced delta time + double currentOmega = rpmToOmega(rpm); + double drehmoment = leistungBerechner.berechneDrehmoment(lastOmega, currentOmega, forcedDeltaTime); + double leistung = leistungBerechner.berechneLeistung(drehmoment, currentOmega); - if (lower == null) { - return upper.getValue(); // RPM is below lowest defined point, return lowest point's value - } - if (upper == null) { - return lower.getValue(); // RPM is above highest defined point, return highest point's value - } + SwingUtilities.invokeLater(() -> { + dynoGUI.updateWerte(leistung, drehmoment, rpm); + }); - double x1 = lower.getKey(); - double y1 = lower.getValue(); - double x2 = upper.getKey(); - double y2 = upper.getValue(); - - // Linear interpolation formula: y = y1 + ((y2 - y1) / (x2 - x1)) * (x - x1) - if (x2 == x1) { // Avoid division by zero if two points have the same RPM (shouldn't happen with proper data) - return y1; - } - return y1 + ((y2 - y1) / (x2 - x1)) * (rpm - x1); + lastOmega = currentOmega; + lastTimestamp = System.currentTimeMillis(); } private static double rpmToOmega(double rpm) { @@ -418,7 +330,9 @@ public class Main { try { job.print(); } catch (PrinterException ex) { - JOptionPane.showMessageDialog(chartFrame, "Printing error: " + ex.getMessage(), "Print Error", JOptionPane.ERROR_MESSAGE); + JOptionPane.showMessageDialog(chartFrame, + "Printing error: " + ex.getMessage(), + "Print Error", JOptionPane.ERROR_MESSAGE); } } }); @@ -451,27 +365,30 @@ public class Main { File finalFile = new File(filePath); try { - // Use the dimensions of the ChartPanel to create the PDF document Document document = new Document(new Rectangle(chartPanel.getWidth(), chartPanel.getHeight())); PdfWriter.getInstance(document, new FileOutputStream(finalFile)); document.open(); - // Create BufferedImage from the ChartPanel's chart - java.awt.Image awtImage = chartPanel.getChart().createBufferedImage(chartPanel.getWidth(), chartPanel.getHeight()); + java.awt.Image awtImage = chartPanel.getChart().createBufferedImage( + chartPanel.getWidth(), chartPanel.getHeight()); Image pdfImage = Image.getInstance(awtImage, null); - // Scale image to fit document - pdfImage.scaleToFit(document.getPageSize().getWidth() - 20, document.getPageSize().getHeight() - 20); // Add some padding + pdfImage.scaleToFit(document.getPageSize().getWidth() - 20, + document.getPageSize().getHeight() - 20); pdfImage.setAlignment(Image.ALIGN_CENTER); document.add(pdfImage); document.close(); - JOptionPane.showMessageDialog(null, "Chart saved as PDF successfully!\n" + finalFile.getAbsolutePath(), "Save Successful", JOptionPane.INFORMATION_MESSAGE); + JOptionPane.showMessageDialog(null, + "Chart saved as PDF successfully!\n" + finalFile.getAbsolutePath(), + "Save Successful", JOptionPane.INFORMATION_MESSAGE); } catch (Exception ex) { - JOptionPane.showMessageDialog(null, "Error saving chart as PDF: " + ex.getMessage(), "Save Error", JOptionPane.ERROR_MESSAGE); + JOptionPane.showMessageDialog(null, + "Error saving chart as PDF: " + ex.getMessage(), + "Save Error", JOptionPane.ERROR_MESSAGE); ex.printStackTrace(); } } } -} \ No newline at end of file +} \ No newline at end of file