// ********************************************************************** // // <copyright> // // BBN Technologies, a Verizon Company // 10 Moulton Street // Cambridge, MA 02138 // (617) 873-8000 // // Copyright (C) BBNT Solutions LLC. All rights reserved. // // </copyright> // ********************************************************************** // // $Source: /cvs/distapps/openmap/src/openmap/com/bbn/openmap/time/Clock.java,v $ // $RCSfile: Clock.java,v $ // $Revision: 1.1 $ // $Date: 2007/09/25 17:30:35 $ // $Author: dietrick $ // // ********************************************************************** package com.bbn.openmap.time; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import java.beans.beancontext.BeanContextChildSupport; import java.io.Serializable; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.Properties; import java.util.Vector; import javax.swing.Timer; import com.bbn.openmap.OMComponent; import com.bbn.openmap.util.Debug; import com.bbn.openmap.util.PropUtils; /** * The Clock is a controller that manages a Timer in order to support the notion * of a time range and a list of objects that can contribute to that time range. * The clock can count forward and backward, can wrap around the time limits, * and can be set to any time between the time range limits. The clock sends out * time notifications as PropertyChangeEvents. */ public class Clock extends OMComponent implements RealTimeHandler, ActionListener, PropertyChangeListener, TimeBoundsHandler, Serializable { public final static int DEFAULT_TIME_INTERVAL = 1000; /** * timeFormat, used for the times listed in properties for rates/pace. */ public final static String TimeFormatProperty = "timeFormat"; /** * TimeFormat default is similar to IETF standard date syntax: "13:30:00" * represented by (HH:mm:ss). */ protected SimpleDateFormat timeFormat = new SimpleDateFormat("HH:mm:ss"); protected Timer timer; protected long startTime = Long.MAX_VALUE; protected long endTime = Long.MIN_VALUE; protected long time = 0; /** * The timeIncrement is the amount of time that passes for each clock tick. * This sets up the ratio for slow and fast motion changes for a set clock * update rate. Can be modified with the pace accessors. */ protected int timeIncrement = DEFAULT_TIME_INTERVAL; protected boolean timeWrap = false; protected int clockDirection = 1; protected List<TimerRateHolder> timerRates; protected transient List<TimeBoundsProvider> timeBoundsProviders; protected transient List<TimeBoundsListener> timeBoundsListeners; protected transient List<TimeEventListener> timeEventListeners; /** * The delay between timer pulses, in milliseconds. */ protected int updateInterval = DEFAULT_TIME_INTERVAL; public Clock() { // Created again with the peer not set, this allows the Clock to be // added to multiple MapHandlers. beanContextChildSupport = new BeanContextChildSupport(); createTimer(); timerRates = new LinkedList<TimerRateHolder>(); timeBoundsProviders = new Vector<TimeBoundsProvider>(); timeBoundsListeners = new Vector<TimeBoundsListener>(); timeEventListeners = new Vector<TimeEventListener>(); } // ////////////////////////// // RealTimeHandler methods. // ////////////////////////// /** * Set the real time clock interval between clock ticks, in milliseconds. */ public void setUpdateInterval(int delay) { updateInterval = delay; if (timer != null) { timer.setDelay(updateInterval); if (timer.isRunning()) { timer.restart(); } } } /** * Return the real time interval between clock ticks, in milliseconds. */ public int getUpdateInterval() { return updateInterval; } /** * Set the amount of simulation time that passes with each clock tick, in * milliseconds. */ public void setPace(int pace) { timeIncrement = pace; } /** * Get the amount of simulation time that passes with each clock tick, in * milliseconds. */ public int getPace() { return timeIncrement; } /** * Called to set the clock to a specific time, usually for jumps. * * @param t the time in unix epoch terms */ public void setTime(long t) { setTime(t, TimerStatus.UPDATE); } /** * The call to set the clock for all reasons. * * @param t the time in unix epoch terms * @param timeStatus TimerStatus indicating how the clock is changing. */ protected void setTime(long t, TimerStatus timeStatus) { // Catch interactive cycles and other duplications that may be // triggered from other gui components. if (t == time) return; if (Debug.debugging("clock")) { Debug.output("Clock.setTime: " + t + " for " + timeStatus); } time = t; fireClockUpdate(timeStatus); } /** * The method that delivers the current time status to the * TimeEventListeners. */ protected void fireClockUpdate(TimerStatus timerStatus) { fireUpdateTime(TimeEvent.create(this, // time, time - systemTime, simTime + time - systemTime, time, systemTime, simTime, timerStatus)); } /** * Get the current time. */ public long getTime() { return time; } /** * Method to call to start the timer. */ public void startClock() { if (!timer.isRunning()) { firePropertyChange(TIMER_STATUS, TimerStatus.STOPPED, (getClockDirection() > 0 ? TimerStatus.FORWARD : TimerStatus.BACKWARD)); fireClockUpdate(getClockDirection() > 0 ? TimerStatus.FORWARD : TimerStatus.BACKWARD); } if (Debug.debugging("clock")) { Debug.output("Clock: Starting clock"); } timer.restart(); } /** * Method to call to stop the timer. */ public void stopClock() { if (timer.isRunning()) { firePropertyChange(TIMER_STATUS, (getClockDirection() > 0 ? TimerStatus.FORWARD : TimerStatus.BACKWARD), TimerStatus.STOPPED); fireClockUpdate(TimerStatus.STOPPED); timer.stop(); } } /** * Set whether time increases or decreases when the clock is run. If * direction is zero or greater, clock runs forward. If direction is * negative, clock runs backward. */ public void setClockDirection(int direction) { TimerStatus oldDirection = clockDirection > 0 ? TimerStatus.FORWARD : TimerStatus.BACKWARD; if (direction >= 0) { clockDirection = 1; } else { clockDirection = -1; } TimerStatus newDirection = clockDirection > 0 ? TimerStatus.FORWARD : TimerStatus.BACKWARD; if (timer.isRunning()) { if (oldDirection != newDirection) { firePropertyChange(TIMER_STATUS, oldDirection, newDirection); fireClockUpdate(newDirection); } } } /** * Get whether time increases or decreases when the clock is run. If * direction is zero or greater, clock runs forward. If direction is * negative, clock runs backward. */ public int getClockDirection() { return clockDirection; } /** * Move the clock forward one time increment. */ public void stepForward() { changeTimeBy(timeIncrement, timeWrap, TimerStatus.STEP_FORWARD); } /** * Move the clock back one time increment. */ public void stepBackward() { changeTimeBy(-timeIncrement, timeWrap, TimerStatus.STEP_BACKWARD); } // ///// Convenience methods for ReadTimeHandler /** * Call setTime with the amount given added to the current time. The amount * should be negative if you are going backward through time. You need to * make sure manageGraphics is called for the map to update. * <p> * * @param amount to change the current time by, in milliseconds. */ protected void changeTimeBy(long amount) { changeTimeBy(amount, timeWrap, (amount >= 0 ? TimerStatus.FORWARD : TimerStatus.BACKWARD)); } /** * Call setTime with the amount given added to the current time. The amount * should be negative if you are going backward through time. You need to * make sure manageGraphics is called for the map to update. * * @param amount to change the current time by, in milliseconds. * @param wrapAroundTimeLimits if true, the time will be set as if the start * and end times of the scenario are connected, so that moving the * time past the time scale in either direction will put the time at * the other end of the scale. */ protected void changeTimeBy(long amount, boolean wrapAroundTimeLimits) { changeTimeBy(amount, wrapAroundTimeLimits, (amount >= 0 ? TimerStatus.FORWARD : TimerStatus.BACKWARD)); } /** * Call setTime with the amount given added to the current time. The amount * should be negative if you are going backward through time. You need to * make sure manageGraphics is called for the map to update. * * @param amount to change the current time by, in milliseconds. * @param wrapAroundTimeLimits if true, the time will be set as if the start * and end times of the scenario are connected, so that moving the * time past the time scale in either direction will put the time at * the other end of the scale. * @param timeStatus the string given to the TimeEvent to let everyone know * why the time is changing. Usually TIMER_TIME_STATUS if the timer * went off normally, or TIME_SET_STATUS if the time is being * specifically set to something. */ protected void changeTimeBy(long amount, boolean wrapAroundTimeLimits, TimerStatus timeStatus) { long oldTime = getTime(); long newTime; boolean stopClock = false; newTime = oldTime + amount; if (newTime > endTime || newTime < startTime) { if (wrapAroundTimeLimits) { newTime = (amount >= 0 ? startTime : endTime); } else { newTime = (amount >= 0 ? endTime : startTime); stopClock = true; } } if (Debug.debugging("clock")) { Debug.output("Clock " + (stopClock ? ("stopping clock at (" + newTime) : ("changing time by [" + amount + "] to (" + newTime)) + ") : " + timeStatus); } // Should set the new time before telling everyone the clock is stopped. setTime(newTime, timeStatus); if (stopClock) { stopClock(); } } // /////////////////////// // ActionListener method // /////////////////////// /** * ActionListener interface, gets called when the timer goes ping if there * isn't a command with the ActionEvent. Otherwise, the command should be * filled in. */ public void actionPerformed(ActionEvent ae) { if (ae.getSource() == getTimer()) { // Normal time change call. // changeTimeBy(timeIncrement * clockDirection); // Hacked version, so that the video can get events that // they will listen to if the clock is running backwards. changeTimeBy(timeIncrement * clockDirection, timeWrap, clockDirection < 0 ? TimerStatus.UPDATE : TimerStatus.FORWARD); } } // ////////////////////////////// // PropertyChangeListener method // ////////////////////////////// /** * PropertyChangeListener method called when the bounds on a * TimeBoundsProvider changes, so so that the range of times can be * adjusted. */ public void propertyChange(PropertyChangeEvent pce) { resetTimeBounds(); } // ////////////////////////////// // TimeBoundsProvider methods // ////////////////////////////// /** * Add a TimeBoundsProvider to the clock, so it knows the bounds of it's * time range. */ public void addTimeBoundsProvider(TimeBoundsProvider tbp) { if (!timeBoundsProviders.contains(tbp)) { timeBoundsProviders.add(tbp); resetTimeBounds(); } } public void removeTimeBoundsProvider(TimeBoundsProvider tbp) { timeBoundsProviders.remove(tbp); resetTimeBounds(); } public void clearTimeBoundsProviders() { timeBoundsProviders.clear(); resetTimeBounds(); } // ////////////////////////////// // TimeBoundsListener methods // ////////////////////////////// /** * Add a TimeBoundsListener to the clock, so it knows who to tell when the * time bounds change. */ public void addTimeBoundsListener(TimeBoundsListener tbl) { if (!timeBoundsListeners.contains(tbl)) { timeBoundsListeners.add(tbl); } } public void removeTimeBoundsListener(TimeBoundsListener tbl) { timeBoundsListeners.remove(tbl); } public void clearTimeBoundsListeners() { timeBoundsListeners.clear(); } public void fireUpdateTimeBounds(TimeBoundsEvent tbe) { if (timeBoundsListeners != null) { List<TimeBoundsListener> copy; synchronized(timeBoundsListeners) { copy = new ArrayList<TimeBoundsListener>(timeBoundsListeners); } for (Iterator<TimeBoundsListener> it = copy.iterator(); it.hasNext();) { it.next().updateTimeBounds(tbe); } } } // ////////////////////////////// // TimeEventListener methods // ////////////////////////////// /** * Add a TimeEventListener to the clock, so it knows who to update when the * time changes. */ public void addTimeEventListener(TimeEventListener tel) { if (!timeEventListeners.contains(tel)) { timeEventListeners.add(tel); } } public void removeTimeEventListener(TimeEventListener tel) { timeEventListeners.remove(tel); } public void clearTimeEventListeners() { timeEventListeners.clear(); } public void fireUpdateTime(TimeEvent te) { if (timeEventListeners != null) { List<TimeEventListener> copy; synchronized(timeEventListeners) { copy = new ArrayList<TimeEventListener>(timeEventListeners); } for (Iterator<TimeEventListener> it = copy.iterator(); it.hasNext();) { it.next().updateTime(te); } } } // ////////////////////////////// // Generic Clock Methods // ////////////////////////////// /** * Method to call when TimeBoundsProviders change, in order to query them * and figure out what the new time range is. */ public void resetTimeBounds() { TimeBounds oldtb = new TimeBounds(startTime, endTime); startTime = Long.MAX_VALUE; endTime = Long.MIN_VALUE; int activeTimeBoundsProviderCount = 0; List<TimeBoundsProvider> copy; synchronized(timeBoundsProviders) { copy = new ArrayList<TimeBoundsProvider>(timeBoundsProviders); } for (Iterator<TimeBoundsProvider> it = copy.iterator(); it.hasNext();) { TimeBoundsProvider tbp = it.next(); if (tbp.isActive()) { activeTimeBoundsProviderCount++; TimeBounds bounds = tbp.getTimeBounds(); if (bounds != null && !bounds.isUnset()) { addTime(bounds.getStartTime()); addTime(bounds.getEndTime()); if (Debug.debugging("clock")) { Debug.output("Clock.resetTimeBounds(" + tbp.getClass().getName() + ") adding " + bounds); } } } else { if (Debug.debugging("clock")) { Debug.output("Clock.resetTimeBounds(" + tbp.getClass().getName() + ") not active"); } } } // system time is startTime, let other components track their // relative system time if it is important to them. systemTime = startTime; /* * First thing, let all the TimeBoundsProviders know what the overall * TimeBounds is, in case they need to update their GUI or something. */ TimeBounds tb = new TimeBounds(startTime, endTime); for (Iterator<TimeBoundsProvider> it = copy.iterator(); it.hasNext();) { it.next().handleTimeBounds(tb); } long currentTime = time; // If the number of activeTimeBoundsProviders is zero, reset the clock // to the startTime, which should be Long.MAX_TIME, and this should // reset it when some TimeBoundsProvider is made active.x if (activeTimeBoundsProviderCount == 0) { setTime(startTime); } else if (currentTime < startTime || currentTime == Long.MAX_VALUE) { setTime(startTime); } else if (currentTime > endTime) { setTime(endTime); } /* * Now, update the TimeBoundsListeners, so they can update their GUIs. */ fireUpdateTimeBounds(new TimeBoundsEvent(this, tb, oldtb)); if (tb.isUnset()) { fireUpdateTime(TimeEvent.NO_TIME); } } /** * Add a time to the time range, fire a TimeBoundsEvent if the time range * changes. * * @param timeStamp in milliseconds */ public void addTimeToBounds(long timeStamp) { long oldStartTime = startTime; long oldEndTime = endTime; addTime(timeStamp); if (oldStartTime != startTime || oldEndTime != endTime) { fireUpdateTimeBounds(new TimeBoundsEvent(this, new TimeBounds(oldStartTime, oldEndTime), new TimeBounds(startTime, endTime))); } } /** * Add a time to the time range. * * @param timeStamp in milliseconds */ protected void addTime(long timeStamp) { if (timeStamp < startTime) { if (Debug.debugging("clock")) { Debug.output("Clock: setting startTime: " + timeStamp); } startTime = timeStamp; } if (timeStamp > endTime) { if (Debug.debugging("clock")) { Debug.output("Clock: setting endTime: " + timeStamp); } endTime = timeStamp; } // This is actually resetting the time even if it doesn't need to be // reset, since startTime and endTime really can be in the middle of // being redefined. The time check should happen after all times have // been added, the old and new system times have been checked, and then // we'll have a better idea of where time should be. // if (time < startTime) { // time = (long) startTime; // } else if (time > endTime) { // time = (long) endTime; // } // else, leave it alone... } public long getStartTime() { return startTime; } public long getEndTime() { return endTime; } protected long systemTime = 0; protected long simTime = 0; /** * Set the system time and simulation time. These times are used in relation * to any offsets. System time usually gets reset when the time bounds are * reset. * * @param sysTime the system (computer) time used in TimeEvents. * @param simulationTime the scenario time used in TimeEvents. */ public void setBaseTimesForTimeEvent(long sysTime, long simulationTime) { systemTime = sysTime; simTime = simulationTime; } public long getSimTime() { return simTime; } public long getSystemTime() { return systemTime; } // ////////////////////////// // Timer management methods // ////////////////////////// /** * Get the timer being used for automatic updates. May be null if a timer is * not set. */ public Timer getTimer() { return timer; } /** * If you want the layer to update itself at certain intervals, you can set * the timer to do that. Set it to null to disable it. If the current timer * is not null, the graphic loader is removed as an ActionListener. If the * new one is not null, the graphic loader is added as an ActionListener. */ public void setTimer(Timer t) { if (timer != null) { timer.stop(); timer.removeActionListener(this); } timer = t; if (timer != null) { timer.removeActionListener(this); timer.addActionListener(this); } } /** * Creates a timer with the current updateInterval and calls setTimer(). */ public void createTimer() { Timer t = new Timer(updateInterval, this); t.setInitialDelay(0); setTimer(t); } /** * Get a list of TimerRateHolders. */ public List<TimerRateHolder> getTimerRates() { return timerRates; } /** * Make sure the List contains TimerRateHolders. */ public void setTimerRates(List<TimerRateHolder> rates) { timerRates = rates; } // //////////////////// // OMComponent methods // //////////////////// /** * @param prefix string prefix used in the properties file for this * component. * @param properties the properties set in the properties file. */ public void setProperties(String prefix, Properties properties) { super.setProperties(prefix, properties); prefix = PropUtils.getScopedPropertyPrefix(prefix); String timeFormatString = properties.getProperty(prefix + TimeFormatProperty, ((SimpleDateFormat) timeFormat).toPattern()); timeFormat = new SimpleDateFormat(timeFormatString); timerRates = TimerRateHolder.getTimerRateHolders(prefix, properties); } /** * OMComponent method, called when new components are added to the * MapHandler. Lets the Clock find TimeBoundsProviders that have been added * to the application, so that the Clock can register itself as a * PropertyChangeListener. */ public void findAndInit(Object someObj) { super.findAndInit(someObj); if (someObj instanceof TimeBoundsProvider) { if (Debug.debugging("clock")) { Debug.output("Clock.findAndInit(TimeBoundsProvider): " + someObj.getClass().getName()); } addTimeBoundsProvider((TimeBoundsProvider) someObj); } } /** * OMComponent method, called when new components are removed from the * MapHandler. Lets the Clock unregister itself as PropertyChangeListener to * TimeBoundsProviders. */ public void findAndUndo(Object someObj) { super.findAndUndo(someObj); if (someObj instanceof TimeBoundsProvider) { removeTimeBoundsProvider((TimeBoundsProvider) someObj); } } /** * Adds a PropertyChangeListener to this Clock, so that object can receive * time PropertyChangeEvents. */ public void addPropertyChangeListener(PropertyChangeListener pcl) { if (Debug.debugging("clock")) { Debug.output("Clock: adding property change listener"); } super.addPropertyChangeListener(TIMER_STATUS, pcl); initializePropertyChangeListener(pcl); } public void addPropertyChangeListener(String propertyName, PropertyChangeListener pcl) { super.addPropertyChangeListener(propertyName, pcl); initializePropertyChangeListener(pcl); } /** * Fires Propertyevents to new PropertyChangeListeners so they get the * latest info. */ protected void initializePropertyChangeListener(PropertyChangeListener pcl) { TimerStatus runningStatus = timer.isRunning() ? (getClockDirection() > 0 ? TimerStatus.FORWARD : TimerStatus.BACKWARD) : TimerStatus.STOPPED; firePropertyChange(TIMER_STATUS, null, runningStatus); fireClockUpdate(runningStatus); } /** * @return true if timer is running. */ public boolean isRunning() { return timer.isRunning(); } }