very good very nice, C++ working now! so you can test the app with a ESP32's serial port
All checks were successful
Maven Build / build (push) Successful in 10m13s

This commit is contained in:
rattatwinko
2025-06-10 14:15:06 +02:00
parent c6602315ba
commit 67e707eac8
2 changed files with 325 additions and 217 deletions

View File

@@ -1,10 +1,201 @@
#include <WiFi.h>
// 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!");
}

View File

@@ -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<Double> smoothRpmData;
private static int smoothRpmDataIndex = 0;
private static final double TEST_SIMULATION_DELTA_TIME = 0.05; // 50 milliseconds
private static TreeMap<Double, Double> simulatedPowerCurve;
private static TreeMap<Double, Double> 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<Double, Double> curve, double rpm) {
if (curve.containsKey(rpm)) {
return curve.get(rpm);
}
Map.Entry<Double, Double> lower = curve.floorEntry(rpm);
Map.Entry<Double, Double> 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();
}
}
}
}
}