From 8b5df70e206dc0c846afc79e22ea91da811a0cc4 Mon Sep 17 00:00:00 2001
From: brutzman <brutzman@nps.edu>
Date: Sun, 23 Jan 2022 14:32:12 -0800
Subject: [PATCH] numerous refactoring improvements, add self test

---
 src/edu/nps/moves/dis7/utilities/DisTime.java | 246 ++++++++++++++----
 .../nps/moves/dis7/utilities/DisTimeLog.txt   |  21 ++
 2 files changed, 215 insertions(+), 52 deletions(-)
 create mode 100644 src/edu/nps/moves/dis7/utilities/DisTimeLog.txt

diff --git a/src/edu/nps/moves/dis7/utilities/DisTime.java b/src/edu/nps/moves/dis7/utilities/DisTime.java
index 8c43c51511..9a551169e1 100644
--- a/src/edu/nps/moves/dis7/utilities/DisTime.java
+++ b/src/edu/nps/moves/dis7/utilities/DisTime.java
@@ -12,57 +12,81 @@ import java.text.SimpleDateFormat;
 import java.util.*;
 
 /**
+ * <p>This common shared class provides static code for timestamp configuration and conversion utilities,
+ * consistently supporting all active open-dis7-java simulations running together on a localhost. 
+ * Multiple timestamp configurations are available, but dissimilar forms within a single simulation
+ * are considered counterproductive and impractical to manage coherently.</p>
+ *
  * <p>DIS time units are a pain to work with. As specified by the IEEE DIS Protocol specification, 
  * DIS time units are defined in a custom manner and set
  * equal to 2^31 - 1 time units per hour. The DIS time is set to the number of time
  * units since the start of the hour. Rollover problems can easily occur.  The timestamp field in the PDU header is
  * four bytes long and is specified to be an unsigned integer value.</p>
  *
- * <p>Additionally, there are two types of official timestamps in the PDU header: 
- * <i>absolute time</i> and <i>relative time</i>. Absolute time is used when the host is synchronized to 
- * <a href="https://en.wikipedia.org/wiki/Coordinated_Universal_Time" target="_blank">Coordinated Universal Time (UTC)</a>, i.e. the host
- * is accurately synchronized with UTC via <a href="https://en.wikipedia.org/wiki/Network_Time_Protocol" target="_blank">Network Time Protocol (NTP)</a>.
+ * <p>Multiple timestamp styles and settings are available.</p>
+ *
+ * <p><i>Absolute time</i> and <i>Relative time</i> are two variations for the 
+ * official timestamp value  in the PDU header. 
+ * Absolute time indicates that the localhost is synchronized to 
+ * <a href="https://en.wikipedia.org/wiki/Coordinated_Universal_Time" target="_blank">Coordinated Universal Time (UTC)</a>, 
+ * typically meaning that the local computer system is accurately synchronized with UTC via
+ * <a href="https://en.wikipedia.org/wiki/Network_Time_Protocol" target="_blank">Network Time Protocol (NTP)</a>.
+ * Synchronization might also be achieved when a computer has a highly accurate reference clock (such as GPS).
  * The packet timestamps originating from such hosts can be legitimately
  * compared to the timestamp of packets received from other hosts, since they all are
- * referenced to the same universal time.</p>
+ * referenced to the same universal time.  
+ * Relative timestamps may require further processing in order to achieve synchronization</p>
  *
  * <p><b>Absolute timestamps</b> have their least significant bit (LSB) set to 1, and relative timestamps have their
  * LSB set to 0. The idea in the DIS specification is to get the current time since the top of the hour,
  * divide by 2^31-1, shift left one bit, then set the LSB to either 0 for relative
  * timestamps or 1 for absolute timestamps.</p>
  *
- * <p><b>Relative timestamps</b> are used when the host does NOT have access to NTP, and hence
+ * <p><b>Relative timestamps</b> are indicated when the host does NOT have access to NTP, and hence
  * the system time might not be coordinated with that of other hosts. This means that
  * a host receiving DIS packets from several hosts might have to set up a per-host
  * table to correlate baseline time references before ordering packets, and that 
  * the PDU timestamp fields from one host is not
- * directly comparable to the PDU timestamp field from another host.</p>
+ * directly comparable to the PDU timestamp field from another host.
+ * (TODO: such support for correlating unsynchronized clocks is not yet implemented by this library.)</p>
  *
- * <p>The nature of shared DIS data is such that the timestamp values <i>roll over</i> once an
+ * <p>Another difficulty with the DIS standard has serious effects.
+ * The nature of shared DIS data is such that the timestamp values <i>roll over</i> once an
  * hour, and simulations must be prepared for that eventuality. In other words, at the top of the hour
  * outgoing PDUs will have a timestamp of 1, then just before the end of the hour the
  * PDUs will have a timestamp of 2^31 - 1, and then they will roll back over to a value of 1.
  * Receiving applications should expect this behavior, and not simply expect a
- * monotonically increasing timestamp field.</p>
+ * monotonically increasing timestamp field.
+ * Two nonstandard timestamp alternatives follow.</p>
  *
  * <p><b>Unix time</b>. Note that many applications in the wild have been known to completely ignore
- * the standard and to simply put commonly used <a href="https://en.wikipedia.org/wiki/Unix_time">Unix time</a> (seconds since 1 January 1970) into the
- * field. </p>
+ * the DIS standard and to simply put commonly used 
+ * <a href="https://en.wikipedia.org/wiki/Unix_time" target="_blank">Unix time</a> (seconds since 1 January 1970)
+ * into the timestamp field. </p>
  *
  * <p><b>Year time</b>.  The rollover associated with official DIS timestamps don't work all that well in numerous applications,
  * which often expect a monotonically increasing timestamp field.   Such unpredictable rollover variations are also incompatible
- * with archival recording or streaming playback of Live-Virtual-Constructive (LVC) behavior streams.
+ * with archival recording or streaming playback of 
+ * <a href="https://en.wikipedia.org/wiki/Live,_virtual,_and_constructive" target="_blank">Live-Virtual-Constructive (LVC)</a> behavior streams.
  * To avoid such problems, NPS created a "yearly" timestamp which measures 
  * hundredths of a second since the start of the current year. The maximum value for
  * such measurements is 3,153,600,000, which can fit into an unsigned int. 
  * One hundredth of a second resolution is accurate enough for most applications, and you typically don't have to worry about
  * rollover, instead getting only a monotonically increasing timestamp value.</p>
  *
- * <p><b>TODO: time 0.0</b>.  Functionality is needed to define a shared common time origin, and also to
+ * <p><b>TODO: timestamp normalization to an initial reference time.</b>
+ * Functionality is needed to define a shared common time origin (epoch) and also to
  * precisely adjust stream timestamps when coordinating recorded PDU playback within LVC applications.
  * We think the ability to "start at time 0.0", or normalizing initial time to zero 
- * for a recorded PDU stream, is actually a pretty common use case.</p>
- * <p> Don McGregor, Mike Bailey,  and Don Brutzman</p>
+ * for a recorded PDU stream, is actually a pretty common use case.  
+ * Implementing such a capability is under active development.</p>
+ *
+ * <p><b>TODO: upgrade to <code>java.time</code> package.</b> See
+ * <a href="https://docs.oracle.com/javase/tutorial/datetime/index.html" target="_blank">Java Tutorials: Date Time</a>
+ * and
+ * <a href="https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/time/package-summary.html" target="_blank">Java Package java.time</a>. </p>
+ * 
+ * <p> Don McGregor, Mike Bailey, and Don Brutzman</p>
  * 
  * @author DMcG
  */
