Added chart stuff and add record dialog.
This commit is contained in:
parent
9f4e3813a0
commit
581eebadbd
|
@ -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>
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue