Added chart stuff and add record dialog.

This commit is contained in:
Andrew Lalis 2023-04-19 21:38:00 +02:00
parent 9f4e3813a0
commit 581eebadbd
6 changed files with 275 additions and 11 deletions

View File

@ -25,6 +25,11 @@
<artifactId>flatlaf</artifactId>
<version>3.1</version>
</dependency>
<dependency>
<groupId>org.jfree</groupId>
<artifactId>jfreechart</artifactId>
<version>1.5.3</version>
</dependency>
</dependencies>
</project>

View File

@ -0,0 +1,208 @@
package com.github.andrewlalis.running_every_day.view;
import com.github.andrewlalis.running_every_day.data.DataSource;
import com.github.andrewlalis.running_every_day.data.RunRecord;
import javax.swing.*;
import java.awt.*;
import java.math.BigDecimal;
import java.sql.SQLException;
import java.time.Duration;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Pattern;
public class AddRunRecordDialog extends JDialog {
private final DataSource dataSource;
private final JTextField dateField;
private final JTextField timeField;
private final JTextField distanceField;
private final JTextField durationField;
private final JTextField weightField;
private final JTextArea commentField;
private boolean added = false;
public AddRunRecordDialog(Window owner, DataSource dataSource) {
super(owner, "Add a Record", ModalityType.APPLICATION_MODAL);
this.dataSource = dataSource;
this.dateField = new JTextField(LocalDate.now().toString(), 0);
this.timeField = new JTextField(LocalTime.now().format(DateTimeFormatter.ofPattern("HH:mm")), 0);
this.distanceField = new JTextField("5.0", 0);
this.durationField = new JTextField("00:00:00", 0);
this.weightField = new JTextField("85.0", 0);
this.commentField = new JTextArea(3, 0);
JPanel mainPanel = new JPanel(new BorderLayout());
JPanel contentPanel = new JPanel();
contentPanel.setLayout(new BoxLayout(contentPanel, BoxLayout.PAGE_AXIS));
contentPanel.add(buildFieldsPanel());
mainPanel.add(contentPanel, BorderLayout.NORTH);
JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT));
JButton addButton = new JButton("Add");
addButton.addActionListener(e -> onAdd());
JButton cancelButton = new JButton("Cancel");
cancelButton.addActionListener(e -> this.dispose());
buttonPanel.add(addButton);
buttonPanel.add(cancelButton);
mainPanel.add(buttonPanel, BorderLayout.SOUTH);
this.setContentPane(mainPanel);
this.setPreferredSize(new Dimension(400, 300));
this.pack();
this.setLocationRelativeTo(owner);
}
public boolean isAdded() {
return added;
}
private Container buildFieldsPanel() {
JPanel panel = new JPanel(new GridBagLayout());
var c = new GridBagConstraints();
c.insets = new Insets(5, 5, 5, 5);
c.weightx = 0;
c.weighty = 0;
c.gridx = 0;
c.gridy = 0;
c.anchor = GridBagConstraints.NORTHWEST;
String[] labels = new String[]{"Date", "Start Time", "Distance (Km)", "Duration (HH:MM:SS)", "Weight (Kg)", "Comments"};
for (var label : labels) {
panel.add(new JLabel(label), c);
c.gridy++;
}
Component[] fields = new Component[]{dateField, timeField, distanceField, durationField, weightField, commentField};
c.weightx = 1;
c.weighty = 1;
c.gridx = 1;
c.gridy = 0;
c.anchor = GridBagConstraints.NORTHEAST;
for (var field : fields) {
if (field instanceof JTextArea) {
c.fill = GridBagConstraints.BOTH;
} else {
c.fill = GridBagConstraints.HORIZONTAL;
}
panel.add(field, c);
c.gridy++;
}
return panel;
}
private List<String> validateForm() {
List<String> errorMessages = new ArrayList<>();
LocalDate date = null;
try {
date = LocalDate.parse(dateField.getText());
if (date.isAfter(LocalDate.now())) {
errorMessages.add("Cannot add a run for a date in the future.");
}
} catch (DateTimeParseException e) {
errorMessages.add("Invalid date format. Should be YYYY-MM-DD.");
}
try {
var time = LocalTime.parse(timeField.getText());
if (date != null && date.atTime(time).isAfter(LocalDateTime.now())) {
errorMessages.add("Cannot add a run for a time in the future.");
}
} catch (DateTimeParseException e) {
errorMessages.add("Invalid start time format. Should be HH:MM.");
}
try {
BigDecimal distanceKm = new BigDecimal(distanceField.getText());
if (distanceKm.compareTo(BigDecimal.ZERO) < 0 || distanceKm.scale() > 3) {
errorMessages.add("Invalid distance.");
}
} catch (NumberFormatException e) {
errorMessages.add("Invalid or missing distance.");
}
try {
Duration duration = parseDuration(durationField.getText());
if (duration.isNegative() || duration.isZero()) {
errorMessages.add("Duration must be positive.");
}
} catch (IllegalArgumentException e) {
errorMessages.add(e.getMessage());
}
try {
BigDecimal weightKg = new BigDecimal(weightField.getText());
if (weightKg.compareTo(BigDecimal.ZERO) < 0 || weightKg.scale() > 3) {
errorMessages.add("Invalid weight.");
}
} catch (NumberFormatException e) {
errorMessages.add("Invalid or missing weight.");
}
if (commentField.getText().isBlank()) {
errorMessages.add("Comments should not be empty.");
}
return errorMessages;
}
private void onAdd() {
var messages = validateForm();
if (!messages.isEmpty()) {
JOptionPane.showMessageDialog(
this,
"Form validation failed:\n" + String.join("\n", messages),
"Validation Failed",
JOptionPane.WARNING_MESSAGE
);
} else {
try {
this.dataSource.runRecords().save(new RunRecord(
0,
LocalDate.parse(dateField.getText()),
LocalTime.parse(timeField.getText()),
new BigDecimal(distanceField.getText()),
parseDuration(durationField.getText()),
new BigDecimal(weightField.getText()),
commentField.getText().strip()
));
this.added = true;
this.dispose();
} catch (SQLException e) {
e.printStackTrace();
JOptionPane.showMessageDialog(
this,
"An SQL Exception occurred: " + e.getMessage(),
"Error",
JOptionPane.ERROR_MESSAGE
);
}
}
}
private static Duration parseDuration(String s) {
String[] parts = s.strip().split(":");
if (parts.length < 2 || parts.length > 3) {
throw new IllegalArgumentException("Invalid or missing duration.");
}
int hours = 0;
int minutes = 0;
int seconds = 0;
if (parts.length == 3) {
hours = Integer.parseInt(parts[0]);
minutes = Integer.parseInt(parts[1]);
seconds = Integer.parseInt(parts[2]);
} else {
minutes = Integer.parseInt(parts[0]);
seconds = Integer.parseInt(parts[1]);
}
return Duration.ofSeconds(hours * 60L * 60 + minutes * 60L + seconds);
}
}

View File

@ -0,0 +1,48 @@
package com.github.andrewlalis.running_every_day.view;
import org.jfree.chart.ChartFactory;
import org.jfree.chart.JFreeChart;
import org.jfree.data.category.DefaultCategoryDataset;
import org.jfree.data.time.Day;
import org.jfree.data.time.TimeSeries;
import org.jfree.data.time.TimeSeriesCollection;
import org.jfree.data.xy.XYDataset;
import javax.swing.*;
import java.awt.*;
import java.awt.geom.Rectangle2D;
public class ChartsPanel extends JPanel {
private final JPanel drawingPanel = new JPanel();
public ChartsPanel() {
super(new BorderLayout());
this.add(drawingPanel, BorderLayout.CENTER);
JPanel buttonPanel = new JPanel();
JButton drawButton = new JButton("Draw");
drawButton.addActionListener(e -> draw());
buttonPanel.add(drawButton);
this.add(buttonPanel, BorderLayout.NORTH);
}
public void draw() {
TimeSeriesCollection ds = new TimeSeriesCollection();
TimeSeries ts = new TimeSeries("Data");
ts.add(new Day(16, 4, 2023), 45);
ts.add(new Day(17, 4, 2023), 50);
ts.add(new Day(18, 4, 2023), 52);
ts.add(new Day(19, 4, 2023), 65);
ds.addSeries(ts);
JFreeChart chart = ChartFactory.createXYLineChart(
"Test XY Line Chart",
"Date",
"Value",
ds
);
Graphics2D g2 = (Graphics2D) drawingPanel.getGraphics();
Rectangle2D area = new Rectangle2D.Float(0, 0, drawingPanel.getWidth(), drawingPanel.getHeight());
chart.draw(g2, area);
}
}

View File

@ -18,16 +18,8 @@ public class RecorderAppWindow extends JFrame {
private Container buildGui(DataSource dataSource) {
JTabbedPane tabbedPane = new JTabbedPane();
tabbedPane.addTab("Run Records", new RunRecordsPanel(dataSource));
tabbedPane.addTab("Aggregate Statistics", buildAggregateStatisticsPanel(dataSource));
tabbedPane.addTab("Charts", buildChartsPanel(dataSource));
tabbedPane.addTab("Aggregate Statistics", new JPanel());
tabbedPane.addTab("Charts", new ChartsPanel());
return tabbedPane;
}
private Container buildAggregateStatisticsPanel(DataSource dataSource) {
return new JPanel();
}
private Container buildChartsPanel(DataSource dataSource) {
return new JPanel();
}
}

View File

@ -68,7 +68,7 @@ public class RunRecordTableModel extends AbstractTableModel {
return currentPage;
}
private void loadPage() {
public void loadPage() {
try {
var page = dataSource.runRecords().findAll(currentPage);
cachedPageCount = (int) dataSource.runRecords().pageCount(currentPage.size());

View File

@ -28,8 +28,12 @@ public class RunRecordsPanel extends JPanel {
table.getColumnModel().getColumn(1).setMaxWidth(80);
table.getColumnModel().getColumn(2).setMaxWidth(80);
table.getColumnModel().getColumn(3).setMaxWidth(100);
table.getColumnModel().getColumn(3).setPreferredWidth(100);
table.getColumnModel().getColumn(4).setMaxWidth(80);
table.getColumnModel().getColumn(5).setMaxWidth(80);
for (int i = 0; i < 6; i++) {
table.getColumnModel().getColumn(i).setResizable(false);
}
var scrollPane = new JScrollPane(table, ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS, ScrollPaneConstants.HORIZONTAL_SCROLLBAR_AS_NEEDED);
this.add(scrollPane, BorderLayout.CENTER);
@ -86,6 +90,13 @@ public class RunRecordsPanel extends JPanel {
JPanel actionsPanel = new JPanel(new FlowLayout(FlowLayout.LEFT));
JButton addActionButton = new JButton("Add a Record");
addActionButton.addActionListener(e -> {
var dialog = new AddRunRecordDialog(SwingUtilities.getWindowAncestor(this), dataSource);
dialog.setVisible(true);
if (dialog.isAdded()) {
tableModel.firstPage();
}
});
actionsPanel.add(addActionButton);
this.add(actionsPanel, BorderLayout.NORTH);
}