@@ -75,9 +99,8 @@ import java.util.*;
 
 public class DisTime
 {
-
-    private       static Method getTimeMethod;
-    private final static DisTime disTime = new DisTime();
+    private       static Method  getTimeMethod;           // needed for reflection
+    private final static DisTime disTime = new DisTime(); // needed for reflection
   
     /** Supported timestamp styles and utility methods.
      * @see edu.nps.moves.dis7.utilities.PduFactory
@@ -92,6 +115,16 @@ public class DisTime
         /** hundreds of a second since the start of the year */
         YEAR
     };
+    
+    /** Whether host computer clock is accurately synchronized with UTC to a time standard, for example by using 
+     * <a href="https://en.wikipedia.org/wiki/Network_Time_Protocol" target="_blank">Network Time Protocol (NTP)</a>.
+     */
+    private static boolean hostClockSynchronized = true;
+    
+    /** Reference starting time for current timestamps
+     * <a href="https://en.wikipedia.org/wiki/Epoch_(computing)" target="_blank">https://en.wikipedia.org/wiki/Epoch_(computing)</a>.
+     */
+    private static int     epoch        = 0;
   
     /** We can marshal the PDU with a timestamp set to any of several styles. 
      * Remember, you MUST set a timestamp. DIS will regard multiple packets sent 
@@ -108,11 +141,13 @@ public class DisTime
     
     /** calendar instance */
     private static GregorianCalendar calendar;
+    
+    private static String dateFormatPattern = "HH:mm:ss";
 
    // public static DisTime disTime = null;
 
     /**
-     * Shared instance. This is not thread-safe. If you are working in multiple threads,
+     * Shared instance. This method is not thread-safe. If you are working in multiple threads,
      * create a new instance for each thread.
      * return singleton instance of DisTime
      */
@@ -121,7 +156,6 @@ public class DisTime
         if (disTime == null) {
             disTime = new DisTime();
         }
-
         return disTime;
     }
     */
@@ -132,8 +166,9 @@ public class DisTime
     }
 
     /**
-     * Returns the number of DIS time units since the top of the hour. there are 2^31-1 DIS time
-     * units per hour.
+     * For current system time, returns the number of DIS time units since the top of the hour, or else
+     * number of DIS time units since previously set epoch timestamp (for time-zero-based streams).
+     * Note that there are 2^31-1 DIS time units per hour.
      * @return integer DIS time units since the start of the hour.
      */
     private static synchronized int getCurrentDisTimeUnitsSinceTopOfHour()
@@ -147,19 +182,29 @@ public class DisTime
         calendar.set(Calendar.MINUTE, 0);
         calendar.set(Calendar.SECOND, 0);
         calendar.set(Calendar.MILLISECOND, 0);
-        long topOfHour = calendar.getTimeInMillis();
+        long topOfHourMsec = calendar.getTimeInMillis();
 
         // Milliseconds since the top of the hour
-        long diff = currentTime - topOfHour;
+        long timeDifferenceMsec; // originally diff = currentTime - topOfHour;
 
         // It turns out that Integer.MAX_VALUE is 2^31-1, which is the time unit value, ie there are
         // 2^31-1 DIS time units in an hour. 3600 sec/hr X 1000 msec/sec divided into the number of
         // msec since the start of the hour gives the percentage of DIS time units in the hour, times
         // the number of DIS time units per hour, equals the time value
