DateToJdDialog.java

/**
 * VStar: a statistical analysis tool for variable star data.
 * Copyright (C) 2009  AAVSO (http://www.aavso.org/)
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as
 * published by the Free Software Foundation, either version 3 of the
 * License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>. 
 */

package org.aavso.tools.vstar.ui.dialog;

import java.awt.BorderLayout;
import java.awt.Container;
import java.awt.FlowLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.text.DecimalFormat;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.YearMonth;

import javax.swing.BorderFactory;
import javax.swing.BoxLayout;
import javax.swing.JButton;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JTextField;

import org.aavso.tools.vstar.util.date.AbstractDateUtil;
import org.aavso.tools.vstar.util.date.YMD;

@SuppressWarnings("serial")
public class DateToJdDialog extends AbstractOkCancelDialog {
	private IntegerField yearField;
	private IntegerField monthField;
	private IntegerField dayField;
	private IntegerField hourField;
	private IntegerField minField;
	private DoubleFieldSeconds secField;
	
	private static final int MIN_YEAR = 1600;
	private static final int MAX_YEAR = 9999;
	
	// Julian day of the Java Epoch 1970-01-01T00:00:00Z
	private static final double JAVA_EPOCH_JD = 2440587.5;
	
	/**
	 * Constructor
	 */
	public DateToJdDialog(String title) {
		super(title);
		
		Container contentPane = this.getContentPane();

		JPanel topPane = new JPanel();
		topPane.setLayout(new BoxLayout(topPane, BoxLayout.PAGE_AXIS));
		topPane.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5));
		topPane.add(createDatePane());
		topPane.add(createButtonPaneX());
		topPane.add(createButtonPane());
		
		contentPane.add(topPane);
		
		setCurrentTime();			
	    
		this.pack();
	}
	
	private JPanel createDatePane() {
		JPanel panel = new JPanel(new FlowLayout());

		yearField = new IntegerField("Year", MIN_YEAR, MAX_YEAR, 0);
		panel.add(setNumberFieldColumns(yearField, 5));
		
		monthField = new IntegerField("Mon", 1, 12, 0);
		panel.add(setNumberFieldColumns(monthField, 4));
		
		dayField = new IntegerField("Day", 1, 31, 0);
		panel.add(setNumberFieldColumns(dayField, 4));
		
		panel.add(new JLabel("  "));
		
		hourField = new IntegerField("Hr", 0, 23, 0);
		panel.add(setNumberFieldColumns(hourField, 4));
		
		minField = new IntegerField("Min", 0, 59, 0);
		panel.add(setNumberFieldColumns(minField, 4));
		
		secField = new DoubleFieldSeconds("Sec", 0.0, 59.99, 0.00);
		panel.add(setNumberFieldColumns(secField, 5));
		return panel;
	}

	protected void cancelAction() {
		// Nothing to do.
	}
	
	protected void okAction() {
		Double d = getJD();
		if (d != null) {
			cancelled = false;
			setVisible(false);
			dispose();
		}
	}
	
	private JPanel createButtonPaneX() {
		JPanel panel = new JPanel(new BorderLayout());
		
		JButton timeNowButton = new JButton("Current Time (UT)");			
		timeNowButton.addActionListener(createTimeNowButtonListener());
		panel.add(timeNowButton, BorderLayout.LINE_END);
		
		return panel;
	}
	
	// Return a listener for the "Now" button.
	private ActionListener createTimeNowButtonListener() {
		return new ActionListener() {
			public void actionPerformed(ActionEvent e) {
				setCurrentTime();
			}
		};
	}
	
	private JTextField setNumberFieldColumns(NumberFieldBase<?> field, int columns) {
		JTextField textField = (JTextField)(field.getUIComponent());
		textField.setColumns(columns);
		return textField;
	}

	public void setJD(double jd) {
		setDateFromJd(jd);
	}
	
	public Double getJD() {
		return getJdFromDate();
	}
	
	// Set form fields to values corresponding to the current time
	private void setCurrentTime() {
		double jd = julianDayNow();
		setDateFromJd(jd);			
	}

	private void setDateFromJd(Double jd) {
		yearField.setValue(null);
		monthField.setValue(null);
		dayField.setValue(null);
		hourField.setValue(null);
		minField.setValue(null);
		secField.setValue(null);
		
		if (jd == null) return; 
		
		YMD ymd = AbstractDateUtil.getInstance().jdToYMD(jd);

		int year = ymd.getYear();
		int month = ymd.getMonth();
		double day = ymd.getDay();
		int iday = (int)day;
		
		int hour = (int)((day - iday) * 24.0);
		int min = (int)((day - iday - hour / 24.0) * 24.0 * 60.0);
		double sec = ((day - iday - hour / 24.0 - min / 24.0 / 60.0) * 24.0 * 60.0 * 60.0);

		// round seconds to 0.01, like in the input field.
		double rsec = Math.round(sec * 100.0) / 100.0;
		
		// After rounding, rsec can be up to 60.00.
		// This is a fix for the case when rounded seconds = 60: 
		if (rsec > 59.99) {
			// Create LocalDateTime object using integer number of seconds. 
			LocalDateTime aTime = LocalDateTime.of(year, month, iday, hour, min, 59);
			// Add 1 second.
			aTime = aTime.plusSeconds(1);
			// Now we can get correct values of Date/Time fields after adding a second (potentially, adding a second can change them all).
			year = aTime.getYear();
			month = aTime.getMonthValue();
			iday = aTime.getDayOfMonth();
			hour = aTime.getHour();
			min = aTime.getMinute();
			rsec = aTime.getSecond(); // should be 0 
		}
		
		yearField.setValue(year);
		monthField.setValue(month);
		dayField.setValue(iday);
		hourField.setValue(hour);
		minField.setValue(min);
		secField.setValue(rsec);
	}
	
	private Double getJdFromDate() {
		
		Integer year =	getAndCheck(yearField);
		if (year == null) return null;
		
		Integer month = getAndCheck(monthField);
		if (month == null) return null;
		
		Integer day = getAndCheck(dayField);
		YearMonth yearMonthObject = YearMonth.of(year, month);
		int daysInMonth = yearMonthObject.lengthOfMonth();
		if (day > daysInMonth) {
			MessageBox.showErrorDialog(this, "Error", dayField.getName() + ": Invalid number of days in the month!");
			(dayField.getUIComponent()).requestFocus();
			((JTextField)(dayField.getUIComponent())).selectAll();
			day = null;
		}
		if (day == null) return null;
		
		Integer hour = getAndCheck(hourField);
		if (hour == null) return null;

		Integer min = getAndCheck(minField);
		if (min == null) return null;

		Double sec = getAndCheck(secField);
		if (sec == null) return null;

		double dday = day + hour / 24.0 + min / 24.0 / 60.0 + sec / 24.0 / 60.0 / 60.0;   
		double jd = AbstractDateUtil.getInstance().calendarToJD(year, month, dday);
		
		return jd;
	}
	
	// Get value of IntegerField, show message in case of error and set focus to the field
	private Integer getAndCheck(IntegerField input) {
		Integer i;
		try {
			i = input.getValue();
		} catch (Exception e) {
			// We should never be here as soon as getValue catches parseInteger() exceptions.  
			i = null;
		}
		if (i == null) {
			MessageBox.showErrorDialog(this, "Error", input.getName() + ": Invalid value!\n" +
					"Value must be integer.\n" +
					numberFieldInfo(input));				
			(input.getUIComponent()).requestFocus();
			((JTextField)(input.getUIComponent())).selectAll();
		}
		return i;
	}

	// Get value of DoubleField, show message in case of error and set focus to the field
	private Double getAndCheck(DoubleField input) {
		Double d;
		try {
			d = input.getValue();
		} catch (Exception e) {
			// We should never be here as soon as getValue catches catches all possible exceptions.  
			d = null;
		}
		if (d == null) {
			MessageBox.showErrorDialog(this, "Error", input.getName() + ": Invalid value!\n" +
					numberFieldInfo(input));				
			(input.getUIComponent()).requestFocus();
			((JTextField)(input.getUIComponent())).selectAll();
		}
		return d;
	}
	
	private String numberFieldInfo(NumberFieldBase<?> field) {
		String s = "";
		if (field.getMin() == null && field.getMax() != null)
			s = "Only values <= " + field.getMax() + " allowed.";
		else if (field.getMin() != null && field.getMax() == null)
			s = "Only values >= " + field.getMin() + " allowed.";
		else if (field.getMin() != null && field.getMax() != null)
			s = "Only values between " + field.getMin() + " and " + field.getMax() + " allowed.";
		return s; 
	}
	
	// Current Julian Day value
    private static double julianDayNow()
    {
        return Instant.now().getEpochSecond() / (24.0 * 60.0 * 60.0) + JAVA_EPOCH_JD;
    }

	/*
	 * Unlike DoubleField, DoubleFieldTime uses special format
	 */
	private class DoubleFieldSeconds extends DoubleField {
	
		private DecimalFormat format = new DecimalFormat("#.##");
		
		public DoubleFieldSeconds(String name, Double min, Double max, Double initial) {
			super(name, min, max, initial);
			setValue(initial); // special formatting
		}
	
		@Override
		public void setValue(Double value) {
			textField.setText(value == null ? "" : format.format(value));
		}
		
	}
	
}