From 1ad5a2ff68f6c5fdb443b5f05e5d751f263d2b10 Mon Sep 17 00:00:00 2001
From: jojot <jojot@10.0.0.153>
Date: Mon, 6 Jun 2022 16:14:11 -0700
Subject: [PATCH] Homework2 Tam

---
 .../Tam/ExampleSimulationProgramTam.java      |  504 ++++++++
 .../Tam/ExampleTrackInterpolationTam.java     |  263 +++++
 .../homework2/Tam/PduTrack.java               | 1043 +++++++++++++++++
 .../homework2/Tam/TamTcpExampleClient.java    |   89 --
 .../homework2/Tam/TamTcpExampleServer.java    |   90 --
 .../homework2/Tam/package-info.java           |   10 -
 6 files changed, 1810 insertions(+), 189 deletions(-)
 create mode 100644 assignments/src/MV3500Cohort2022MayJune/homework2/Tam/ExampleSimulationProgramTam.java
 create mode 100644 assignments/src/MV3500Cohort2022MayJune/homework2/Tam/ExampleTrackInterpolationTam.java
 create mode 100644 assignments/src/MV3500Cohort2022MayJune/homework2/Tam/PduTrack.java
 delete mode 100644 assignments/src/MV3500Cohort2022MayJune/homework2/Tam/TamTcpExampleClient.java
 delete mode 100644 assignments/src/MV3500Cohort2022MayJune/homework2/Tam/TamTcpExampleServer.java
 delete mode 100644 assignments/src/MV3500Cohort2022MayJune/homework2/Tam/package-info.java