-        double val = (diff / (3600.0 * 1000.0)) * Integer.MAX_VALUE;
-        int ts = (int) val;
-
-        return ts;
+        double differenceValue;
+        int    differenceTimestamp;
+        
+        if  (epoch == 0)
+        {
+            timeDifferenceMsec = currentTime - topOfHourMsec;
+        }
+        else // normalized time reference having 00:00 at start
+        {
+            timeDifferenceMsec = currentTime - epoch;
+        }
+        differenceValue     = (timeDifferenceMsec / (3600.0 * 1000.0)) * Integer.MAX_VALUE;
+        differenceTimestamp = (int) differenceValue;
+        return differenceTimestamp;
     }
 
     /**
@@ -170,22 +215,23 @@ public class DisTime
      */
     public static synchronized int getCurrentDisAbsoluteTimestamp()
     {
-         int val = getCurrentDisTimeUnitsSinceTopOfHour();
-         val = (val << 1) | ABSOLUTE_TIMESTAMP_MASK; // always flip the lsb to 1
-         return val;
+        int value = getCurrentDisTimeUnitsSinceTopOfHour();
+        value = (value << 1) | ABSOLUTE_TIMESTAMP_MASK; // always flip the lsb to 1
+        return value;
     }
 
     /**
-     * Checks local system clock and returns the current DIS standard relative timestamp, which should be used if this host
-     * is not slaved to NTP. Fix to bitshift by mvormelch
+     * Checks local system clock and returns the current DIS standard relative timestamp, 
+     * which should be used if this host is not synchronized to UTC.
+     * // Fix to bitshift by mvormelch
      * @see <a href="https://en.wikipedia.org/wiki/Network_Time_Protocol" target="_blank">Wikipedia: Network Time Protocol (NTP)</a>
      * @return DIS time units, relative
      */
     public static int getCurrentDisRelativeTimestamp()
     {
-        int val = getCurrentDisTimeUnitsSinceTopOfHour();
-        val = (val << 1) & RELATIVE_TIMESTAMP_MASK; // always flip the lsb to 0
-        return val;
+        int value = getCurrentDisTimeUnitsSinceTopOfHour();
+        value = (value << 1) & RELATIVE_TIMESTAMP_MASK; // always flip the lsb to 0
+        return value;
     }
 
     /**
@@ -234,40 +280,50 @@ public class DisTime
      *
      * Unix time (in seconds) rolls over in 2038. 
      *
-     * Consult the Wikipedia page on <a href="https://en.wikipedia.org/wiki/Unix_time">Unix time</a> for the gory details
+     * Consult the Wikipedia page on <a href="https://en.wikipedia.org/wiki/Unix_time" target="_blank">Unix time</a> for the gory details
      * @return seconds since 1970
      */
     public static synchronized int getCurrentUnixTimestamp()
     {
         long t = System.currentTimeMillis();
-        t /= 1000l;   // NB: integer division, convert milliseconds to seconds
+        t /= 1000l;   // NB: integer division used to convert milliseconds to seconds
         return (int) t;
     }
     
     /**
-     * Convert timestamp value to string for logging and diagnostics.
+     * Convert timestamp value to string for logging and diagnostics,
+     * taking into account epoch and TimeStampStyle (DIS absolute/relative, Unix or Year).
      * TODO consider different formats for different timestampStyle values.
      * @param timestamp value in milliseconds
      * @see GregorianCalendar
+     * @see DisTime.TimeStampStyle
      * @return string value provided by GregorianCalendar
      */
     public static String convertToString(int timestamp)
     {
         GregorianCalendar newCalendar = new GregorianCalendar();
-        newCalendar.setTimeInMillis(timestamp);
-        DateFormat formatter = new SimpleDateFormat("HH:mm:ss");
-        return formatter.format(newCalendar.getTime());
-    }
-  
-    /** Set one of four time references as timestampStyle: IEEE_ABSOLUTE, IEEE_RELATIVE, UNIX, or YEAR.
-     * @param newtTimestampStyle the timestamp style to set for this PDU
-     */
-    public static void setTimestampStyle(TimestampStyle newtTimestampStyle) {
-        timestampStyle = newtTimestampStyle;
-        setTimestampMethod();
+        DateFormat formatter = new SimpleDateFormat(dateFormatPattern);
+        
+        if      ((timestampStyle == TimestampStyle.IEEE_ABSOLUTE) || 
+                 (timestampStyle == TimestampStyle.IEEE_RELATIVE))
+        {
+            // if epoch is not set, this is regular DIS value
+            newCalendar.setTimeInMillis(timestamp - epoch);
+            return formatter.format(newCalendar.getTime());
+        }
+        else if (timestampStyle == TimestampStyle.UNIX) // TODO
+        {
+            newCalendar.setTimeInMillis(timestamp);
+            return formatter.format(newCalendar.getTime());
+        }
+        else // (timestampStyle == TimestampStyle.YEAR) // TODO
+        {
+            newCalendar.setTimeInMillis(timestamp);
+            return formatter.format(newCalendar.getTime());
+        }
     }
 
