Added chart stuff and add record dialog.
This commit is contained in:
parent
9f4e3813a0
commit
581eebadbd
|
@ -25,6 +25,11 @@
|
||||||
<artifactId>flatlaf</artifactId>
|
<artifactId>flatlaf</artifactId>
|
||||||
<version>3.1</version>
|
<version>3.1</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.jfree</groupId>
|
||||||
|
<artifactId>jfreechart</artifactId>
|
||||||
|
<version>1.5.3</version>
|
||||||
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|
||||||
</project>
|
</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) {
|
private Container buildGui(DataSource dataSource) {
|
||||||
JTabbedPane tabbedPane = new JTabbedPane();
|
JTabbedPane tabbedPane = new JTabbedPane();
|
||||||
tabbedPane.addTab("Run Records", new RunRecordsPanel(dataSource));
|
tabbedPane.addTab("Run Records", new RunRecordsPanel(dataSource));
|
||||||
tabbedPane.addTab("Aggregate Statistics", buildAggregateStatisticsPanel(dataSource));
|
tabbedPane.addTab("Aggregate Statistics", new JPanel());
|
||||||
tabbedPane.addTab("Charts", buildChartsPanel(dataSource));
|
tabbedPane.addTab("Charts", new ChartsPanel());
|
||||||
return tabbedPane;
|
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;
|
return currentPage;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void loadPage() {
|
public void loadPage() {
|
||||||
try {
|
try {
|
||||||
var page = dataSource.runRecords().findAll(currentPage);
|
var page = dataSource.runRecords().findAll(currentPage);
|
||||||
cachedPageCount = (int) dataSource.runRecords().pageCount(currentPage.size());
|
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(1).setMaxWidth(80);
|
||||||
table.getColumnModel().getColumn(2).setMaxWidth(80);
|
table.getColumnModel().getColumn(2).setMaxWidth(80);
|
||||||
table.getColumnModel().getColumn(3).setMaxWidth(100);
|
table.getColumnModel().getColumn(3).setMaxWidth(100);
|
||||||
|
table.getColumnModel().getColumn(3).setPreferredWidth(100);
|
||||||
table.getColumnModel().getColumn(4).setMaxWidth(80);
|
table.getColumnModel().getColumn(4).setMaxWidth(80);
|
||||||
table.getColumnModel().getColumn(5).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);
|
var scrollPane = new JScrollPane(table, ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS, ScrollPaneConstants.HORIZONTAL_SCROLLBAR_AS_NEEDED);
|
||||||
this.add(scrollPane, BorderLayout.CENTER);
|
this.add(scrollPane, BorderLayout.CENTER);
|
||||||
|
|
||||||
|
@ -86,6 +90,13 @@ public class RunRecordsPanel extends JPanel {
|
||||||
|
|
||||||
JPanel actionsPanel = new JPanel(new FlowLayout(FlowLayout.LEFT));
|
JPanel actionsPanel = new JPanel(new FlowLayout(FlowLayout.LEFT));
|
||||||
JButton addActionButton = new JButton("Add a Record");
|
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);
|
actionsPanel.add(addActionButton);
|
||||||
this.add(actionsPanel, BorderLayout.NORTH);
|
this.add(actionsPanel, BorderLayout.NORTH);
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue