// Copyright 2003
// Association for Universities for Research in Astronomy, Inc.,
// Observatory Control System, Gemini Telescopes Project.
//
// $Id: ElevationPlotModel.java,v 1.1.1.1 2009/02/17 22:49:36 abrighto Exp $
package jsky.plot;
import jsky.coords.SiteDesc;
import jsky.coords.TargetDesc;
import jsky.util.CalendarUtil;
import jsky.util.Preferences;
import jsky.util.StringUtil;
import org.jfree.data.category.IntervalCategoryDataset;
import org.jfree.data.gantt.Task;
import org.jfree.data.gantt.TaskSeries;
import org.jfree.data.gantt.TaskSeriesCollection;
import org.jfree.data.time.Second;
import org.jfree.data.time.SimpleTimePeriod;
import org.jfree.data.time.TimeSeries;
import org.jfree.data.time.TimeSeriesCollection;
import org.jfree.data.xy.XYDataset;
import javax.swing.event.*;
import javax.swing.table.TableModel;
import java.text.SimpleDateFormat;
import java.util.*;
/**
* A model class for plotting elevation vs local sidereal time for a given list of target
* positions. This class can be used to display the graph using JFreeChart.
*
* @version $Revision: 1.1.1.1 $
* @author Allan Brighton
*/
public class ElevationPlotModel {
public static final int HOURS_TO_MILLISEC = 3600000;
/** Constant for local sidereal time */
public static final String LST = "LST";
/** Constant for UT time */
public static final String UT = "UT";
/** Constant for telescope site time */
public static final String SITE_TIME = "Site Time";
// Keys for saving and restoring user settings between sessions
static final String SITE_PREF_KEY = ElevationPlotModel.class.getName() + ".site";
static final String TIMEZONE_DISPLAY_NAME_PREF_KEY = ElevationPlotModel.class.getName() + ".timeZoneDisplayName";
static final String TIMEZONE_ID_PREF_KEY = ElevationPlotModel.class.getName() + ".timeZoneId";
// The name and location of the observatory site
private SiteDesc _site;
// Offset in hours (0..24) from UT midnight to noon at the telescope site
// private double _siteOffset;
// The starting date for the plot
private Date _startDate;
// The end date for the plot
private Date _endDate;
// The target objects, whose elevations are being plotted
private TargetDesc[] _targets;
// the X values are UT or LST dates
private Date[][] _xDate;
// Used to format the dates in the table
private SimpleDateFormat _dateFormat = new SimpleDateFormat("HH:mm:ss");
// the Y values are elevation (angle between 0 and 90 degrees)
private double[][] _yData;
// the secondary Y axis values are airmass
private double[][] _yDataAirmass;
// the other secondary Y axis values are the parallectic angles
private double[][] _yDataPa;
// For each target index, contains the approximate max elevation
private double[] _maxElevation;
// For each target index, contains the approximate time of the max elevation in ms
private double[] _maxElevationTime;
// The time zone display name to use for the X values
private String _timeZoneDisplayName = UT;
// The time zone id to use for the X values
private String _timeZoneId = UT;
// The time zone for the X values
private TimeZone _timeZone = TimeZone.getTimeZone(UT);
// list of listeners for change events
private EventListenerList _listenerList = new EventListenerList();
// An array of table models corresponding to the targets
private ElevationPlotTableModel[] _tableModels;
// Threshold in degrees above the horizon where targets are considered observable
private static double _obsThreshold = 30.;
// Utility class responsible for elevation calculations
private ElevationPlotUtil _plotUtil;
// Utility class responsible for sunrise/sunset/twilight calculations
private SunRiseSet _sunRiseSet;
/**
* Initialize an elevation plot model for the given date, location, and target coordinates.
*
* @param site the name and location of the observatory site
* @param date the date for which the plot should be calculated
* @param targets an array of target object descriptions
* @param timeZoneDisplayName the display name for the time zone for the X axis
* @param timeZoneId the time zone id for the X axis (one of "UT", "LST", or some standard time zone id)
*/
public ElevationPlotModel(SiteDesc site, Date date, TargetDesc[] targets,
String timeZoneDisplayName, String timeZoneId) {
_setSite(site);
_setDate(date);
_setTargets(targets);
_setTimeZone(timeZoneDisplayName, timeZoneId);
_updateModel();
}
/**
* Initialize an elevation plot model for the given date, location, and target coordinates.
*
* @param site the name and location of the observatory site
* @param date the date for which the plot should be calculated
* @param targets an array of target object descriptions
*/
public ElevationPlotModel(SiteDesc site, Date date, TargetDesc[] targets) {
this(site, date, targets, UT, UT);
}
/**
* Set the sample interval for the plot.
*/
public void setSampleInterval(int minutes) {
ElevationPlotUtil.setDefaultNumSteps((24*60)/minutes);
_updateModel();
_fireChangeEvent();
}
/** Return a title based on the site and the date */
public String getTitle() {
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy MMM d");
return _site.getName() + ": Night of "
+ dateFormat.format(_startDate)
+ " ---> "
+ dateFormat.format(_endDate);
}
/** Return the threshold in degrees above the horizon where targets are considered observable */
public static double getObsThreshold() {
return _obsThreshold;
}
/** Set the threshold in degrees above the horizon where targets are considered observable */
public static void setObsThreshold(double d) {
_obsThreshold = d;
}
// Update the model data based on the current settings
private void _updateModel() {
_plotUtil = new ElevationPlotUtil(_startDate, _site, _targets);
int numSteps = _plotUtil.getNumSteps();
_maxElevation = new double[_targets.length];
_maxElevationTime = new double[_targets.length];
_tableModels = new ElevationPlotTableModel[_targets.length];
for (int i = 0; i < _targets.length; i++)
_tableModels[i] = new ElevationPlotTableModel(i);
_sunRiseSet = new SunRiseSet(_startDate, _site);
_xDate = _plotUtil.getXData();
_yData = _plotUtil.getYData();
_yDataAirmass = _plotUtil.getYDataAirmass();
_yDataPa = _plotUtil.getYDataPa();
// Determine the min and max elevations for each target
for (int i = 0; i < _targets.length; i++) {
double[] el = _yData[i]; // elevation at each sample
_maxElevation[i] = -99.;
_maxElevationTime[i] = 0.;
for (int j = 0; j < numSteps; j++) {
if (el[j] > _maxElevation[i]) {
_maxElevation[i] = el[j];
_maxElevationTime[i] = _xDate[i][j].getTime();
}
}
}
}
/** Return the name and location of the observatory site */
public SiteDesc getSite() {
return _site;
}
/** Set the observatory site description */
public void setSite(SiteDesc site) {
_setSite(site);
if (_timeZoneId.equals(SITE_TIME)) {
_setTimeZone(site.getTimeZone().getDisplayName(), SITE_TIME);
}
_setDate(_startDate);
_updateModel();
_fireChangeEvent();
}
// Set the observatory site
private void _setSite(SiteDesc site) {
_site = site;
Preferences.set(SITE_PREF_KEY, _site.getName());
}
/** Return the time zone display name to use for the X values */
public String getTimeZoneDisplayName() {
return _timeZoneDisplayName;
}
/** Return the time zone id to use for the X values */
public String getTimeZoneId() {
return _timeZoneId;
}
/** Return the time zone to use for the X values */
public TimeZone getTimeZone() {
return _timeZone;
}
/**
* Set the time zone to use to display the X values.
* @param timeZoneDisplayName name to display for the time zone
* @param timeZoneId the id of the time zone
*/
public void setTimeZone(String timeZoneDisplayName, String timeZoneId) {
_setTimeZone(timeZoneDisplayName, timeZoneId);
_fireChangeEvent();
}
// Set the time zone to use to display the X values
private void _setTimeZone(String timeZoneDisplayName, String timeZoneId) {
_timeZoneDisplayName = timeZoneDisplayName;
_timeZoneId = timeZoneId;
Preferences.set(TIMEZONE_DISPLAY_NAME_PREF_KEY, _timeZoneDisplayName);
Preferences.set(TIMEZONE_ID_PREF_KEY, _timeZoneId);
_timeZone = ElevationPlotUtil.UT;
if (! _timeZoneId.equals(LST) && ! _timeZoneId.equals(UT)) {
_timeZone = _site.getTimeZone();
}
_dateFormat.setTimeZone(_timeZone);
}
/** Return the date for the plot */
public Date getDate() {
return _startDate;
}
/** Set the date of the plot */
public void setDate(Date date) {
_setDate(date);
_updateModel();
_fireChangeEvent();
}
/**
* Set the starting date of the plot and enforce a base starting time in the Date
* object of 12:00 noon at the telescope site
*/
private void _setDate(Date date) {
Calendar cal = Calendar.getInstance(_site.getTimeZone());
cal.setTime(date);
cal.set(Calendar.HOUR_OF_DAY, 12); // noon
cal.set(Calendar.MINUTE, 0);
cal.set(Calendar.SECOND, 0);
_startDate = cal.getTime();
cal.add(Calendar.HOUR_OF_DAY, 24); // noon next day
_endDate = cal.getTime();
}
// Notify all DatasetChangeListeners that the model changed
private void _fireChangeEvent() {
// Notify the model's change listeners
ChangeEvent changeEvent = new ChangeEvent(this);
Object[] listeners = _listenerList.getListenerList();
for (int i = listeners.length - 2; i >= 0; i -= 2) {
if (listeners[i] == ChangeListener.class) {
((ChangeListener) listeners[i + 1]).stateChanged(changeEvent);
}
}
// Fire change events for the tables
for (ElevationPlotTableModel _tableModel : _tableModels)
_tableModel.fireTableModelEvent();
}
/** Return the number of target objects returned by {@link #getTargets}. */
public int getNumTargets() {
if (_targets == null)
return 0;
return _targets.length;
}
/** Return an array of target objects, whose elevations are being plotted */
public TargetDesc[] getTargets() {
return _targets;
}
/** Set the array of target objects, whose elevations are being plotted */
public void setTargets(TargetDesc[] targets) {
_setTargets(targets);
_updateModel();
_fireChangeEvent();
}
// Set the array of target objects, whose elevations are being plotted
private void _setTargets(TargetDesc[] targets) {
_targets = targets;
}
/** Return the TableModel corresponding to the given index in the array returned from {@link #getTargets}. */
public TableModel getTableModel(int targetIndex) {
return _tableModels[targetIndex];
}
/** Return an XYDataset for this model that shows altitude against time */
public XYDataset getXYDataset() {
TimeSeriesCollection tsc = new TimeSeriesCollection(_timeZone);
int numSteps = _plotUtil.getNumSteps();
for (int i = 0; i < _targets.length; i++) {
TimeSeries ts = new TimeSeries(_targets[i].getName(), Second.class);
Date[] times = _xDate[i];
double[] elevations = _yData[i];
for (int j = 0; j < numSteps; j++) {
ts.add(new Second(times[j], _timeZone), elevations[j]);
}
tsc.addSeries(ts);
}
return tsc;
}
/** Return an XYDataset for this model that shows parallactic angle against time */
public XYDataset getSecondaryDataset() {
TimeSeriesCollection tsc = new TimeSeriesCollection(_timeZone);
int numSteps = _plotUtil.getNumSteps();
for (int i = 0; i < _targets.length; i++) {
TimeSeries ts = new TimeSeries(_targets[i].getName(), Second.class);
Date[] times = _xDate[i];
double[] pa = _yDataPa[i];
for (int j = 0; j < numSteps; j++) {
ts.add(new Second(times[j], _timeZone), pa[j]);
}
tsc.addSeries(ts);
}
return tsc;
}
/** Return the approximate maximum elevation for the given target index */
public double getMaxElevation(int targetIndex) {
return _maxElevation[targetIndex];
}
/** Return the approximate time in ms of the maximum elevation for the given target index */
public double getMaxElevationTime(int targetIndex) {
return _maxElevationTime[targetIndex];
}
/** Return an array of the available categories */
public String[] getCategories() {
TreeSet<String> set = new TreeSet<String>();
for (TargetDesc _target : _targets) {
set.add(_target.getCategory());
}
String[] result = new String[set.size()];
set.toArray(result);
return result;
}
/** Return an IntervalCategoryDataset for this model for the given category */
public IntervalCategoryDataset getIntervalCategoryDataset(String category) {
TaskSeriesCollection collection = new TaskSeriesCollection();
TaskSeries taskSeries = new TaskSeries("Targets");
// calculate the target description field widths, so they can be lined up in columns
int[] maxWidths = _calculateTargetDescriptionColumnWidths(_targets);
int totalWidth = 0;
for (int maxWidth : maxWidths) {
totalWidth += maxWidth;
}
// make the dataset
for (int i = 0; i < _targets.length; i++) {
if (_targets[i].getCategory().equals(category)) {
String name = _getTargetDescription(_targets[i], maxWidths, totalWidth);
Date[] times = _xDate[i];
double[] elevations = _yData[i];
Date[] xTimes = _findCrossingPoints(times, elevations);
if (xTimes.length == 2) {
// simple range
Task task = new Task(name, new SimpleTimePeriod(xTimes[0], xTimes[1]));
taskSeries.add(task);
} else {
// range is split and wraps around graph
Task task = new Task(name, new SimpleTimePeriod(xTimes[0], xTimes[xTimes.length - 1]));
int n = xTimes.length / 2;
for (int j = 0; j < n; j++) {
Task subtask = new Task(name, new SimpleTimePeriod(xTimes[j * 2], xTimes[j * 2 + 1]));
task.addSubtask(subtask);
}
taskSeries.add(task);
}
}
}
collection.add(taskSeries);
return collection;
}
// calculate the target description field widths, so they can be lined up in columns
private int[] _calculateTargetDescriptionColumnWidths(TargetDesc[] targets) {
String[] ar = targets[0].getDescriptionFields();
int numDesc = ar.length;
int[] maxWidths = new int[numDesc];
for (int j = 0; j < numDesc; j++)
maxWidths[j] = ar[j].length();
for (int i = 1; i < targets.length; i++) {
ar = targets[i].getDescriptionFields();
for (int j = 0; j < numDesc; j++)
if (ar[j].length() > maxWidths[j])
maxWidths[j] = ar[j].length();
}
return maxWidths;
}
// Return a formatted string describing the given target, using the given field widths
private String _getTargetDescription(TargetDesc target, int[] maxWidths, int totalWidth) {
String[] ar = target.getDescriptionFields();
StringBuilder sb = new StringBuilder(totalWidth);
for (int i = 0; i < maxWidths.length; i++) {
sb.append(StringUtil.pad(ar[i], maxWidths[i], true));
if (i != maxWidths.length - 1)
sb.append(' ');
}
return sb.toString();
}
/** Return the LST date for the given UT date */
public Date getLst(Date date) {
return _plotUtil.getLst(date);
}
/** Return the minimum date value */
public Date getStartDate() {
return _startDate;
}
/** Return the maximum date value */
public Date getEndDate() {
return _endDate;
}
/** Return the date/time given the UT hour between 0 and 24 */
public Date getDateForHour(double hour) {
Calendar cal = Calendar.getInstance(ElevationPlotUtil.UT);
cal.setTime(_startDate);
int h = cal.get(Calendar.HOUR_OF_DAY);
boolean nextDay = (hour < h);
CalendarUtil.setHours(cal, hour, nextDay);
return cal.getTime();
}
/** Return the official sunset time in the selected time zone */
public Date getSunset() {
return _sunRiseSet.getSunset();
}
/** Return the official sunrise time in the selected time zone */
public Date getSunrise() {
return _sunRiseSet.getSunrise();
}
/** Return the start time of nautical twilight in the selected time zone */
public Date getNauticalTwilightStart() {
return _sunRiseSet.getNauticalTwilightStart();
}
/** Return the end time of nautical twilight in the selected time zone */
public Date getNauticalTwilightEnd() {
return _sunRiseSet.getNauticalTwilightEnd();
}
/** Return the start time of astronomical twilight in the selected time zone */
public Date getAstronomicalTwilightStart() {
return _sunRiseSet.getAstronomicalTwilightStart();
}
/** Return the end time of astronomical twilight in the selected time zone */
public Date getAstronomicalTwilightEnd() {
return _sunRiseSet.getAstronomicalTwilightEnd();
}
// Return the index of the lowest value in the given array, assuming that the array
// contains values in increasing order, but may wrap around
private int _getOffset(Date[] ar) {
Date val = ar[0];
for (int i=0; i<ar.length; i++) {
if (ar[i].compareTo(val) < 0) return i;
val = ar[i];
}
return 0;
}
// Return an array of date/time objects indicating the points in the given arrays
// where the elevation crosses _obsThreshold. The return array will contain an even
// number of dates: where the first date of each pair is the time the star goes above
// the threshold, and the second is when it goes below. Since we are only looking
// at one day, the beginning and end of the day are treated specially, if
// the elevation is above the threshold there.
// The return array is also sorted in increasing order of time.
private Date[] _findCrossingPoints(Date[] times, double[] elevations) {
// used to get times in increasing order
int offset = _getOffset(times);
List l = new ArrayList();
boolean started = false;
// start interval
double val = elevations[offset];
if (val > _obsThreshold) {
l.add(times[offset]);
started = true;
}
int n = elevations.length - 1;
for (int i = 1; i < n; i++) {
int index = (offset + i) % elevations.length;
val = elevations[index];
if (started == false && val > _obsThreshold) {
l.add(times[index]);
started = true;
} else if (started == true && val <= _obsThreshold) {
l.add(times[index]);
started = false;
}
}
// close out any interval
if (started)
l.add(times[(offset + n) % elevations.length]);
// make return array from list
Date[] ar;
if (l.size() != 0) {
ar = new Date[l.size()];
l.toArray(ar);
} else {
// no crossing points: add dummy start and stop point at 0
ar = new Date[2];
ar[0] = ar[1] = times[0];
}
return ar;
}
/**
* register to receive change events from this object whenever the
* model changes.
*/
public void addChangeListener(ChangeListener l) {
_listenerList.add(ChangeListener.class, l);
}
/**
* Stop receiving change events from this object.
*/
public void removeChangeListener(ChangeListener l) {
_listenerList.remove(ChangeListener.class, l);
}
// Local table model class, used to view the graph data for a single target as a table
private class ElevationPlotTableModel implements TableModel {
private int _targetIndex;
private EventListenerList _tableListenerList = new EventListenerList();
public ElevationPlotTableModel(int targetIndex) {
_targetIndex = targetIndex;
}
public int getRowCount() {
if (_xDate == null)
return 0;
return _xDate[_targetIndex].length;
}
public int getColumnCount() {
return 4;
}
public String getColumnName(int columnIndex) {
switch (columnIndex) {
case 0:
return "Time (hours)";
case 1:
return "Elevation (degrees)";
case 2:
return "Airmass";
case 3:
return "Parallactic Angle";
}
throw new IndexOutOfBoundsException("columnIndex");
}
public Class getColumnClass(int columnIndex) {
if (columnIndex == 0) {
return String.class;
}
return Double.class;
}
public boolean isCellEditable(int rowIndex, int columnIndex) {
return false;
}
public Object getValueAt(int rowIndex, int columnIndex) {
switch (columnIndex) {
case 0:
Date date = _xDate[_targetIndex][rowIndex];
if (_timeZoneId.equals(LST)) {
date = _plotUtil.getLst(date);
}
return _dateFormat.format(date);
case 1:
return new Double(_yData[_targetIndex][rowIndex]);
case 2:
return new Double(_yDataAirmass[_targetIndex][rowIndex]);
case 3:
return new Double(_yDataPa[_targetIndex][rowIndex]);
}
throw new IndexOutOfBoundsException("columnIndex");
}
public void setValueAt(Object aValue, int rowIndex, int columnIndex) {
}
public void fireTableModelEvent() {
TableModelEvent changeEvent = new TableModelEvent(this);
Object[] listeners = _tableListenerList.getListenerList();
for (int i = listeners.length - 2; i >= 0; i -= 2) {
if (listeners[i] == TableModelListener.class) {
((TableModelListener) listeners[i + 1]).tableChanged(changeEvent);
}
}
}
public void addTableModelListener(TableModelListener l) {
_tableListenerList.add(TableModelListener.class, l);
}
public void removeTableModelListener(TableModelListener l) {
_tableListenerList.remove(TableModelListener.class, l);
}
}
}