-    public static void setTimestampMethod()
+    private static void initializeTimestampMethod()
     {
         try {
             switch (timestampStyle)
@@ -306,12 +362,98 @@ public class DisTime
   {
     try {
         if (getTimeMethod == null)
-            setTimestampMethod(); // avoid NPE
+            initializeTimestampMethod(); // avoid NPE
         return (int) getTimeMethod.invoke(disTime, (Object[]) null);
     }
     catch (IllegalAccessException | InvocationTargetException ex) {
       throw new RuntimeException(ex);
     }
   }
+  
+    /** Set one of four time references as timestampStyle: IEEE_ABSOLUTE, IEEE_RELATIVE, UNIX, or YEAR.
+     * @param newtTimestampStyle the timestamp style to set for this PDU
+     */
+    public static void setTimestampStyle(TimestampStyle newtTimestampStyle)
+    {
+        timestampStyle = newtTimestampStyle;
+        initializeTimestampMethod();
+    }
+    /** Retrieve the current timestampStyle.
+     * @return the current timestampStyle
+     */
+    public static TimestampStyle getTimestampStyle()
+    {
+      return timestampStyle;
+    }
+    
+    /** Declare whether host computer clock is accurately synchronized with UTC to a time standard
+     * @param newhostClockSynchronized whether localhost is synchronized to time reference
+     */
+    public static void setHostClockSynchronized(boolean newhostClockSynchronized)
+    {
+        hostClockSynchronized = newhostClockSynchronized;
+    }
+    /**  Determine whether host computer clock is accurately synchronized with UTC to a time standard
+     * @return whether localhost is synchronized to time reference
+     */
+    public static boolean isHostClockSynchronized()
+    {
+      return hostClockSynchronized;
+    }
+    
+    /** Set epoch using current time for zero-based clock, meaning timestamps are normalized to "time zero" of simulation
+     * as initial starting time
+     */
+    public static void setEpochCurrentTimestamp()
+    {
+        setEpoch(getCurrentDisAbsoluteTimestamp());
+    }
+    
+    /** Set initial timestamp as epoch for zero-based clock, meaning timestamps normalized to 0 as initial starting time
+     * @param initialTimestamp first timestamp in series, considered time zero
+     */
+    public static void setEpoch(int initialTimestamp)
+    {
+        epoch = initialTimestamp;
+    }
+    /**  Get initial timestamp for zero-based clock, meaning all timestamps are measured with resepct to given starting time
+     * @return whether localhost is synchronized to time reference
+     */
+    public static int getEpoch()
+    {
+      return epoch;
+    }
+    /** Self-test for basic smoke testing */
+    private void selfTest()
+    {        
+        DisTime.initializeTimestampMethod();
+        System.out.println("DisTime.getTimestampStyle()                    = " + DisTime.getTimestampStyle());
+        System.out.println("                                               = " + dateFormatPattern);
+        int initialTimestamp = DisTime.getTimestamp();
+        System.out.println("DisTime.getTimestamp() initialTimestamp        = " + convertToString(initialTimestamp)                               + " = " + Integer.toUnsignedString(initialTimestamp)       + " = " + initialTimestamp      + " (unsigned vs signed output)");
+        System.out.println("DisTime.getTimestamp()                         = " + convertToString(DisTime.getTimestamp())                         + " = " + Integer.toUnsignedString(DisTime.getTimestamp()) + " = " + DisTime.getTimestamp()+ " (unsigned vs signed output)");
+        System.out.println("DisTime.getCurrentDisAbsoluteTimestamp()       = " + convertToString(DisTime.getCurrentDisAbsoluteTimestamp())       + " = " + Integer.toUnsignedString(DisTime.getCurrentDisAbsoluteTimestamp()));
+        System.out.println("DisTime.getCurrentDisRelativeTimestamp()       = " + convertToString(DisTime.getCurrentDisRelativeTimestamp())       + " = " + Integer.toUnsignedString(DisTime.getCurrentDisRelativeTimestamp()));
+        System.out.println("DisTime.getCurrentDisTimeUnitsSinceTopOfHour() = " + convertToString(DisTime.getCurrentDisTimeUnitsSinceTopOfHour()) + " = " + DisTime.getCurrentDisTimeUnitsSinceTopOfHour());
+        System.out.println("DisTime.getEpoch()                             = " + convertToString(DisTime.getEpoch())                             + " = " + DisTime.getEpoch());
+        setEpochCurrentTimestamp();
+        System.out.println("DisTime.setEpochCurrentTimestamp();");
+        System.out.println("DisTime.getEpoch()                             = " + convertToString(DisTime.getEpoch())                             + " = " + DisTime.getEpoch());
+        System.out.println("DisTime.getCurrentDisTimeUnitsSinceTopOfHour() = " + convertToString(DisTime.getCurrentDisTimeUnitsSinceTopOfHour()) + " = " + DisTime.getCurrentDisTimeUnitsSinceTopOfHour());
+   }
 
+    /**
+     * Main method for testing.
+     * @see <a href="https://docs.oracle.com/javase/tutorial/getStarted/application/index.html">Java Tutorials: A Closer Look at the "Hello World!" Application</a>
+     * @param args [unused] command-line arguments are an array of optional String parameters that are passed from execution environment during invocation
+     */
+    public static void main(String[] args)
+    {
+        System.out.println("*** DisTime.main() self test started...");
+
+        DisTime disTimeInstance = new DisTime();
+        disTimeInstance.selfTest();
+        
+        System.out.println("*** DisTime.main() self test complete.");
+    }
 }
diff --git a/src/edu/nps/moves/dis7/utilities/DisTimeLog.txt b/src/edu/nps/moves/dis7/utilities/DisTimeLog.txt
new file mode 100644
index 0000000000..c01445fb5c
--- /dev/null
+++ b/src/edu/nps/moves/dis7/utilities/DisTimeLog.txt
@@ -0,0 +1,21 @@
+ant -f C:\\x3d-github\\open-dis7-java -Dnb.internal.action.name=run.single -Djavac.includes=edu/nps/moves/dis7/utilities/DisTime.java -Drun.class=edu.nps.moves.dis7.utilities.DisTime run-single
+init:
+Deleting: C:\x3d-github\open-dis7-java\build\built-jar.properties
+deps-jar:
+Updating property file: C:\x3d-github\open-dis7-java\build\built-jar.properties
+Compiling 1 source file to C:\x3d-github\open-dis7-java\build\classes
+compile-single:
+run-single:
+*** DisTime.main() self test started...
+DisTime.getTimestampStyle()                    = IEEE_ABSOLUTE
+DisTime.getTimestamp() initialTimestamp        = 08:52:40 = 1011160571 = 1011160571 (unsigned vs signed output)
+DisTime.getTimestamp()                         = 08:52:59 = 1011180853 = 1011180853 (unsigned vs signed output)
+DisTime.getCurrentDisAbsoluteTimestamp()       = 08:53:00 = 1011180853
+DisTime.getCurrentDisRelativeTimestamp()       = 08:53:03 = 1011184430
+DisTime.getCurrentDisTimeUnitsSinceTopOfHour() = 12:26:32 = 505592215
+DisTime.getEpoch()                             = 16:00:00 = 0
+DisTime.setEpochCurrentTimestamp();
+DisTime.getEpoch()                             = 16:00:00 = 1011184431
+DisTime.getCurrentDisTimeUnitsSinceTopOfHour() = 19:38:19 = 2147483647
+*** DisTime.main() self test complete.
+BUILD SUCCESSFUL (total time: 0 seconds)
-- 
GitLab