diff --git a/assignments/src/MV3500Cohort2022MayJune/homework2/Tam/ExampleSimulationProgramTam.java b/assignments/src/MV3500Cohort2022MayJune/homework2/Tam/ExampleSimulationProgramTam.java
new file mode 100644
index 0000000000..a6ab862686
--- /dev/null
+++ b/assignments/src/MV3500Cohort2022MayJune/homework2/Tam/ExampleSimulationProgramTam.java
@@ -0,0 +1,504 @@
+/**
+ * Copyright (c) 2008-2022, MOVES Institute, Naval Postgraduate School (NPS). All rights reserved.
+ * This work is provided under a BSD open-source license, see project license.html and license.txt
+ * @author brutzman@nps.edu
+ */
+package MV3500Cohort2022MayJune.homework2.Tam;
+
+import edu.nps.moves.dis7.entities.swe.platform.surface._001Poseidon;
+import edu.nps.moves.dis7.entities.swe.platform.surface._002Triton;
+import edu.nps.moves.dis7.enumerations.*;
+import edu.nps.moves.dis7.pdus.*;
+import edu.nps.moves.dis7.utilities.DisThreadedNetworkInterface;
+import edu.nps.moves.dis7.utilities.DisTime;
+import edu.nps.moves.dis7.utilities.PduFactory;
+import edu.nps.moves.dis7.utilities.SimulationManager;
+import edu.nps.moves.dis7.utilities.stream.PduRecorder;
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.util.ArrayList;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/** The purpose of this inheritable class is to provide an easily modifiable example simulation program
+ *  that includes DIS-capable entities performing tasks of interest, and then reporting activity via PDUs
+ *  to the network.
+ *  Default program initialization includes PDU recording turned on by default.
+ *  @see <a href="https://gitlab.nps.edu/Savage/NetworkedGraphicsMV3500/-/blob/master/examples/src/OpenDis7Examples/ExampleSimulationProgramLog.txt">ExampleSimulationProgramLog.txt</a>
+ */
+public class ExampleSimulationProgramTam
+{
+    private      boolean verboseComments         = true;
+    static final String  NETWORK_ADDRESS_DEFAULT  = "239.1.2.3";
+    static final int     NETWORK_PORT_DEFAULT     = 3000;
+                 String  networkAddress           = NETWORK_ADDRESS_DEFAULT;
+                 int     networkPort              = NETWORK_PORT_DEFAULT;
+                 String  thisHostName             = "localhost";
+                 String  DEFAULT_OUTPUT_DIRECTORY = "./pduLog";
+    
+    /** seconds for real-time execution (not simulation time, which may or may not be the same) */
+    double  currentTimeStep  =  2.0; // seconds
+    /** initial simulation time */
+    double  initialTime = 0.0;
+    /** current simulation time */
+    double  simulationTime;
+
+    /**
+     * Output prefix to help with logging by identifying this class (can be overridden in subclass).
+     */
+    protected static String TRACE_PREFIX;
+    
+    /* Declare DIS Protocol Data Unit (PDU) classes for simulation entities */
+    
+    DisTime.TimestampStyle       timestampStyle      = DisTime.TimestampStyle.IEEE_ABSOLUTE;
+    PduFactory                   pduFactory          = new PduFactory(timestampStyle);
+    
+    /** EntityID settings for entity 1 */
+    protected EntityID           entityID_1          = new EntityID();
+    /** EntityID settings for entity 2 */
+    protected EntityID           entityID_2          = new EntityID();
+    /** ESPDU for entity 1 */
+    protected EntityStatePdu     entityStatePdu_1    = pduFactory.makeEntityStatePdu();
+    /** ESPDU for entity 2 */
+    protected EntityStatePdu     entityStatePdu_2    = pduFactory.makeEntityStatePdu();
+    /** FirePdu for entity 1 first  weapon (if any) */
+    protected FirePdu            firePdu_1a          = pduFactory.makeFirePdu();
+    /** FirePdu for entity 1 second weapon (if any) */
+    protected FirePdu            firePdu_1b          = pduFactory.makeFirePdu();
+    /** MunitionDescriptor for these weapons */
+    protected MunitionDescriptor munitionDescriptor1 = new MunitionDescriptor();
+    
+    /** this class instantiated as an object */
+    SimulationManager        simulationManager = new SimulationManager();
+    
+    /** Get ready, get set... initialize simulation entities
+     */
+    public void initializeSimulationEntities()
+    {
+        // Your model setup: define participants.  who's who in this zoo?
+        // Assuming you keep track of entity objects...  here is some support for for Entity 1.
+        
+        // PDU objects are already created, now set their values.
+        entityID_1.setSiteID(1).setApplicationID(2).setEntityID(3); // made-up example ID;
+        
+        entityID_2.setSiteID(1).setApplicationID(2).setEntityID(4); // made-up example ID; 
+        // TODO someday, use enumerations; is there a unique site triplet for MOVES Institute?
+
+        entityStatePdu_1.setEntityID(entityID_1);
+        entityStatePdu_1.setForceId(ForceID.FRIENDLY);
+        entityStatePdu_1.setEntityType(new _001Poseidon()); // note import statement above
+        entityStatePdu_1.setMarking("Entity #1");
+        entityStatePdu_1.getMarkingString(); // check
+
+        entityStatePdu_2.setEntityID(entityID_2);
+        entityStatePdu_2.setForceId(ForceID.OPPOSING);
+        entityStatePdu_2.setEntityType(new _002Triton()); // note import statement above
+        entityStatePdu_2.setMarking("Entity #2");
+
+        // TODO how should we customize this munition?  what is it for your simulation?
+        munitionDescriptor1.setQuantity(1);
+        firePdu_1a.setDescriptor(munitionDescriptor1).setRange(1000.0f);
+        
+        // TODO simulation management PDUs for startup, planning to design special class support 
+//        simulationManager.addEntity();
+        simulationManager.setDescriptor("ExampleSimulationProgram");
+        simulationManager.addHost(thisHostName);
+        simulationManager.setDisThreadedNetworkInterface(disNetworkInterface);
+    }
+                 
+    /**
+     * This runSimulationLoops() method is for you, a customizable programmer-modifiable
+     * code block for defining and running a new simulation of interest.
+     * 
+     * Welcome! Other parts of this program handle bookkeeping and plumbing tasks so that
+     * you can focus on your model entities and activities.
+     * Expandable support includes DIS EntityStatePdu, FirePdu and CommentPdu all available for 
+     * modification and sending in a simulation loop.
+     * Continuous improvement efforts seek to make this program as easy and straightforward
+     * as possible for DIS simulationists to use and adapt.
+     * All of the other methods are setup, teardown and configuration that you may find
+     * interesting, even helpful, but don't really have to worry about.
+     */
+    @SuppressWarnings("SleepWhileInLoop") // yes we do that
+    public void runSimulationLoops ()
+    {
+      try
+      {              
+        final int     SIMULATION_MAX_LOOP_COUNT = 10; // be deliberate out there!  also avoid infinite loops.
+              int     simulationLoopCount = 0;        // variable, initialized at 0
+              boolean simulationComplete = false;     // sentinel variable as termination condition, are we done yet?
+        
+        // TODO reset Clock Time to today's date and timestamp to zero, providing consistent outputs for each simulation run
+
+        pduRecorder.setVerbose(true);
+        
+        initializeSimulationEntities();
+        
+        simulationManager.simulationJoin();
+        simulationManager.simulationStart();
+        
+        // ===================================================================================================
+        // loop the simulation while allowed, programmer can set additional conditions to break out and finish
+        while (simulationLoopCount < SIMULATION_MAX_LOOP_COUNT)  // are we done yet?
+        {
+            simulationLoopCount++; // good practice: increment loop counter as first action in that loop
+            
+            // =============================================================================================
+            // * your own simulation code starts here! *
+            // =============================================================================================
+            
+            //  are there any other variables to modify at the beginning of your loop?
+            
+            // compute a track, update an ESPDU, whatever it is that your model is doing...
+            
+            // Where is my entity?  Insert changes in position; this sample only changes X position.
+            entityStatePdu_1.getEntityLocation().setX(entityStatePdu_1.getEntityLocation().getX() + 1.0); // 1m per timestep
+            
+            // decide whether to fire, and then update the firePdu.  Hmmm, you might want a target to shoot at!
+            
+            // etc. etc. your code goes here for your simulation of interest
+                
+            // something happens between my simulation entities, la de da de da...
+            System.out.println ("... My simulation just did something, no really...");
+            System.out.flush();
+            
+            
+            // make your reports: narrative code for CommentPdu here (set all to empty strings to avoid sending)
+            narrativeMessage1 = "MV3500 ExampleSimulationProgram";
+            narrativeMessage2 = "runSimulation() loop " + simulationLoopCount;
+            narrativeMessage3 = ""; // intentionally blank for testing
+
+            // your loop termination condition goes here
+            if (simulationLoopCount > 4) // for example
+            {
+                simulationComplete = true;
+            }      
+            // =============================================================================================
+            // * your own simulation code is finished here! *
+            // =============================================================================================
+            
+            // staying synchronized with timestep: wait duration for elapsed time in this loop
+            // Thread.sleep needs a (long) parameter for milliseconds, which are clumsy to use sometimes
+            Thread.sleep((long)(currentTimeStep * 1000)); // seconds * (1000 msec/sec) = milliseconds
+            System.out.println ("... [Pausing for " + currentTimeStep + " seconds]");
+            
+            // OK now send the status PDUs for this loop, and then continue
+            System.out.println ("sending PDUs for simulation step " + simulationLoopCount + ", monitor loopback to confirm sent");
+            System.out.flush();
+            sendAllPdusForLoopTimestep(entityStatePdu_1, firePdu_1a, currentTimeStepComment, narrativeMessage1, narrativeMessage2, narrativeMessage3);
+            sendSinglePdu(entityStatePdu_2); // me too i.e. 2!
+            System.out.println ("... [PDUs successfully sent for this loop]");
+            System.out.flush();
+            
+            // ===============================
+            // current loop now finished, check whether to terminate if simulation complete, otherwise continue
+            if (simulationComplete || (simulationLoopCount > 10000)) // for example; including fail-safe condition is good
+            {
+                System.out.println ("... [loop termination condition met, simulationComplete=" + simulationComplete + "]"); // ", final loopCount=" + loopCount + 
+                System.out.flush();
+                break;
+            }
+        }   // end of simulation loop
+        // ===================================================================================================
+
+        narrativeMessage2 = "runSimulation() completed successfully"; // all done
+        sendCommentPdu(narrativeComment, narrativeMessage1, narrativeMessage2, narrativeMessage3);
+        System.out.println ("... [final CommentPdu successfully sent for simulation]");
+        
+        // TODO simulation management PDUs
+        simulationManager.simulationStop();
+        simulationManager.simulationLeave();
+      } 
+      catch (InterruptedException iex) // handle any exception that your code might choose to provoke!
+      {
+        Logger.getLogger(ExampleSimulationProgramTam.class.getName()).log(Level.SEVERE, null, iex);
+      }
+    }
+    /* **************************** infrastructure code, modification is seldom needed ************************* */
+                 
+    String narrativeMessage1 = new String();
+    String narrativeMessage2 = new String();
+    String narrativeMessage3 = new String();
+          
+    /* VariableRecordType enumerations have potential use with CommentPdu logs */
+    /* TODO contrast to EntityType */
+    VariableRecordType     descriptionComment = VariableRecordType.DESCRIPTION;
+    VariableRecordType       narrativeComment = VariableRecordType.COMPLETE_EVENT_REPORT;
+    VariableRecordType          statusComment = VariableRecordType.APPLICATION_STATUS;
+    VariableRecordType currentTimeStepComment = VariableRecordType.APPLICATION_TIMESTEP;
+    VariableRecordType           otherComment = VariableRecordType.OTHER;
+    
+    // class variables
+    DisThreadedNetworkInterface             disNetworkInterface;
+    DisThreadedNetworkInterface.PduListener pduListener;
+    Pdu                                     receivedPdu;
+    PduRecorder                             pduRecorder;
+    
+    /**
+     * Constructor design goal: additional built-in initialization conveniences can go here
+     * to keep student efforts focused on the runSimulation() method.
+     */
+    public ExampleSimulationProgramTam()
+    {
+        DisTime.setTimestampStyle(timestampStyle);
+        
+        try
+        {
+            thisHostName = InetAddress.getLocalHost().getHostName();
+            System.out.println(TRACE_PREFIX + "thisHostName=" + thisHostName);
+        }
+        catch (UnknownHostException uhe)
+        {
+            System.out.println(TRACE_PREFIX + thisHostName + "not connected to network: " + uhe.getMessage());
+        }
+    }
+    
+    /**
+     * Utility Constructor that allows your example simulation program to override default network address and port
+     * @param address network address to use
+     * @param port corresponding network port to use
+     */
+    public ExampleSimulationProgramTam(String address, int port)
+    {
+        super();
+        
+        setNetworkAddress(address);
+        
+        setNetworkPort(port);
+    }
+
+    /**
+     * get current networkAddress as a string
+     * @return the networkAddress
+     */
+    public String getNetworkAddress()
+    {
+        return networkAddress;
+    }
+    /**
+     * set current networkAddress using a string
+     * @param newNetworkAddress the networkAddress to set
+     */
+    public final void setNetworkAddress(String newNetworkAddress)
+    {
+        this.networkAddress = newNetworkAddress;
+    }
+
+    /**
+     * get current networkPort
+     * @return the networkPort
+     */
+    public int getNetworkPort()
+    {
+        return networkPort;
+    }
+    /**
+     * set current networkPort
+     * @param newNetworkPort the networkPort to set
+     */
+    public final void setNetworkPort(int newNetworkPort)
+    {
+        this.networkPort = newNetworkPort;
+    }
+    /**
+     * Get timestampStyle used by PduFactory
+     * @return current timestampStyle
+     */
+    public DisTime.TimestampStyle getTimestampStyle()
+    {
+        return timestampStyle;
+    }
+    /**
+     * Set timestampStyle used by PduFactory
+     * @param newTimestampStyle the timestampStyle to set
+     * @return same object to permit progressive setters 
+     */
+    public ExampleSimulationProgramTam setTimestampStyle(DisTime.TimestampStyle newTimestampStyle) 
+    {
+        timestampStyle = newTimestampStyle;
+        DisTime.setTimestampStyle(newTimestampStyle);
+        return this;
+    }
+
+    /**
+     * Initialize network interface, choosing best available network interface
+     */
+    public void setUpNetworkInterface()
+    {
+        disNetworkInterface = new DisThreadedNetworkInterface(getNetworkAddress(), getNetworkPort());
+        disNetworkInterface.setDescriptor ("ExampleSimulationProgram pdu looping");
+        
+        System.out.println("Network confirmation:" +
+               " address=" + disNetworkInterface.getAddress()+ //  disNetworkInterface.getMulticastGroup() + 
+                  " port=" + disNetworkInterface.getPort());   // + disNetworkInterface.getDisPort());
+        pduListener = new DisThreadedNetworkInterface.PduListener()
+        {
+            /** Callback handler for listener */
+            @Override
+            public void incomingPdu(Pdu newPdu)
+            {
+                receivedPdu = newPdu;
+            }
+        };
+        disNetworkInterface.addListener(pduListener);
+        
+        String outputDirectory = DEFAULT_OUTPUT_DIRECTORY;
+        System.out.println("Beginning pdu save to directory " + outputDirectory);
+        pduRecorder = new PduRecorder(outputDirectory, getNetworkAddress(), getNetworkPort()); // assumes save
+        pduRecorder.setEncodingPduLog(PduRecorder.ENCODING_PLAINTEXT);
+        pduRecorder.setVerbose(true); // either sending, receiving or both
+        pduRecorder.start(); // begin running
+    }
+
+    /** All done, release network resources */
+    public void tearDownNetworkInterface()
+    {
+        pduRecorder.stop(); // handles disNetworkInterface.close(), tears down threads and sockets
+    }
+
+    /** 
+     * Send a single Protocol Data Unit (PDU) of any type
+     * @param pdu the pdu to send
+     */
+    protected void sendSinglePdu(Pdu pdu)
+    {
+        try
+        {
+            disNetworkInterface.send(pdu);
+            Thread.sleep(100); // TODO consider refactoring the wait logic and moving externally
+        } 
+        catch (InterruptedException ex)
+        {
+            System.err.println(this.getClass().getName() + " Error sending PDU: " + ex.getLocalizedMessage());
+            System.exit(1);
+        }
+    }
+
+    /**
+     * Send Comment PDU
+     * @see <a href="https://docs.oracle.com/javase/tutorial/java/javaOO/arguments.html">Passing Information to a Method or a Constructor</a> Arbitrary Number of Arguments
+     * @param commentType    enumeration value describing purpose of the narrative comment
+     * @param comments       String array of narrative comments
+     */
+    public void sendCommentPdu(VariableRecordType commentType,
+                                     // vararg... variable-length set of String comments can optionally follow
+                                        String... comments)
+    {
+        sendAllPdusForLoopTimestep (null, null, commentType, comments);
+    }
+
+    /**
+     * Send EntityState, Fire, Comment PDUs that got updated for this loop, reflecting state of current simulation timestep.
+     * @see <a href="https://docs.oracle.com/javase/tutorial/java/javaOO/arguments.html">Passing Information to a Method or a Constructor</a> Arbitrary Number of Arguments
+     * @param entityStatePdu the ESPDU to send, if any
+     * @param firePdu        the FirePDU to send, if any
+     * @param commentType    enumeration value describing purpose of the narrative comment
+     * @param comments       String array of narrative comments
+     */
+    public void sendAllPdusForLoopTimestep(EntityStatePdu entityStatePdu,
+                                   FirePdu firePdu,
+                        VariableRecordType commentType,
+                              // vararg... variable-length set of String comments can optionally follow
+                                 String... comments)
+    {
+        if (entityStatePdu != null)
+            sendSinglePdu(entityStatePdu);
+            
+        if (firePdu != null)
+            sendSinglePdu(firePdu); // bang
+        
+        if ((comments != null) && (comments.length > 0))
+        {
+            ArrayList<String> newCommentsList = new ArrayList<>();
+            for (String comment : comments)
+            {
+                if (!comment.isEmpty())
+                {
+                    newCommentsList.add(comment); // OK found something to send
+                }
+            }
+            if (!newCommentsList.isEmpty())
+            {
+                if (commentType == null)
+                    commentType = otherComment; // fallback value otherComment
+                // now build the commentPdu from these string inputs, thus constructing a narrative entry
+                CommentPdu commentPdu = pduFactory.makeCommentPdu(commentType, newCommentsList.toArray(new String[0])); // comments);
+                sendSinglePdu(commentPdu);
+                if (isVerboseComments())
+                {
+                    System.out.println("*** [Narrative comment sent: " + commentType.name() + "] " + newCommentsList.toString());
+                    System.out.flush();
+                }
+            }
+        }
+    }
+
+    /**
+     * test for verboseComments mode
+     * @return whether verboseComments mode is enabled
+     */
+    public boolean isVerboseComments() {
+        return verboseComments;
+    }
+
+    /**
+     * set verboseComments mode
+     * @param newVerboseComments whether verboseComments mode is enabled
+     */
+    public void setVerboseComments(boolean newVerboseComments) {
+        this.verboseComments = newVerboseComments;
+    }
+    
+    /**
+     * Initial execution via main() method: handle args array of command-line initialization (CLI) arguments here
+     * @param args command-line parameters: network address and port
+     */
+    protected void handleArgs (String[] args)
+    {
+        // initial execution: handle args array of initialization arguments here
+        if (args.length == 2) 
+        {
+            if ((args[0] != null) && !args[0].isEmpty())
+                thisProgram.setNetworkAddress(args[0]);
+            if ((args[1] != null) && !args[1].isEmpty())
+                thisProgram.setNetworkPort(Integer.parseInt(args[1]));
+        }
+        else if (args.length != 0) 
+        {
+            System.err.println("Usage: " + thisProgram.getClass().getSimpleName() + " [address port]");
+            System.exit(-1);
+        }
+    }
+    
+    /** Locally instantiable copy of program, can be subclassed. */
+    protected static ExampleSimulationProgramTam thisProgram;
+  
+    /**
+     * Main method is first executed when a program instance is loaded.
+     * @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 command-line parameters: network address and port.
+     *    Command-line arguments are an array of optional String parameters that are passed from execution environment during invocation
+     */
+    public static void main(String[] args)
+    {
+        TRACE_PREFIX = "[" + ExampleSimulationProgramTam.class.getName() + "] ";
+        
+        System.out.println(TRACE_PREFIX + "main() started...");
+        
+        thisProgram = new ExampleSimulationProgramTam(); // creates instance of self within static main() method
+        
+        thisProgram.handleArgs (args); // process command-line invocation arguments
+
+        thisProgram.setUpNetworkInterface();
+        
+//        thisProgram.pduRecorder.setDescriptor (TRACE_PREFIX.replace("[","").replace("]","") + " pduRecorder");
+
+        thisProgram.runSimulationLoops(); // ... your simulation execution code goes in there ...
+        
+        thisProgram.tearDownNetworkInterface(); // make sure no processes are left lingering
+        
+        System.out.println(TRACE_PREFIX + "complete."); // report successful completion
+        
+        System.exit(0); // ensure all threads and sockets released
+    }
+}
diff --git a/assignments/src/MV3500Cohort2022MayJune/homework2/Tam/ExampleTrackInterpolationTam.java b/assignments/src/MV3500Cohort2022MayJune/homework2/Tam/ExampleTrackInterpolationTam.java
new file mode 100644
index 0000000000..93d9b1c9f0
--- /dev/null
+++ b/assignments/src/MV3500Cohort2022MayJune/homework2/Tam/ExampleTrackInterpolationTam.java
@@ -0,0 +1,263 @@
+/**
+ * Copyright (c) 2008-2022, MOVES Institute, Naval Postgraduate School (NPS). All rights reserved.
+ * This work is provided under a BSD open-source license, see project license.html and license.txt
+ *
+ * @author brutzman@nps.edu
+ */
+package MV3500Cohort2022MayJune.homework2.Tam;
+
+
+import static MV3500Cohort2022MayJune.homework2.Tam.ExampleSimulationProgramTam.TRACE_PREFIX;
+import static MV3500Cohort2022MayJune.homework2.Tam.ExampleSimulationProgramTam.thisProgram;
+import edu.nps.moves.dis7.entities.swe.platform.surface._001Poseidon;
+import edu.nps.moves.dis7.enumerations.ForceID;
+import edu.nps.moves.dis7.pdus.EntityStatePdu;
+import edu.nps.moves.dis7.pdus.Pdu;
+import edu.nps.moves.dis7.pdus.Vector3Double;
+import edu.nps.moves.dis7.utilities.DisTime;
+import edu.nps.moves.dis7.utilities.stream.X3dCreateInterpolators;
+import edu.nps.moves.dis7.utilities.stream.X3dCreateLineSet;
+import java.util.ArrayList;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * The purpose of this program is to provide an easily modifiable example
+ * simulation for networked entity tracks and presentation, including
+ * DIS-capable entities doing tasks and reporting them to the network.
+ * Default settings include PDU recording turned on by default.
+ * @see <a href="https://gitlab.nps.edu/Savage/NetworkedGraphicsMV3500/-/blob/master/examples/src/OpenDis7Examples/ExampleTrackInterpolationLog.txt">ExampleTrackInterpolationLog.txt</a>
+ * @see <a href="https://calhoun.nps.edu/handle/10945/65436">REPEATABLE UNIT TESTING OF DISTRIBUTED INTERACTIVE SIMULATION (DIS) PROTOCOL BEHAVIOR STREAMS USING WEB STANDARDS</a> by Tobias Brennenstuhl, Masters Thesis, Naval Postgraduate School (NPS), June 2020
+ * @see <a href="https://gitlab.nps.edu/Savage/SavageTheses/-/tree/master/BrennenstuhlTobias">https://gitlab.nps.edu/Savage/SavageTheses/-/tree/master/BrennenstuhlTobias</a>
+ */
+public class ExampleTrackInterpolationTam extends ExampleSimulationProgramTam
+{
+    // -------------------- Begin Variables for X3D autogenerated code
+    private X3dCreateInterpolators x3dInterpolators = new X3dCreateInterpolators();
+    private X3dCreateLineSet x3dLineSet = new X3dCreateLineSet();
+    private byte[] globalByteBufferForX3dInterpolators = null;
+    // -------------------- End Variables for X3D autogenerated code
+    
+    ArrayList<Pdu> pduSentList = new ArrayList<>();
+    
+    /**
+     * This runSimulationLoops() method is a programmer-modifiable method for
+     * defining and running a new simulation of interest. Welcome! Other parts
+     * of this program handle bookkeeping and plumbing tasks so that you can
+     * focus on your model entities and activities. Expandable support includes
+     * DIS EntityStatePdu, FirePdu and CommentPdu all available for modification
+     * and sending in a simulation loop. Continuous improvement efforts seek to
+     * make this program as easy and straightforward as possible for DIS
+     * simulationists to use and adapt. All of the other methods are setup,
+     * teardown and configuration that you may find interesting, even helpful,
+     * but don't really have to worry about.
+     */
+    @SuppressWarnings("SleepWhileInLoop") // yes we do that
+    @Override // indicates that this method supercedes corresponding superclass method
+    public void runSimulationLoops() 
+    {
+        try
+        {
+            final int SIMULATION_MAX_LOOP_COUNT = 50; // be deliberate out there!  also avoid infinite loops.
+            int simulationLoopCount = 0;        // variable, initialized at 0
+            boolean simulationComplete = false;     // sentinel variable as termination condition, are we done yet?
+
+            // TODO reset Clock Time to today's date and timestamp to zero, providing consistent outputs for each simulation run
+            DisTime.setEpochLvcNow();
+            simulationTime = initialTime - currentTimeStep; // pre-initialization for first loop
+        
+            initializeSimulationEntities();
+            
+            // use declared PDU objects and set their values.
+            entityID_1.setSiteID(1).setApplicationID(2).setEntityID(3); // made-up example ID;
+            // TODO someday, use enumerations; is there a unique site triplet for MOVES Institute?
+            
+            pduRecorder.setVerbose(false);
+            pduRecorder.hasVerboseOutput(); // debug check
+
+            EntityStatePdu espdu_1 = pduFactory.makeEntityStatePdu();
+            espdu_1.setEntityID(entityID_1);
+            espdu_1.setForceId(ForceID.FRIENDLY);
+            espdu_1.setEntityType(new _001Poseidon()); // note import statement above
+            espdu_1.setMarking("track path");
+//          espdu_1.clearMarking();     // test
+//          espdu_1.getMarkingString(); // trace
+//          espdu_1.setEntityLocation(new Vector3Double().setX(0).setY(0).setZ(0)); // long form
+            espdu_1.setEntityLocation(0, 0, 0); // utility method
+            
+            float speedEntity_1 = 1.0f; // meters/second
+            EntityStatePdu.Direction directionEntity_1 = EntityStatePdu.Direction.NORTH;
+            
+            PduTrack pduTrack_1 = new PduTrack();
+            pduTrack_1.setDescriptor("testing 123");
+            pduTrack_1.setDefaultWaypointInterval(1.0f); // overrides timestamps
+            pduTrack_1.setAddLineBreaksWithinKeyValues(true);
+            pduTrack_1.setAuthor("Don Brutzman");
+            pduTrack_1.setX3dModelName("ExampleTrackInterpolation.x3d");
+            pduTrack_1.setX3dModelIdentifier("https://gitlab.nps.edu/Savage/NetworkedGraphicsMV3500/-/blob/master/examples/src/OpenDis7Examples/ExampleTrackInterpolation.x3d");
+            pduTrack_1.addPdu(espdu_1); // initial location
+            
+            // OK send initial PDUs prior to loop
+            if (pduRecorder.hasVerboseOutput())
+                System.out.println("sending PDUs for simulation step " + simulationLoopCount + ", monitor loopback to confirm sent");
+            sendSinglePdu(espdu_1);
+//            sendCommentPdu(currentTimeStepComment, narrativeMessage1, narrativeMessage2, narrativeMessage3);
+            pduSentList.add(espdu_1);
+            reportPdu(simulationLoopCount, espdu_1.getEntityLocation(), directionEntity_1);
+
+            // TODO simulation management PDUs for startup, planning to design special class support
+            // ===================================================================================================
+            // loop the simulation while allowed, programmer can set additional conditions to break out and finish
+            while (simulationLoopCount < SIMULATION_MAX_LOOP_COUNT) // are we done yet?
+            {
+                simulationLoopCount++;      // good practice: increment loop counter as first action in that loop
+                simulationTime += currentTimeStep; // good practice: update clock along with loop index
+
+                // =============================================================================================
+                // * your own simulation code starts here! *
+                // =============================================================================================
+                //  are there any other variables to modify at the beginning of your loop?
+                // compute a track, update an ESPDU, whatever it is that your model is doing...
+                
+                // Pick direction, change each 10 seconds, traverse a box.  No physics.  Walk a box!
+                if (simulationLoopCount <= 10)
+                    directionEntity_1 = EntityStatePdu.Direction.NORTH;
+                else if (simulationLoopCount <= 20)
+                    directionEntity_1 = EntityStatePdu.Direction.EAST;
+                else if (simulationLoopCount <= 30)
+                    directionEntity_1 = EntityStatePdu.Direction.SOUTH;
+                else // if (simulationLoopCount <= 40)
+                    directionEntity_1 = EntityStatePdu.Direction.WEST;
+                
+                // use utility method to simply update velocity vector using speed value and direction
+                espdu_1.setEntityLinearVelocity(speedEntity_1, directionEntity_1);
+                
+                // Where is my entity?  Insert changes in position; this sample only changes X position.
+                espdu_1.advanceEntityLocation(currentTimeStep);
+
+                // make your reports: narrative code for CommentPdu here (set all to empty strings to avoid sending)
+                narrativeMessage1 = "MV3500 TrackSimulationProgram";
+                narrativeMessage2 = "runSimulation() loop " + simulationLoopCount + " at time " + simulationTime;
+                narrativeMessage3 = ""; // intentionally blank for testing
+
+                // your loop termination condition goes here
+                if (simulationLoopCount > 40) // for example
+                {
+                    simulationComplete = true;
+                }
+                // =============================================================================================
+                // * your own simulation code is finished here! *
+                // =============================================================================================
+
+                // staying synchronized with timestep: wait duration for elapsed time in this loop
+                // Thread.sleep needs a (long) parameter for milliseconds, which are clumsy to use sometimes
+                if (false) // real-time operation or simulation speedup
+                {
+                    Thread.sleep((long) (currentTimeStep * 1000)); // seconds * (1000 msec/sec) = milliseconds
+                    System.out.println(TRACE_PREFIX + "Pausing for " + currentTimeStep + " seconds");
+                }
+
+                // OK now send the status PDUs for this loop, and then continue
+                if (pduRecorder.hasVerboseOutput())
+                    System.out.println("sending PDUs for simulation step " + simulationLoopCount + ", monitor loopback to confirm sent");
+                sendSinglePdu(espdu_1);
+                sendCommentPdu(currentTimeStepComment, narrativeMessage1, narrativeMessage2, narrativeMessage3);
+                if (pduRecorder.hasVerboseOutput())
+                    System.out.println(TRACE_PREFIX + "PDUs successfully sent for this loop");
+                pduSentList.add(espdu_1);
+                pduTrack_1.addPdu(espdu_1);
+                Vector3Double location = espdu_1.getEntityLocation();
+                reportPdu(simulationLoopCount, location, directionEntity_1);
+                
+                // ===============================
+                // current loop now finished, check whether to terminate if simulation complete, otherwise continue
+                if (simulationComplete || (simulationLoopCount > 10000)) // for example; including fail-safe condition is good
+                {
+                    System.out.println(TRACE_PREFIX + "loop termination condition met, simulationComplete=" + simulationComplete); // ", final loopCount=" + loopCount +
+                    break;
+                }
+            }   // end of simulation loop
+            // ===================================================================================================
+            System.out.println(TRACE_PREFIX + "all PDUs successfully sent for this loop (pduSentList.size()=" + pduSentList.size() + " total)");
+            
+            // track analysis
+            System.out.println(TRACE_PREFIX + "pduTrack_1 initialLocation=" + pduTrack_1.getInitialLocation() + ", latestLocation=" + pduTrack_1.getLatestLocation());
+            pduTrack_1.sortPdus()
+                      .createRawWaypoints();
+            
+            System.out.println("pduTrack_1 getEspduCount()=" + pduTrack_1.getEspduCount());
+            System.out.println("pduTrack_1 duration = " + pduTrack_1.getTotalDurationSeconds() + " seconds = " +
+                                                          pduTrack_1.getTotalDurationTicks() + " ticks");
+            System.out.println("=================================");
+            System.out.println(pduTrack_1.createX3dModel());
+            System.out.println("=================================");
+            
+            narrativeMessage2 = "runSimulation() completed successfully"; // all done
+            sendCommentPdu(narrativeComment, narrativeMessage1, narrativeMessage2, narrativeMessage3);
+            if (pduRecorder.hasVerboseOutput())
+                System.out.println(TRACE_PREFIX + "final CommentPdu successfully sent for simulation");
+            // TODO simulation management PDUs
+        }
+        catch (InterruptedException iex) // handle any exception that your code might choose to provoke!
+        {
+            Logger.getLogger(ExampleTrackInterpolationTam.class.getName()).log(Level.SEVERE, null, iex);
+        }
+    }
+    
+    /**
+     * Report current PDU information to console
+     * @param simulationLoopCount current loop index
+     * @param location current location
+     * @param directionEntity current direction
+     * @return same object to permit progressive setters
+     */
+    public ExampleTrackInterpolationTam reportPdu(int simulationLoopCount, Vector3Double location, EntityStatePdu.Direction directionEntity)
+    {
+        System.out.println (String.format("%2d ", simulationLoopCount) + "Entity location=(" + 
+                String.format("%4.1f", location.getX()) + ", " +
+                String.format("%4.1f", location.getY()) + ", " + 
+                String.format("%4.1f", location.getZ()) + ") "  +
+                String.format("%-5s",   directionEntity.name())
+//              + " " + espdu_1.getEntityLinearVelocity().toString()
+        );
+        return this;
+    }
+
+    /* Default constructors used unless otherwise defined/overridden.  */
+    /**
+     * Main method is first executed when a program instance is loaded.
+     *
+     * @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 command-line arguments are an array of optional String
+     * parameters that are passed from execution environment during invocation
+     */
+    public static void main(String[] args)
+    {
+        TRACE_PREFIX = "[" + ExampleTrackInterpolationTam.class.getName() + "] ";
+
+        System.out.println(TRACE_PREFIX + "main() started...");
+        
+        thisProgram = new ExampleTrackInterpolationTam(); // creates instance of self within static main() method
+        
+        thisProgram.handleArgs (args); // process command-line invocation arguments
+
+        thisProgram.setUpNetworkInterface();
+        
+        thisProgram.pduRecorder.setDescriptor (TRACE_PREFIX.replace("[","").replace("]","") + " pduRecorder");
+        
+        thisProgram.pduRecorder.setVerbose(false);
+        thisProgram.setVerboseComments(false);
+        thisProgram.disNetworkInterface.setVerbose(false);
+        
+        thisProgram.runSimulationLoops(); // ... your simulation execution code goes in there ...
+        
+        thisProgram.tearDownNetworkInterface(); // make sure no processes are left lingering
+        
+        System.out.println(TRACE_PREFIX + "complete."); // report successful completion
+        
+        System.exit(0); // ensure all threads and sockets released
+    }
+
+}
diff --git a/assignments/src/MV3500Cohort2022MayJune/homework2/Tam/PduTrack.java b/assignments/src/MV3500Cohort2022MayJune/homework2/Tam/PduTrack.java
new file mode 100644
index 0000000000..504ea3db17
--- /dev/null
+++ b/assignments/src/MV3500Cohort2022MayJune/homework2/Tam/PduTrack.java
@@ -0,0 +1,1043 @@
+/*
+Copyright (c) 1995-2022 held by the author(s).  All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions
+are met:
+
+    * Redistributions of source code must retain the above copyright
+      notice, this list of conditions and the following disclaimer.
+    * Redistributions in binary form must reproduce the above copyright
+      notice, this list of conditions and the following disclaimer
+      in the documentation and/or other materials provided with the
+      distribution.
+    * Neither the names of the Naval Postgraduate School (NPS)
+      Modeling Virtual Environments and Simulation (MOVES) Institute
+      https://www.nps.edu and https://www.nps.edu/web/moves
+      nor the names of its contributors may be used to endorse or
+      promote products derived from this software without specific
+      prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
+COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
+ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGE.
+*/
+// TODO move into opendis7 distribution tree in package edu.nps.moves.dis7.utilities.stream;
+
+package MV3500Cohort2022MayJune.homework2.Tam;
+
+import MV3500Cohort2022MayJune.homework2.Duran.*;
+import edu.nps.moves.dis7.entities.swe.platform.surface._001Poseidon;
+import edu.nps.moves.dis7.enumerations.DisPduType;
+import edu.nps.moves.dis7.enumerations.ForceID;
+import edu.nps.moves.dis7.pdus.EntityID;
+import edu.nps.moves.dis7.pdus.EntityStatePdu;
+import edu.nps.moves.dis7.pdus.EulerAngles;
+import edu.nps.moves.dis7.pdus.Pdu;
+import edu.nps.moves.dis7.pdus.Vector3Double;
+import edu.nps.moves.dis7.utilities.DisTime;
+import edu.nps.moves.dis7.utilities.PduFactory;
+import java.io.ByteArrayOutputStream;
+import java.io.DataOutputStream;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.time.Instant;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+
+/**
+ * Create a track from DIS ESPDUs
+ * @author brutzman
+ */
+public class PduTrack
+{   
+    private String          descriptor = new String();
+    private ArrayList<Pdu>  pduList = new ArrayList<>();
+    private EntityStatePdu  initialEspdu;
+    private EntityStatePdu  latestEspdu;
+    private Vector3Double   initialLocation;
+    private Vector3Double   latestLocation;
+    
+    /** waypoint timelineList in seconds */
+    private ArrayList<Float>          timelineList = new ArrayList<>();
+    private ArrayList<Vector3Double> waypointsList = new ArrayList<>();
+    private ArrayList<EulerAngles> eulerAnglesList = new ArrayList<>();
+    
+    private String                             author = new String();
+    private String                 x3dModelIdentifier = new String();
+    private String                 x3dModelName       = "PduTrackInterpolation.x3d";
+    private float             defaultWaypointInterval = -1;
+    private float                     durationSeconds = -1;
+    private String                   x3dTimeSensorDEF = new String();
+    private String         x3dPositionInterpolatorDEF = new String();
+    private String      x3dOrientationInterpolatorDEF = new String();
+    private boolean      addLineBreaksWithinKeyValues = false;
+    /** what kind of timestamp is being used */
+    public    DisTime.TimestampStyle timestampStyle        = DisTime.TimestampStyle.IEEE_ABSOLUTE;
+    /** direct access to pduFactory for creating new PDU instances */
+    protected PduFactory             pduFactory            = new PduFactory(timestampStyle);
+    private   LocalDateTime          recordingStart;
+    private   LocalDateTime          recordingStop;
+    private   String                 todaysDateString      = new String();
+    
+    /** direct access to byteArrayOutputStream */
+    protected ByteArrayOutputStream  byteArrayOutputStream = new ByteArrayOutputStream();
+    /** direct access to DataOutputStream */
+    protected DataOutputStream       dataOutputStream      = new DataOutputStream(byteArrayOutputStream);
+    private   String TRACE_PREFIX = "[" + (PduTrack.class.getSimpleName()) + "] ";
+    
+    /**
+     * Constructor for PduTrack
+     */
+    public PduTrack()
+    {
+        // initialization code here
+        
+        // https://docs.oracle.com/javase/tutorial/datetime/TOC.html
+        // https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/time/package-summary.html
+        // https://stackoverflow.com/questions/5175728/how-to-get-the-current-date-time-in-java/5175900 (scroll down to java.time)
+        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("d MMMM yyyy");
+        todaysDateString = LocalDate.now().format(formatter);
+//      System.out.println(TRACE_PREFIX + "today=" + todayString);
+    }
+    
+    /**
+     * Set timestampStyle used by PduFactory
+     * @param newTimestampStyle new value to set
+     */
+    public PduTrack(DisTime.TimestampStyle newTimestampStyle)
+    {
+        timestampStyle = newTimestampStyle;
+        DisTime.setTimestampStyle(newTimestampStyle);
+        // automatic super invocation: return PduTrack();
+    }
+    
+    /**
+     * Get simple descriptor (such as parent class name) for this SimulationManager, used in trace statements
+     * @return simple descriptor name
+     */
+    public String getDescriptor() 
+    {
+        return descriptor;
+    }
+    /**
+     * Set new simple descriptor (such as parent class name) for this SimulationManager, used in trace statements
+     * @param newDescriptor simple descriptor name for this interface
+     * @return same object to permit progressive setters */
+    public PduTrack setDescriptor(String newDescriptor) 
+    {
+        if (newDescriptor != null)
+            this.descriptor = newDescriptor.trim();
+        TRACE_PREFIX = "[" + (PduTrack.class.getSimpleName() + " " + descriptor) + "] ";
+        return this;
+    }
+    /**
+     * Get timestampStyle used by PduFactory
+     * @return current timestampStyle
+     */
+    public DisTime.TimestampStyle getTimestampStyle()
+    {
+        return timestampStyle;
+    }
+    /**
+     * Set timestampStyle used by PduFactory
+     * @param newTimestampStyle the timestampStyle to set
+     * @return same object to permit progressive setters 
+     */
+    public PduTrack setTimestampStyle(DisTime.TimestampStyle newTimestampStyle) 
+    {
+        this.timestampStyle = newTimestampStyle;
+        DisTime.setTimestampStyle(newTimestampStyle);
+        return this;
+    }
+
+    /**
+     * Determine initial location, reset to (0 0 0) if not found
+     * @return current initialLocation
+     */
+    public Vector3Double getInitialLocation() {
+        if (initialLocation == null)
+        {
+            System.out.println (TRACE_PREFIX + "getInitialLocation() not found, isTrackEmpty()=" + isTrackEmpty() + ", returning 0 0 0");
+            return new Vector3Double();
+        }
+        return initialLocation;
+    }
+    /**
+     * Determine current location, reset to (0 0 0) if not found
+     * @return current latestLocation
+     */
+    public Vector3Double getLatestLocation() {
+        if (latestLocation == null)
+        {
+            System.out.println (TRACE_PREFIX + "getLatestLocation() not found, isTrackEmpty()=" + isTrackEmpty() + ", returning 0 0 0");
+            return new Vector3Double();
+        }
+        return latestLocation;
+    }
+    /**
+     * Get individual Pdu from pduList, index must not exceed existing pduList size
+     * @param index for pdu of interest
+     * @return pdu of interest
+     */
+    public Pdu getPdu(int index) throws IndexOutOfBoundsException
+    {
+        if ((index >= pduList.size()) || (index < 0))
+        {
+            System.out.println (TRACE_PREFIX + "getPdu(" + index + ") out of bounds, pduList.size()=" + pduList.size());
+            // then throw exception
+        }
+        return pduList.get(index);
+    }
+    /**
+     * get current pduList
+     * @return current pduList
+     */
+    public ArrayList<Pdu> getPduList() {
+        return pduList;
+    }
+    /**
+     * get current waypointsList
+     * @return current waypointsList
+     */
+    public ArrayList<Vector3Double> getWaypointsList() {
+        return waypointsList;
+    }
+    /**
+     * current eulerAnglesList
+     * @return current eulerAnglesList
+     */
+    public ArrayList<EulerAngles> getEulerAnglesList() {
+        return eulerAnglesList;
+    }
+    /**
+     * Time in seconds corresponding to each PDU
+     * @return current timelineList
+     */
+    public ArrayList<Float> getTimelineList() {
+        return timelineList;
+    }
+    /**
+     * Add PDU, typically ESPDU
+     * @param newPdu new Pdu to add, typically ESPDU
+     * @return same object to permit progressive setters
+     */
+    public PduTrack addPdu(Pdu newPdu)
+    {
+        if (newPdu.getPduType() == DisPduType.ENTITY_STATE)
+        {
+////          EntityStatePdu deepCopyEspdu = new EntityStatePdu();
+//            EntityStatePdu deepCopyEspdu = pduFactory.makeEntityStatePdu();
+//            deepCopyEspdu.setTimestamp        (((EntityStatePdu)newPdu).getTimestamp());
+//            deepCopyEspdu.setMarking          (((EntityStatePdu)newPdu).getMarking());
+//            deepCopyEspdu.setEntityID         (((EntityStatePdu)newPdu).getEntityID());
+//            deepCopyEspdu.setForceId          (((EntityStatePdu)newPdu).getForceId());
+//            deepCopyEspdu.setEntityType       (((EntityStatePdu)newPdu).getEntityType());
+//            deepCopyEspdu.setEntityLocation   (((EntityStatePdu)newPdu).getEntityLocation());
+//            deepCopyEspdu.setEntityOrientation(((EntityStatePdu)newPdu).getEntityOrientation());
+            
+            EntityStatePdu deepCopyEspdu = ((EntityStatePdu)newPdu).copy();
+            pduList.add(deepCopyEspdu);
+//          alternative trials:
+//          pduList.add(((EntityStatePdu)newPdu).copyDataOutputStream());
+//          pduList.add(((EntityStatePdu)newPdu).copy());
+            
+            if (initialLocation == null)
+            {
+                initialEspdu    = deepCopyEspdu; // must save object since pduList might contain more than ESPDUs
+                initialLocation = deepCopyEspdu.getEntityLocation();
+            }
+            latestEspdu         = deepCopyEspdu; // must save object since pduList might contain more than ESPDUs
+            latestLocation      = deepCopyEspdu.getEntityLocation();
+        }
+        else pduList.add(newPdu); // TODO copy() - careful, must be a new object and not a reference
+        return this;
+    }
+    /**
+     * clear all PDUs
+     * @return same object to permit progressive setters
+     */
+    public PduTrack clearPduLists()
+    {
+           getPduList().clear();
+          waypointsList.clear();
+        eulerAnglesList.clear();
+           timelineList.clear();
+           initialEspdu = null;
+            latestEspdu = null;
+        initialLocation = null;
+         latestLocation = null;
+        return this;
+    }
+    
+    private String normalizeNameToken(String candidateDEF)
+    {
+        return candidateDEF.replace(" ", "").replace("-", "")
+                           .replace("(", "").replace(")", "")
+                           .replace("[", "").replace("])", "");
+    }
+
+    /**
+     * Provide DEF value if not defined by program
+     * @return current x3dTimeSensorDEF
+     */
+    public String getX3dTimeSensorDEF() {
+        if    (x3dTimeSensorDEF.isEmpty())
+               x3dTimeSensorDEF = normalizeNameToken(getDescriptor()) + "Clock";
+        return x3dTimeSensorDEF;
+    }
+
+    /**
+     * Set DEF value for X3D node
+     * @param x3dTimeSensorDEF the x3dTimeSensorDEF to set
+     */
+    public void setX3dTimeSensorDEF(String x3dTimeSensorDEF) {
+        this.x3dTimeSensorDEF = normalizeNameToken(x3dTimeSensorDEF);
+    }
+
+    /**
+     * Provide DEF value if not defined by program
+     * @return current x3dPositionInterpolatorDEF
+     */
+    public String getX3dPositionInterpolatorDEF() {
+        if    (x3dPositionInterpolatorDEF.isEmpty())
+               x3dPositionInterpolatorDEF = normalizeNameToken(getDescriptor()) + "Positions";
+        return x3dPositionInterpolatorDEF;
+    }
+
+    /**
+     * Set DEF value for X3D node
+     * @param x3dPositionInterpolatorDEF the x3dPositionInterpolatorDEF to set
+     */
+    public void setX3dPositionInterpolatorDEF(String x3dPositionInterpolatorDEF) {
+        this.x3dPositionInterpolatorDEF = normalizeNameToken(x3dPositionInterpolatorDEF);
+    }
+
+    /**
+     * Provide DEF value if not defined by program
+     * @return current x3dOrientationInterpolatorDEF
+     */
+    public String getX3dOrientationInterpolatorDEF() {
+        if    (x3dOrientationInterpolatorDEF.isEmpty())
+               x3dOrientationInterpolatorDEF = normalizeNameToken(getDescriptor()) + "Orientations";
+        return x3dOrientationInterpolatorDEF;
+    }
+
+    /**
+     * Set DEF value for X3D node
+     * @param x3dOrientationInterpolatorDEF the x3dOrientationInterpolatorDEF to set
+     */
+    public void setX3dOrientationInterpolatorDEF(String x3dOrientationInterpolatorDEF) {
+        this.x3dOrientationInterpolatorDEF = normalizeNameToken(x3dOrientationInterpolatorDEF);
+    }
+
+    /**
+     * Sort all PDUs by timestamp
+     * @see <a href="https://stackoverflow.com/questions/16252269/how-to-sort-an-arraylist">StackOverflow: How to sort an ArrayList?</a>
+     * @see <a href="https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/doc-files/coll-overview.html">Collections Framework Overview</a>
+     * @return same object to permit progressive setters
+     */
+    public PduTrack sortPdus()
+    {
+        Collections.sort(pduList, new Comparator<Pdu>() {
+            @Override
+            public int compare(Pdu lhs, Pdu rhs)
+            {
+                // -1 less than, 1 greater than, 0 equal
+                if      (lhs.occursBefore(rhs))
+                         return -1;
+                else if (lhs.occursSameTime(rhs))
+                         return 0;
+                else     return 1;
+            }
+        });
+        return this;
+    }
+    /**
+     * Reverse order of PDU list
+     * @see <a href="https://stackoverflow.com/questions/10766492/what-is-the-simplest-way-to-reverse-an-arraylist">StackOverflow: What is the Simplest Way to Reverse an ArrayList?</a>
+     * @see <a href="https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/doc-files/coll-overview.html">Collections Framework Overview</a>
+     * @return same object to permit progressive setters
+     */
+    public PduTrack reversePdus()
+    {
+        Collections.reverse(pduList);
+        return this;
+    }
+
+    /**
+     * Determine whether any ESPDUs have been received by this track
+     * @return whether track is empty
+     */
+    public boolean isTrackEmpty()
+    {
+        return (getEspduCount() == 0);
+    }
+    /**
+     * Count ESPDUs in pduList
+     * @return number of ESPDUs in pduList
+     */
+    public int getEspduCount()
+    {
+        int counter = 0;
+        for (Pdu nextPdu : getPduList())
+        {
+            if (nextPdu.getPduType() == DisPduType.ENTITY_STATE)
+               counter += 1;
+        }
+        return counter;
+    }
+    /**
+     * Compute track duration in timestamp ticks
+     * @return duration in timestamp ticks between initial and final ESPDU timestamps in waypointList
+     */
+    public int getTotalDurationTicks()
+    {
+        int    initialTime = -1;
+        int      finalTime = -1;
+        int  durationTicks = -1; // used if pduList is empty
+        
+        // must skip through pduList since non-ESPDU PDUs may be present
+        for (Pdu nextPdu : getPduList())
+        {
+            if (nextPdu.getPduType() == DisPduType.ENTITY_STATE)
+            {
+                if (initialTime == -1)
+                    initialTime = nextPdu.getTimestamp();
+                finalTime = nextPdu.getTimestamp();
+            }
+        }
+        if ((initialTime >= 0) && (finalTime >= 0))
+             durationTicks = (finalTime - initialTime);
+        if (getPduList().isEmpty())
+        {
+            System.out.println(TRACE_PREFIX + "getTrackDuration() computed illegal duration=" + durationTicks + " due to empty pdu list");
+        }
+        else if ((durationTicks <= 0) && (defaultWaypointInterval <= 0))
+        {
+            System.out.println(TRACE_PREFIX + "getTrackDuration() computed illegal duration=" + durationTicks + " due to illegal pdu list");
+        }
+        return durationTicks;
+    }
+    /**
+     * Compute track duration in seconds
+     * @return duration in seconds between initial and final ESPDU timestamps in waypointList
+     */
+    public float getTotalDurationSeconds()
+    {
+        if (defaultWaypointInterval > 0)
+        {
+            return getEspduCount() * defaultWaypointInterval;
+        }
+        else if (getTotalDurationTicks() < 0)
+               durationSeconds = getTotalDurationTicks() * 1.0f; // TODO convert
+        return durationSeconds;
+    }
+    /**
+     * Create waypoints and angles using all ESPDU points, with no linear regression or array reduction.
+     * @return same object to permit progressive setters
+     */
+    public PduTrack createRawWaypoints()
+    {
+        // https://stackoverflow.com/questions/6536094/java-arraylist-copy
+        
+           timelineList.clear();
+          waypointsList.clear();
+        eulerAnglesList.clear();
+        float clock = 0.0f;
+        for (int i = 0; i < pduList.size(); i++)
+        {
+            Pdu nextPdu = pduList.get(i);
+            if (nextPdu.getPduType() == DisPduType.ENTITY_STATE)
+            {
+                EntityStatePdu espdu = (EntityStatePdu)nextPdu;
+                if (defaultWaypointInterval > 0)
+                {
+                    timelineList.add(clock);
+                    clock += defaultWaypointInterval;
+                }
+                else
+                {
+                    timelineList.add(espdu.getTimestamp() * 1.0f); // TODO convert
+                }
+                   waypointsList.add(espdu.getEntityLocation());
+                 eulerAnglesList.add(espdu.getEntityOrientation());
+            }
+        }
+        return this;
+    }
+    /** 
+     * Utility method to create TimeSensor
+     * @return TimeSensor string in XML format
+     */
+    public String createX3dTimeSensorString()
+    {
+        StringBuilder sb = new StringBuilder();
+        sb.append("    <TimeSensor");
+        sb.append(" DEF='").append(getX3dTimeSensorDEF()).append("'");
+        sb.append(" cycleInterval='").append(String.valueOf(getTotalDurationSeconds())).append("'");
+        sb.append(" loop='true'");
+        sb.append("/>").append("\n");
+        
+        return sb.toString();
+    }
+    /**
+     * Create PositionInterpolator from Pdu list
+     * @return X3D PositionInterpolator as string
+     */
+    public String createX3dPositionInterpolatorString()
+    {
+        StringBuilder sb = new StringBuilder();
+        sb.append("    <PositionInterpolator");
+        sb.append(" DEF='").append(getX3dPositionInterpolatorDEF()).append("'");
+        sb.append(" key='");
+        for (int i = 0; i < timelineList.size(); i++)
+        {
+            float nextDuration = timelineList.get(i) * 1.0f; // TODO convert
+            sb.append(String.valueOf(nextDuration));
+            if (i < timelineList.size() - 1)
+                sb.append(" ");
+        }
+        sb.append("'");
+        sb.append(" keyValue='");
+        for (int i = 0; i < waypointsList.size(); i++)
+        {
+            if (hasAddLineBreaksWithinKeyValues())
+                sb.append("\n");
+            Vector3Double nextPosition = waypointsList.get(i);
+            sb.append(String.valueOf(nextPosition.getX())).append(" ")
+              .append(String.valueOf(nextPosition.getY())).append(" ")
+              .append(String.valueOf(nextPosition.getZ()));
+            if (i < waypointsList.size() - 1)
+                sb.append(",");
+        }
+        sb.append("'");
+        sb.append("/>").append("\n");
+        
+        return sb.toString();
+    }
+    /**
+     * Create OrientationInterpolator from Pdu list
+     * TODO preliminary support only includes horizontal rotation.
+     * @return X3D OrientationInterpolator as string
+     */
+    public String createX3dOrientationInterpolatorString()
+    {
+        StringBuilder sb = new StringBuilder();
+        sb.append("    <OrientationInterpolator");
+        sb.append(" DEF='").append(getX3dOrientationInterpolatorDEF()).append("'");
+        sb.append(" key='");
+        for (int i = 0; i < timelineList.size(); i++)
+        {
+            float nextDuration = timelineList.get(i) * 1.0f; // TODO convert
+            sb.append(String.valueOf(nextDuration));
+            if (i < timelineList.size() - 1)
+                sb.append(" ");
+        }
+        sb.append("'");
+        sb.append(" keyValue='");
+        for (int i = 0; i < eulerAnglesList.size(); i++)
+        {
+            if (hasAddLineBreaksWithinKeyValues())
+                sb.append("\n");
+            EulerAngles nextEulerAngle = new EulerAngles();
+            float axisX = 0.0f;
+            float axisY = 1.0f;
+            float axisZ = 0.0f;
+            float angle = 0.0f; // radians
+            
+            nextEulerAngle = eulerAnglesList.get(i);
+            angle = nextEulerAngle.getTheta();
+            
+            sb.append(String.valueOf(axisX)).append(" ")
+              .append(String.valueOf(axisY)).append(" ")
+              .append(String.valueOf(axisZ)).append(" ")
+              .append(String.valueOf(angle));
+            if (i < eulerAnglesList.size() - 1)
+                sb.append(",");
+        }
+        sb.append("'");
+        sb.append("/>").append("\n");
+        
+        return sb.toString();
+    }
+
+    /**
+     * Get name of author used as creator of X3D model
+     * @return current author
+     */
+    public String getAuthor() {
+        return author;
+    }
+
+    /**
+     * Set name of author used as creator of X3D model
+     * @param author the author to set
+     */
+    public void setAuthor(String author) {
+        if  (author == null)
+             author = new String();
+        author = author.trim();
+        this.author = author;
+    }
+
+    /**
+     * Get name of online url identifier for X3D model
+     * @return current x3dModelIdentifier
+     */
+    public String getX3dModelIdentifier() {
+        return x3dModelIdentifier;
+    }
+
+    /**
+     * Set name of online url identifier for X3D model
+     * @param x3dModelIdentifier the x3dModelIdentifier to set
+     */
+    public void setX3dModelIdentifier(String x3dModelIdentifier) {
+        if  (x3dModelIdentifier == null)
+             x3dModelIdentifier = new String();
+        x3dModelIdentifier = x3dModelIdentifier.trim();
+        if (!x3dModelIdentifier.startsWith("http://") && !x3dModelIdentifier.startsWith("https://"))
+            System.out.println(TRACE_PREFIX + "warning, identifier typically begins with https:// or http://");
+        else this.x3dModelIdentifier = x3dModelIdentifier;
+    }
+
+    /**
+     * File name for X3D model production
+     * @return current x3dModelName
+     */
+    public String getX3dModelName() {
+        return x3dModelName;
+    }
+    /**
+     * File name for X3D model production
+     * @param x3dModelName the x3dModelName to set
+     */
+    public void setX3dModelName(String x3dModelName) {
+        if  (x3dModelName == null)
+             x3dModelName = new String();
+        x3dModelName = x3dModelName.trim();
+        this.x3dModelName = x3dModelName;
+    }
+
+    /**
+     * Verbose (but more readable) output of numeric arrays in X3D model
+     * @return current addLineBreaksWithinKeyValues
+     */
+    public boolean hasAddLineBreaksWithinKeyValues() {
+        return addLineBreaksWithinKeyValues;
+    }
+
+    /**
+     * Verbose (but more readable) output of numeric arrays in X3D model
+     * @param addLineBreaksWithinKeyValues the addLineBreaksWithinKeyValues to set
+     */
+    public void setAddLineBreaksWithinKeyValues(boolean addLineBreaksWithinKeyValues) {
+        this.addLineBreaksWithinKeyValues = addLineBreaksWithinKeyValues;
+    }
+    /**
+     * Create full X3D interpolator model from Pdu list, assembling sections of scene graph
+     * @return X3D model as string
+     */
+    public String createX3dModel()
+    {
+        StringBuilder sb = new StringBuilder();
+        sb.append(createX3dModelHeaderString());
+        sb.append(createX3dTimeSensorString());
+        sb.append(createX3dPositionInterpolatorString());
+        sb.append(createX3dOrientationInterpolatorString());
+        sb.append(createX3dRoutesGeometryFooterString());
+        return sb.toString();
+    }
+    /**
+     * Create PositionInterpolator from Pdu list
+     * @return X3D PositionInterpolator as string
+     */
+    public String createX3dModelHeaderString()
+    {        
+        StringBuilder sb = new StringBuilder();
+        
+        sb.append("<?xml version=\"1.0\" encoding=\"UTF-8\"?>").append("\n");
+        sb.append("<!DOCTYPE X3D PUBLIC \"ISO//Web3D//DTD X3D 4.0//EN\" \"https://www.web3d.org/specifications/x3d-4.0.dtd\">").append("\n");
+        sb.append("<X3D profile='Interchange' version='4.0' xmlns:xsd='http://www.w3.org/2001/XMLSchema-instance' xsd:noNamespaceSchemaLocation='https://www.web3d.org/specifications/x3d-4.0.xsd'>").append("\n");
+        sb.append("  <head>").append("\n");
+        if (!getX3dModelName().isEmpty())
+            sb.append("    <meta content='").append(getX3dModelName()).append("' name='title'/>").append("\n");
+        sb.append("    <meta content='Conversion of ESPDU track into X3D animation interpolators and LineSet.' name='description'/>").append("\n");
+        
+        sb.append("    <meta content='1 January 2022' name='created'/>").append("\n");
+        sb.append("    <meta content='").append(todaysDateString).append("' name='modified'/>").append("\n");
+        if (!getAuthor().isEmpty())
+            sb.append("    <meta content='").append(getAuthor()).append("' name='creator'/>").append("\n");
+        if (!getX3dModelIdentifier().isEmpty())
+            sb.append("    <meta content='").append(getX3dModelIdentifier()).append("' name='identifier'/>").append("\n");
+        
+        sb.append("    <meta content='PduTrack utility, opendis7-java Library https://github.com/open-dis/opendis7-java' name='generator'/>").append("\n");
+        sb.append("    <meta content='NPS MOVES MV3500 Networked Graphics https://gitlab.nps.edu/Savage/NetworkedGraphicsMV3500' name='reference'/>").append("\n");
+        sb.append("    <meta content='X3D Resources https://www.web3d.org/x3d/content/examples/X3dResources.html' name='reference'/>").append("\n");
+        sb.append("    <meta content='X3D Scene Authoring Hints https://www.web3d.org/x3d/content/examples/X3dSceneAuthoringHints.html' name='reference'/>").append("\n");
+        sb.append("    <meta content='X3D Tooltips https://www.web3d.org/x3d/tooltips/X3dTooltips.html' name='reference'/>").append("\n");
+        sb.append("    <meta content='X3D Validator https://savage.nps.edu/X3dValidator' name='reference'/>").append("\n");
+        sb.append("    <meta content='Open source https://raw.githubusercontent.com/open-dis/opendis7-java/master/license.html' name='license'/>").append("\n");
+        sb.append("  </head>").append("\n");
+        sb.append("  <Scene>").append("\n");
+        sb.append("    <WorldInfo title='PduTrackInterpolation.x3d'/>").append("\n");
+
+        return sb.toString(); 
+    }
+    /**
+     * Create X3D ROUTEs and footer to connect TimeSensor to interpolators
+     * @return X3D PositionInterpolator as string
+     */
+    public String createX3dRoutesGeometryFooterString()
+    {
+        StringBuilder sb = new StringBuilder();
+        
+        sb.append("    <ROUTE fromField='fraction_changed' fromNode='")
+          .append(getX3dTimeSensorDEF())
+          .append("' toField='set_fraction' toNode='")
+          .append(getX3dPositionInterpolatorDEF())
+          .append("'/>").append("\n");
+        sb.append("    <ROUTE fromField='fraction_changed' fromNode='")
+          .append(getX3dTimeSensorDEF())
+          .append("' toField='set_fraction' toNode='")
+          .append(getX3dOrientationInterpolatorDEF())
+          .append("'/>").append("\n");
+        
+        sb.append("    <Shape>").append("\n");
+        sb.append("      <Appearance DEF='TrackAppearance'>").append("\n");
+        sb.append("        <Material emissiveColor='0.2 0.8 0.8'/>").append("\n");
+        sb.append("      </Appearance>").append("\n");
+        sb.append("      <LineSet vertexCount='").append(waypointsList.size()).append("'>").append("\n");
+        sb.append("        <Coordinate point='");
+        for (int i = 0; i < waypointsList.size(); i++)
+        {
+            if (hasAddLineBreaksWithinKeyValues())
+                sb.append("\n");
+            Vector3Double nextPosition = waypointsList.get(i);
+            sb.append(String.valueOf(nextPosition.getX())).append(" ")
+              .append(String.valueOf(nextPosition.getY())).append(" ")
+              .append(String.valueOf(nextPosition.getZ()));
+            if (i < waypointsList.size() - 1)
+                sb.append(",");
+        }
+        sb.append("'/>").append("\n");
+        sb.append("      </LineSet>").append("\n");
+        sb.append("    </Shape>").append("\n");
+        sb.append("    <Transform DEF='AnimationTransform'>").append("\n");
+        sb.append("      <Transform rotation='0 0 1 1.57'>").append("\n");
+        sb.append("        <Shape>").append("\n");
+        sb.append("          <Appearance USE='TrackAppearance'/>").append("\n");
+        sb.append("          <Cone bottomRadius='0.5'/>").append("\n");
+        sb.append("        </Shape>").append("\n");
+        sb.append("      </Transform>").append("\n");
+        sb.append("    </Transform>").append("\n");
+        sb.append("    <ROUTE fromField='value_changed' fromNode='")
+          .append(getX3dPositionInterpolatorDEF())
+          .append("' toField='translation' toNode='AnimationTransform'/>").append("\n");
+        sb.append("    <ROUTE fromField='value_changed' fromNode='")
+          .append(getX3dOrientationInterpolatorDEF())
+          .append("' toField='rotation' toNode='AnimationTransform'/>").append("\n");
+        
+        sb.append("  </Scene>").append("\n");
+        sb.append("</X3D>").append("\n");
+        
+        return sb.toString();
+    }
+
+    /**
+     * get defaultWaypointInterval
+     * @return current wayPointInterval
+     */
+    public float getDefaultWaypointInterval() {
+        return defaultWaypointInterval;
+    }
+
+    /**
+     * Set uniform waypoint interval (currently for testing)
+     * @param newWaypointInterval the wayPointInterval to set, in seconds, must be greater than zero
+     * @return same object to permit progressive setters */
+    public PduTrack setDefaultWaypointInterval(float newWaypointInterval) {
+        if (newWaypointInterval > 0.0)
+            this.defaultWaypointInterval = newWaypointInterval;
+        else 
+        {
+            System.out.println(TRACE_PREFIX + "error in setWaypointInterval(newWaypointInterval=" + newWaypointInterval + ") must be greater than zero");
+            return this;
+        }
+            
+        float clock = 0.0f;
+        if (!timelineList.isEmpty())
+        {   
+            ArrayList<Float> newTimelineList = new ArrayList<>();
+            for (int i = 0; i < getEspduCount(); i++)
+            {
+                newTimelineList.add(clock);
+                clock += defaultWaypointInterval;
+            }
+            timelineList = newTimelineList; // TO Array copy?
+        }
+        return this;
+    }
+    /** whether or not to insert commas between hex values */
+    private boolean insertCommas = true;
+    /**
+     * determine whether comma insertion is turned on
+     * @return whether or not to insert commas between hex values
+     */
+    public boolean hasInsertCommas() {
+        return insertCommas;
+    }
+    /**
+     * set whether comma insertion is turned on
+     * @param insertCommas the insertCommas value to set
+     */
+    public void setInsertCommas(boolean insertCommas) {
+        this.insertCommas = insertCommas;
+    }
+    /**
+     * Convert byte array to hex string
+     * @param bytes input data
+     * @param insertCommas whether to insert commas between hex values
+     * @return hex string
+     */
+    public String bytesToHex(byte[] bytes, boolean insertCommas)
+    {
+        this.setInsertCommas(insertCommas);
+        return bytesToHex(bytes);
+    }
+    /**
+     * Convert byte array to hex string
+     * @param bytes input data
+     * @see <a href="https://stackoverflow.com/questions/9655181/how-to-convert-a-byte-array-to-a-hex-string-in-java">https://stackoverflow.com/questions/9655181/how-to-convert-a-byte-array-to-a-hex-string-in-java</a>
+     * @return hex string
+     */
+    public static String bytesToHex(byte[] bytes)
+    {
+        boolean insertCommas = true;
+        final char[] HEX_ARRAY = "0123456789ABCDEF".toCharArray();
+        char[] hexChars = new char[bytes.length * 2];
+        StringBuilder sb = new StringBuilder();
+        for (int j = 0; j < bytes.length; j++) {
+            int v = bytes[j] & 0xFF;
+            hexChars[j * 2]     = HEX_ARRAY[v >>> 4];
+            hexChars[j * 2 + 1] = HEX_ARRAY[v & 0x0F];
+//          if (!(hexChars[j * 2] == '0')) // omit leading zero
+            sb.append(hexChars[j * 2]);
+            sb.append(hexChars[j * 2 + 1]);
+            if (insertCommas && (j < bytes.length - 1))
+                sb.append(", ");
+        }
+        return sb.toString();
+    }
+    /**
+     * Report current PDU information to console
+     * @param anEspdu EntityStatePdu of interest
+     * @return same object to permit progressive setters
+     */
+    public PduTrack reportPdu(EntityStatePdu anEspdu)
+    {
+        System.out.println (
+                String.format("%s", anEspdu.getMarkingString().trim()) + ", " +
+                DisTime.convertToString(anEspdu.getTimestamp()) + " (" +
+                String.format("%08d", anEspdu.getTimestamp()) + "), " + 
+                "EntityID=(" + 
+                anEspdu.getEntityID().getSiteID() + ", " +
+                anEspdu.getEntityID().getApplicationID() + ", " + 
+                anEspdu.getEntityID().getEntityID() + "), " +
+                "location=(" + 
+                String.format("%4.1f", anEspdu.getEntityLocation().getX()) + ", " +
+                String.format("%4.1f", anEspdu.getEntityLocation().getY()) + ", " + 
+                String.format("%4.1f", anEspdu.getEntityLocation().getZ()) + ")"
+//              + " " + espdu_1.getEntityLinearVelocity().toString()
+        );
+        return this;
+    }
+    
+    /** Flush all buffers to reduce console scrambling while threaded
+     */
+    protected void flushBuffers()
+    {
+        try
+        {
+                 dataOutputStream.flush(); 
+            byteArrayOutputStream.flush();
+            byteArrayOutputStream.reset();
+            System.err.flush(); 
+            System.out.flush();
+        }
+        catch (IOException ioe)
+        {
+            System.out.println(TRACE_PREFIX + "flushBuffers() IOException: " + ioe.getMessage());
+        }
+    }
+    
+    /** Self test to check basic operation, invoked by main()
+     */
+    @SuppressWarnings("SleepWhileInLoop")
+    public void selfTest()
+    {
+        final int TOTAL_PDUS = 5;
+        System.out.println(TRACE_PREFIX + "selfTest() start...");
+      
+        PduTrack pduTrack = new PduTrack();
+        pduTrack.setDescriptor("PduTrack Self Test");
+        pduTrack.setAuthor("Don Brutzman");
+        pduTrack.setX3dModelIdentifier("https://gitlab.nps.edu/Savage/NetworkedGraphicsMV3500/-/blob/master/examples/src/OpenDis7Examples/PduTrackInterpolation.x3d");
+        pduTrack.setDefaultWaypointInterval(1.0f); // experimentation with timestamp values
+
+        DisTime.setEpochLvcNow();
+        recordingStart = LocalDateTime.now();
+        Instant epoch = DisTime.getEpochLvc();
+        System.out.println(TRACE_PREFIX + "DisTime.hasEpochLvc()=" + DisTime.hasEpochLvc() + 
+                                         ", DisTime.getEpochLvc()=" + epoch + 
+                                         ", Instant.now()=" + Instant.now());
+        
+        EntityID entityID_123 = new EntityID();
+        entityID_123.setSiteID(1).setApplicationID(2).setEntityID(3); // made-up example ID;
+        // TODO someday, use enumerations; is there a unique site triplet for MOVES Institute?
+
+        for (int i = 0; i < TOTAL_PDUS; i++) // create espdus and add each to track pduList
+        {
+//          EntityStatePdu espdu = new EntityStatePdu();
+            EntityStatePdu espdu = pduFactory.makeEntityStatePdu(); // TODO check Pdu.Type
+            espdu.setTimestamp(DisTime.getCurrentDisTimestamp()); // chooses appropriate version
+            espdu.setMarking("ESPDU " + i);
+            espdu.setEntityLocation(i, i, i);
+            espdu.setEntityOrientation(0, (float)(45.0 * Math.PI / 180.0), 0);
+            espdu.setEntityID(entityID_123);
+            espdu.setForceId(ForceID.FRIENDLY);
+            espdu.setEntityType(new _001Poseidon()); // note import statement above
+            pduTrack.addPdu(espdu); // create copy
+            reportPdu(espdu);
+            try
+            {
+                Thread.sleep(100l);
+            }
+            catch (InterruptedException ie)
+            {
+                System.out.println(TRACE_PREFIX + "exceptional sleep dulay when generating ESPDUs: " + ie.getMessage());
+            }
+        }
+//        System.out.println(TRACE_PREFIX + "reversePdus() then sortPdus() to test track operations");
+//        pduTrack.reversePdus(); // test
+//        pduTrack.sortPdus();    // restore
+        pduTrack.createRawWaypoints(); // copies all ESPDU points to waypoints
+        
+        System.out.println(TRACE_PREFIX + "getEspduCount()="              + pduTrack.getEspduCount());
+        System.out.println(TRACE_PREFIX + "getDefaultWaypointInterval()=" + pduTrack.getDefaultWaypointInterval());
+        System.out.println(TRACE_PREFIX + "getTotalDurationSeconds()="    + pduTrack.getTotalDurationSeconds());
+        
+        System.out.println("=================================");
+        System.out.println("PduTrack pduList marshalling checks");
+        System.out.println("= = = = = = = = = = = = = = = = =");
+        try
+        {
+//          int BYTE_BUFFER_SIZE = 400; // TODO what is expected max buffer size?
+            for (int i = 0; i < TOTAL_PDUS; i++)
+            {
+                Pdu pdu = pduTrack.getPduList().get(i);
+                if (!(pdu instanceof EntityStatePdu))
+                    continue; // skip remainder of this loop
+                EntityStatePdu espdu = (EntityStatePdu) pdu;
+                System.out.println("espdu from pduTrack pduList");
+                reportPdu(espdu);
+                byte[] byteArray = espdu.marshal();
+                System.out.println("espdu.marshal() byteArray:                              " + bytesToHex(byteArray));
+                flushBuffers();
+                
+                ByteBuffer byteBuffer = ByteBuffer.allocate(byteArray.length);
+                espdu.marshal(byteBuffer);
+                System.out.println("espdu.marshal(byteBuffer):                              " + bytesToHex(byteBuffer.array()));
+                flushBuffers();
+                
+                espdu.marshal(dataOutputStream);
+                byte[] byteArrayDOS = byteArrayOutputStream.toByteArray();
+                System.out.println("espdu.marshal(dataOutputStream):                        " + bytesToHex(byteArrayDOS));
+                flushBuffers();
+
+                System.out.println(); // - - - - - - - - - - - - - - - - -
+                
+                System.out.println("espdu.copyByteBuffer()");
+                reportPdu(espdu.copyByteBuffer());
+                byte[] byteArrayCopy = espdu.copyByteBuffer().marshal();
+                System.out.println("espdu.copyByteBuffer().marshal() byteArray:             " + bytesToHex(byteArrayCopy));
+                flushBuffers();
+                
+                ByteBuffer byteBufferCopy = ByteBuffer.allocate(byteArray.length); // TODO is there a better way to reset?
+                espdu.copyByteBuffer().marshal(byteBufferCopy);
+                System.out.println("espdu.copyByteBuffer().marshal(byteBufferCopy):         " + bytesToHex(byteBufferCopy.array()));
+                flushBuffers();
+                
+                espdu.copyByteBuffer().marshal(dataOutputStream);
+                byte[] byteArrayDosCopy = byteArrayOutputStream.toByteArray();
+                System.out.println("espdu.copyByteBuffer().marshal(dataOutputStream):       " + bytesToHex(byteArrayDosCopy));
+                flushBuffers();
+
+                System.out.println(); // - - - - - - - - - - - - - - - - -
+                
+                System.out.println("espdu.copyDataOutputStream()");
+                reportPdu(espdu.copyDataOutputStream());
+                byte[] byteArrayCopyDOS = espdu.copyDataOutputStream().marshal();
+                System.out.println("espdu.copyDataOutputStream().marshal() byteArray:       " + bytesToHex(byteArrayCopyDOS));
+                flushBuffers();
+                
+                ByteBuffer byteBufferCopyDOS = ByteBuffer.allocate(byteArray.length); // TODO is there a better way to reset?
+                espdu.copyDataOutputStream().marshal(byteBufferCopyDOS);
+                System.out.println("espdu.copyDataOutputStream().marshal(byteBufferCopy):   " + bytesToHex(byteBufferCopyDOS.array()));
+                flushBuffers();
+                
+                espdu.copyDataOutputStream().marshal(dataOutputStream);
+                byte[] byteArrayDosCopy2 = byteArrayOutputStream.toByteArray();
+                System.out.println("espdu.copyDataOutputStream().marshal(dataOutputStream): " + bytesToHex(byteArrayDosCopy2));
+                flushBuffers();
+                System.out.println();
+                
+                System.out.println("= = = = = = = = = = = = = = = = =");
+            }
+        }
+        catch(Exception e)
+        {
+            System.out.println(TRACE_PREFIX + "Marshalling test exception: " + e.getMessage());
+        }
+        System.out.println("=================================");
+        pduTrack.setAddLineBreaksWithinKeyValues(true);
+        System.out.println(pduTrack.createX3dModel()); // 
+        System.out.println("=================================");
+
+        recordingStop = LocalDateTime.now();
+        System.out.println(TRACE_PREFIX + "selfTest() complete.");
+    }
+    
+    /**
+     * 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 [address, port, descriptor] 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("*** PduTrack.main() self test started...");
+          
+        PduTrack pduTrack = new PduTrack();
+        
+        pduTrack.setDescriptor("main() self test");
+        
+        pduTrack.selfTest();
+        
+        System.out.println("*** PduTrack.main() self test complete.");
+    }
+
+}
diff --git a/assignments/src/MV3500Cohort2022MayJune/homework2/Tam/TamTcpExampleClient.java b/assignments/src/MV3500Cohort2022MayJune/homework2/Tam/TamTcpExampleClient.java
deleted file mode 100644
index 05157977aa..0000000000
--- a/assignments/src/MV3500Cohort2022MayJune/homework2/Tam/TamTcpExampleClient.java
+++ /dev/null
@@ -1,89 +0,0 @@
-package MV3500Cohort2022MayJune.homework2.Tam;
-import MV3500Cohort2022MayJune.homework2.Duran.*;
-import MV3500Cohort2021JulySeptember.homework2.Domonique.*;
-import java.io.*;
-import java.net.*;
-
-/**
- * 
- * @author Johanna Tam
- */
-public class TamTcpExampleClient {
-
-    /** IPv6 String constant for localhost address, similarly IPv4 127.0.0.1
-     * @see <a href="https://en.wikipedia.org/wiki/localhost">https://en.wikipedia.org/wiki/localhost</a>
-     * @see <a href="https://en.wikipedia.org/wiki/IPv6_address">https://en.wikipedia.org/wiki/IPv6_address</a> 
-     */
-    public final static String LOCALHOST = "0:0:0:0:0:0:0:1";
-
-    /**
-     * Program invocation, execution starts here
-     * @param args command-line arguments
-     * @throws java.lang.InterruptedException user can cancel execution
-     */
-    public static void main(String[] args) throws InterruptedException {
-        
-        // Local variables/fields
-        Socket socket = null;
-        InputStream is;
-        Reader isr;
-        BufferedReader br;
-        String serverMessage;
-        int clientLoopCount = 0;
-        
-        try {
-            while (true)
-            {
-                clientLoopCount++; // increment at beginning of loop for reliability
-                System.out.println(TamTcpExampleClient.class.getName() + " creating socket...");
-
-                // We request an IP to connect to ("localhost") and
-                // port number at that IP (2317). This establishes
-                // a connection to that IP in the form of a Socket
-                // object; the server uses a ServerSocket to wait for
-                // connections.
-                socket = new Socket(LOCALHOST, 2317); // locohost?
-
-                // Now hook everything up (i.e. set up the streams), Java style:
-                is  = socket.getInputStream();
-                isr = new InputStreamReader(is);
-                br  = new BufferedReader(isr);
-
-                // Read a single line written by the server. We'd
-                // do things a bit differently if there were many lines to be read
-                // from the server instead of one only.
-                serverMessage = br.readLine();
-                System.out.println("==================================================");
-                       
-//                System.out.print  ("Client loop " + clientLoopCount + ": ");
-                System.out.println("I am hungry");
-                System.out.println("The message the server sent was: '" + serverMessage + "'");
-                // socket gets closed, either automatically/silently by this code (or possibly by the server)
-                
-                Thread.sleep(500l); // slow things down, for example 500l (long) = 500 msec (1/2 second)
-                
-            } // end while(true) // infinite loops are dangerous, be sure to kill this process!
-        } 
-        catch (IOException e)
-        {
-            System.err.println("Problem with " + TamTcpExampleClient.class.getName() + " networking:"); // describe what is happening
-            System.err.println("Error: " + e);
-            
-            // Provide more helpful information to user if exception occurs due to running twice at one time
-            if (e instanceof java.net.BindException) {
-                System.err.println("*** Be sure to stop any other running instances of programs using this port!");
-            }
-        }
-        finally // occurs after any other activity when shutting down
-        {
-            try {
-                if (socket != null)
-                    socket.close();
-            } catch (IOException e) {}
-            
-            // program exit: tell somebody about that happening.  Likely cause: server drops connection.
-            System.out.println();
-            System.out.println(TamTcpExampleClient.class.getName() + " exit");
-        }
-    }
-}
\ No newline at end of file
diff --git a/assignments/src/MV3500Cohort2022MayJune/homework2/Tam/TamTcpExampleServer.java b/assignments/src/MV3500Cohort2022MayJune/homework2/Tam/TamTcpExampleServer.java
deleted file mode 100644
index 18154a40ec..0000000000
--- a/assignments/src/MV3500Cohort2022MayJune/homework2/Tam/TamTcpExampleServer.java
+++ /dev/null
@@ -1,90 +0,0 @@
-package MV3500Cohort2022MayJune.homework2.Tam;
-
-import MV3500Cohort2022MayJune.homework2.Duran.*;
-import MV3500Cohort2021JulySeptember.homework2.Domonique.*;
-import java.io.*;
-import java.net.*;
-
-/**
- * homework assignment
- * @author Johanna Tam
- */
-public class TamTcpExampleServer {
-
-    /**
-     * Program invocation, execution starts here
-     * If already compiled, can run using console in directory ../../build/classes/ by invoking \
-     *      java -classpath . TcpExamples.TcpExample3Server
-     * @param args command-line arguments
-     */
-    public static void main(String[] args) {
-        try {
-            
-            // ServerSocket waits for a connection from a client. 
-            // Notice that it is outside the loop; ServerSocket
-            // needs to be made only once.
-            System.out.println(TamTcpExampleServer.class.getName() + " has started..."); // it helps debugging to put this on console first
-            
-            ServerSocket serverSocket = new ServerSocket(2317);
-            OutputStream os;
-            PrintStream ps;
-            InetAddress localAddress, remoteAddress;
-            int localPort, remotePort;
-            int serverLoopCount = 0;
-
-            // Server is up and waiting (i.e. "blocked" or paused)
-            // Loop, infinitely, waiting for client connections.
-            // Stop the program somewhere else.
-            while (true) { 
-                
-                // block until connected to a client
-                try (Socket clientConnectionSocket = serverSocket.accept())
-                {
-                    serverLoopCount++; // increment at beginning of loop for reliability
-                    
-                    // Now hook everything up (i.e. set up the streams), Java style:
-                    os = clientConnectionSocket.getOutputStream();
-                    ps = new PrintStream(os);
-                    ps.println("okay " + serverLoopCount + " let's go to Starbucks"); // this gets sent back to client!
-                    
-                    // Print some information locally about the Socket connection.
-                    // This includes the port and IP numbers on both sides (the socket pair).
-                     localAddress = clientConnectionSocket.getLocalAddress();
-                    remoteAddress = clientConnectionSocket.getInetAddress();
-                        localPort = clientConnectionSocket.getLocalPort();
-                       remotePort = clientConnectionSocket.getPort();
-                       
-                    System.out.print ("Server loop " + serverLoopCount + ": ");
-                    
-                    // My socket pair connection looks like this, to localhost:
-                    // Socket pair: (( /0:0:0:0:0:0:0:1, 2317 ), ( /0:0:0:0:0:0:0:1, 54876 ))
-                    // Socket pair: (( /0:0:0:0:0:0:0:1, 2317 ), ( /0:0:0:0:0:0:0:1, 54881 ))
-                    
-                    // Why is the first IP/port the same, while the second set has different ports?
-                    System.out.println(TamTcpExampleServer.class.getName() + " socket pair showing host name, address, port:");
-                    System.out.println("  (( " + 
-                         localAddress.getHostName() + "=" +  localAddress.getHostAddress() + ", " + localPort + " ), ( " + 
-                        remoteAddress.getHostName() + "=" + remoteAddress.getHostAddress() + ", " + remotePort + " ))");
-                    
-                    if ( localAddress.getHostName().equals( localAddress.getHostAddress()) ||
-                        remoteAddress.getHostName().equals(remoteAddress.getHostAddress()))
-                        System.out.println("  note HostName matches address if host has no DNS name");
-                    
-                    // Notice the use of flush() and try w/ resources. Without
-                    // the try w/ resources the Socket object may stay open for
-                    // a while after the client has stopped needing this
-                    // connection. try w/ resources explicitly ends the connection.
-                    ps.flush();
-                    // like it or not, you're outta here!
-                }
-            }
-        } catch (IOException e) {
-            System.err.println("Problem with " + TamTcpExampleServer.class.getName() + " networking: " + e);
-
-            // Provide more helpful information to user if exception occurs due to running twice at one time
-            if (e instanceof java.net.BindException) {
-                System.err.println("*** Be sure to stop any other running instances of programs using this port!");
-            }
-        }
-    }
-}
diff --git a/assignments/src/MV3500Cohort2022MayJune/homework2/Tam/package-info.java b/assignments/src/MV3500Cohort2022MayJune/homework2/Tam/package-info.java
deleted file mode 100644
index 76d553f312..0000000000
--- a/assignments/src/MV3500Cohort2022MayJune/homework2/Tam/package-info.java
+++ /dev/null
@@ -1,10 +0,0 @@
-/**
- * ExampleSimpleSimulation program-modification homework assignment supporting the NPS MOVES MV3500 Networked Graphics course.
- * 
- * @see <a href="https://gitlab.nps.edu/Savage/NetworkedGraphicsMV3500/-/tree/master/assignments">networkedGraphicsMV3500 assignments</a>
- * @see java.lang.Package
- * @see <a href="https://stackoverflow.com/questions/22095487/why-is-package-info-java-useful">StackOverflow: why-is-package-info-java-useful</a>
- * @see <a href="https://stackoverflow.com/questions/624422/how-do-i-document-packages-in-java">StackOverflow: how-do-i-document-packages-in-java</a>
- */
-
-package MV3500Cohort2022MayJune.homework2.Tam;
-- 
GitLab