From a1f1ca79b6fc7909ccff6ec9d44f02431706aa36 Mon Sep 17 00:00:00 2001
From: brutzman <brutzman@nps.edu>
Date: Mon, 3 Jan 2022 09:05:04 -0800
Subject: [PATCH] PduTrack.createX3dModel output

---
 .../ExampleTrackInterpolation.java            |   5 +-
 .../ExampleTrackInterpolationLog.txt          |  31 +++-
 examples/src/OpenDis7Examples/PduTrack.java   | 173 ++++++++++++++++--
 examples/src/OpenDis7Examples/PduTrackLog.txt |  46 +++++
 4 files changed, 234 insertions(+), 21 deletions(-)
 create mode 100644 examples/src/OpenDis7Examples/PduTrackLog.txt

diff --git a/examples/src/OpenDis7Examples/ExampleTrackInterpolation.java b/examples/src/OpenDis7Examples/ExampleTrackInterpolation.java
index eb7ab1687a..2e0a322d24 100644
--- a/examples/src/OpenDis7Examples/ExampleTrackInterpolation.java
+++ b/examples/src/OpenDis7Examples/ExampleTrackInterpolation.java
@@ -178,8 +178,9 @@ public class ExampleTrackInterpolation extends ExampleSimulationProgram
             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(pduTrack_1.createX3dTimeSensorString());
-            System.out.println(pduTrack_1.createX3dPositionInterpolatorString());
+            System.out.println("=================================");
+            System.out.println(pduTrack_1.createX3dModel());
+            System.out.println("=================================");
             
             narrativeMessage2 = "runSimulation() completed successfully"; // all done
             sendCommentPdu(narrativeComment, narrativeMessage1, narrativeMessage2, narrativeMessage3);
diff --git a/examples/src/OpenDis7Examples/ExampleTrackInterpolationLog.txt b/examples/src/OpenDis7Examples/ExampleTrackInterpolationLog.txt
index 106cc9f0e6..9e974477ca 100644
--- a/examples/src/OpenDis7Examples/ExampleTrackInterpolationLog.txt
+++ b/examples/src/OpenDis7Examples/ExampleTrackInterpolationLog.txt
@@ -13,7 +13,7 @@ run-single:
 [DisThreadedNetworkInterface] createThreads() sendingThread.isAlive()=true
 Network confirmation: address=239.1.2.3 port=3000
 Beginning pdu save to directory ./pduLog
-Recorder log file open: C:\x-nps-gitlab\NetworkedGraphicsMV3500\examples\pduLog\PduCaptureLog165.dislog
+Recorder log file open: C:\x-nps-gitlab\NetworkedGraphicsMV3500\examples\pduLog\PduCaptureLog171.dislog
 [DisThreadedNetworkInterface] using network interface Intel(R) Wi-Fi 6E AX210 160MHz
 [DisThreadedNetworkInterface] datagramSocket.joinGroup  address=239.1.2.3 port=3000 isConnected()=false createDatagramSocket() complete.
 [DisThreadedNetworkInterface] createThreads() receiveThread.isAlive()=true
@@ -66,14 +66,35 @@ Recorder log file open: C:\x-nps-gitlab\NetworkedGraphicsMV3500\examples\pduLog\
 [OpenDis7Examples.ExampleTrackInterpolation] pduTrack_1 initialLocation=Vector3Double x:0.0 y:0.0 z:0.0, latestLocation=Vector3Double x:-1.0 y:0.0 z:0.0
 pduTrack_1 getEspduCount()=42
 pduTrack_1 duration = 42.0 seconds = 0 ticks
-<TimeSensor DEF='testing123Clock' cycleInterval='42.0' loop='true'/>
-<PositionInterpolator DEF='testing123Positions' key='0.0 1.0 2.0 3.0 4.0 5.0 6.0 7.0 8.0 9.0 10.0 11.0 12.0 13.0 14.0 15.0 16.0 17.0 18.0 19.0 20.0 21.0 22.0 23.0 24.0 25.0 26.0 27.0 28.0 29.0 30.0 31.0 32.0 33.0 34.0 35.0 36.0 37.0 38.0 39.0 40.0 41.0' keyValue='0.0 0.0 0.0,0.0 1.0 0.0,0.0 2.0 0.0,0.0 3.0 0.0,0.0 4.0 0.0,0.0 5.0 0.0,0.0 6.0 0.0,0.0 7.0 0.0,0.0 8.0 0.0,0.0 9.0 0.0,0.0 10.0 0.0,1.0 10.0 0.0,2.0 10.0 0.0,3.0 10.0 0.0,4.0 10.0 0.0,5.0 10.0 0.0,6.0 10.0 0.0,7.0 10.0 0.0,8.0 10.0 0.0,9.0 10.0 0.0,10.0 10.0 0.0,10.0 9.0 0.0,10.0 8.0 0.0,10.0 7.0 0.0,10.0 6.0 0.0,10.0 5.0 0.0,10.0 4.0 0.0,10.0 3.0 0.0,10.0 2.0 0.0,10.0 1.0 0.0,10.0 0.0 0.0,9.0 0.0 0.0,8.0 0.0 0.0,7.0 0.0 0.0,6.0 0.0 0.0,5.0 0.0 0.0,4.0 0.0 0.0,3.0 0.0 0.0,2.0 0.0 0.0,1.0 0.0 0.0,0.0 0.0 0.0,-1.0 0.0 0.0'/>
-*** setKillSentinelAndInterrupts() killed=true sendingThread.isInterrupted()=true receiveThread.isInterrupted()=true
+=================================
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE X3D PUBLIC "ISO//Web3D//DTD X3D 4.0//EN" "https://www.web3d.org/specifications/x3d-4.0.dtd">
+<X3D profile='Immersive' version='4.0' xmlns:xsd='http://www.w3.org/2001/XMLSchema-instance' xsd:noNamespaceSchemaLocation='https://www.web3d.org/specifications/x3d-4.0.xsd'>
+  <head>
+    <meta content='PduTrackInterpolator.x3d' name='title'/>
+    <meta content='PduTrack utility open-dis7-java Library https://github.com/open-dis/open-dis7-java' name='generator'/>
+    <meta content='NPS MOVES MV3500 Networked Graphics https://gitlab.nps.edu/Savage/NetworkedGraphicsMV3500' name='reference'/>
+    <meta content='Open source https://raw.githubusercontent.com/open-dis/open-dis7-java/master/license.html' name='license'/>
+  </head>
+  <Scene>
+    <TimeSensor DEF='testing123Clock' cycleInterval='42.0' loop='true'/>
+    <PositionInterpolator DEF='testing123Positions' key='0.0 1.0 2.0 3.0 4.0 5.0 6.0 7.0 8.0 9.0 10.0 11.0 12.0 13.0 14.0 15.0 16.0 17.0 18.0 19.0 20.0 21.0 22.0 23.0 24.0 25.0 26.0 27.0 28.0 29.0 30.0 31.0 32.0 33.0 34.0 35.0 36.0 37.0 38.0 39.0 40.0 41.0' keyValue='0.0 0.0 0.0,0.0 1.0 0.0,0.0 2.0 0.0,0.0 3.0 0.0,0.0 4.0 0.0,0.0 5.0 0.0,0.0 6.0 0.0,0.0 7.0 0.0,0.0 8.0 0.0,0.0 9.0 0.0,0.0 10.0 0.0,1.0 10.0 0.0,2.0 10.0 0.0,3.0 10.0 0.0,4.0 10.0 0.0,5.0 10.0 0.0,6.0 10.0 0.0,7.0 10.0 0.0,8.0 10.0 0.0,9.0 10.0 0.0,10.0 10.0 0.0,10.0 9.0 0.0,10.0 8.0 0.0,10.0 7.0 0.0,10.0 6.0 0.0,10.0 5.0 0.0,10.0 4.0 0.0,10.0 3.0 0.0,10.0 2.0 0.0,10.0 1.0 0.0,10.0 0.0 0.0,9.0 0.0 0.0,8.0 0.0 0.0,7.0 0.0 0.0,6.0 0.0 0.0,5.0 0.0 0.0,4.0 0.0 0.0,3.0 0.0 0.0,2.0 0.0 0.0,1.0 0.0 0.0,0.0 0.0 0.0,-1.0 0.0 0.0'/>
+    <OrientationInterpolator DEF='testing123Orientations' key='0.0 1.0 2.0 3.0 4.0 5.0 6.0 7.0 8.0 9.0 10.0 11.0 12.0 13.0 14.0 15.0 16.0 17.0 18.0 19.0 20.0 21.0 22.0 23.0 24.0 25.0 26.0 27.0 28.0 29.0 30.0 31.0 32.0 33.0 34.0 35.0 36.0 37.0 38.0 39.0 40.0 41.0' keyValue='0.0 1.0 0.0 0.0,0.0 1.0 0.0 0.0,0.0 1.0 0.0 0.0,0.0 1.0 0.0 0.0,0.0 1.0 0.0 0.0,0.0 1.0 0.0 0.0,0.0 1.0 0.0 0.0,0.0 1.0 0.0 0.0,0.0 1.0 0.0 0.0,0.0 1.0 0.0 0.0,0.0 1.0 0.0 0.0,0.0 1.0 0.0 0.0,0.0 1.0 0.0 0.0,0.0 1.0 0.0 0.0,0.0 1.0 0.0 0.0,0.0 1.0 0.0 0.0,0.0 1.0 0.0 0.0,0.0 1.0 0.0 0.0,0.0 1.0 0.0 0.0,0.0 1.0 0.0 0.0,0.0 1.0 0.0 0.0,0.0 1.0 0.0 0.0,0.0 1.0 0.0 0.0,0.0 1.0 0.0 0.0,0.0 1.0 0.0 0.0,0.0 1.0 0.0 0.0,0.0 1.0 0.0 0.0,0.0 1.0 0.0 0.0,0.0 1.0 0.0 0.0,0.0 1.0 0.0 0.0,0.0 1.0 0.0 0.0,0.0 1.0 0.0 0.0,0.0 1.0 0.0 0.0,0.0 1.0 0.0 0.0,0.0 1.0 0.0 0.0,0.0 1.0 0.0 0.0,0.0 1.0 0.0 0.0,0.0 1.0 0.0 0.0,0.0 1.0 0.0 0.0,0.0 1.0 0.0 0.0,0.0 1.0 0.0 0.0,0.0 1.0 0.0 0.0'/>
+    <ROUTE fromField='fraction_changed' fromNode='testing123Clock' toField='set_fraction' toNode='testing123Positions'/>
+    <ROUTE fromField='fraction_changed' fromNode='testing123Clock' toField='set_fraction' toNode='testing123Orientations'/>
+    <LineSet vertexCount='42'>
+      <Coordinate point='0.0 0.0 0.0,0.0 1.0 0.0,0.0 2.0 0.0,0.0 3.0 0.0,0.0 4.0 0.0,0.0 5.0 0.0,0.0 6.0 0.0,0.0 7.0 0.0,0.0 8.0 0.0,0.0 9.0 0.0,0.0 10.0 0.0,1.0 10.0 0.0,2.0 10.0 0.0,3.0 10.0 0.0,4.0 10.0 0.0,5.0 10.0 0.0,6.0 10.0 0.0,7.0 10.0 0.0,8.0 10.0 0.0,9.0 10.0 0.0,10.0 10.0 0.0,10.0 9.0 0.0,10.0 8.0 0.0,10.0 7.0 0.0,10.0 6.0 0.0,10.0 5.0 0.0,10.0 4.0 0.0,10.0 3.0 0.0,10.0 2.0 0.0,10.0 1.0 0.0,10.0 0.0 0.0,9.0 0.0 0.0,8.0 0.0 0.0,7.0 0.0 0.0,6.0 0.0 0.0,5.0 0.0 0.0,4.0 0.0 0.0,3.0 0.0 0.0,2.0 0.0 0.0,1.0 0.0 0.0,0.0 0.0 0.0,-1.0 0.0 0.0'/>
+    </LineSet>
+  </Scene>
+</X3D>
+
+=================================
+*** setKillSentinelAndInterrupts() killed=true sendingThread.isInterrupted()=false receiveThread.isInterrupted()=true
 [DisThreadedNetworkInterface PduRecorder] close(): pdus2send.size()=0 baos.size()=0 dos.size()=0
 *** killThread() status: sendingThread.isAlive()=false sendingThread.isInterrupted()=true
 *** killThread() status: receiveThread.isAlive()=false receiveThread.isInterrupted()=true
 *** Thread close status: sendingThread.isAlive()=false receiveThread.isAlive()=false
 
-PduRecorder.stop() closing recorder log file: C:\x-nps-gitlab\NetworkedGraphicsMV3500\examples\pduLog\PduCaptureLog165.dislog
+PduRecorder.stop() closing recorder log file: C:\x-nps-gitlab\NetworkedGraphicsMV3500\examples\pduLog\PduCaptureLog171.dislog
 [OpenDis7Examples.ExampleTrackInterpolation] complete.
 BUILD SUCCESSFUL (total time: 12 seconds)
diff --git a/examples/src/OpenDis7Examples/PduTrack.java b/examples/src/OpenDis7Examples/PduTrack.java
index e089498822..ce2c54d83e 100644
--- a/examples/src/OpenDis7Examples/PduTrack.java
+++ b/examples/src/OpenDis7Examples/PduTrack.java
@@ -65,6 +65,7 @@ public class PduTrack
     private String                   x3dTimeSensorDEF = new String();
     private String         x3dPositionInterpolatorDEF = new String();
     private String      x3dOrientationInterpolatorDEF = new String();
+    private boolean      addLineBreaksWithinKeyValues = false;
     
     private String TRACE_PREFIX = "[" + (PduTrack.class.getSimpleName()) + "] ";
     
@@ -192,6 +193,13 @@ public class PduTrack
          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
@@ -199,7 +207,7 @@ public class PduTrack
      */
     public String getX3dTimeSensorDEF() {
         if    (x3dTimeSensorDEF.isEmpty())
-               x3dTimeSensorDEF = getDescriptor().replace(" ", "") + "Clock";
+               x3dTimeSensorDEF = normalizeNameToken(getDescriptor()) + "Clock";
         return x3dTimeSensorDEF;
     }
 
@@ -208,7 +216,7 @@ public class PduTrack
      * @param x3dTimeSensorDEF the x3dTimeSensorDEF to set
      */
     public void setX3dTimeSensorDEF(String x3dTimeSensorDEF) {
-        this.x3dTimeSensorDEF = x3dTimeSensorDEF;
+        this.x3dTimeSensorDEF = normalizeNameToken(x3dTimeSensorDEF);
     }
 
     /**
@@ -217,7 +225,7 @@ public class PduTrack
      */
     public String getX3dPositionInterpolatorDEF() {
         if    (x3dPositionInterpolatorDEF.isEmpty())
-               x3dPositionInterpolatorDEF = getDescriptor().replace(" ", "") + "Positions";
+               x3dPositionInterpolatorDEF = normalizeNameToken(getDescriptor()) + "Positions";
         return x3dPositionInterpolatorDEF;
     }
 
@@ -226,7 +234,7 @@ public class PduTrack
      * @param x3dPositionInterpolatorDEF the x3dPositionInterpolatorDEF to set
      */
     public void setX3dPositionInterpolatorDEF(String x3dPositionInterpolatorDEF) {
-        this.x3dPositionInterpolatorDEF = x3dPositionInterpolatorDEF;
+        this.x3dPositionInterpolatorDEF = normalizeNameToken(x3dPositionInterpolatorDEF);
     }
 
     /**
@@ -235,7 +243,7 @@ public class PduTrack
      */
     public String getX3dOrientationInterpolatorDEF() {
         if    (x3dOrientationInterpolatorDEF.isEmpty())
-               x3dOrientationInterpolatorDEF = getDescriptor().replace(" ", "") + "Orientations";
+               x3dOrientationInterpolatorDEF = normalizeNameToken(getDescriptor()) + "Orientations";
         return x3dOrientationInterpolatorDEF;
     }
 
@@ -244,7 +252,7 @@ public class PduTrack
      * @param x3dOrientationInterpolatorDEF the x3dOrientationInterpolatorDEF to set
      */
     public void setX3dOrientationInterpolatorDEF(String x3dOrientationInterpolatorDEF) {
-        this.x3dOrientationInterpolatorDEF = x3dOrientationInterpolatorDEF;
+        this.x3dOrientationInterpolatorDEF = normalizeNameToken(x3dOrientationInterpolatorDEF);
     }
 
     /**
@@ -365,22 +373,22 @@ public class PduTrack
     public String createX3dTimeSensorString()
     {
         StringBuilder sb = new StringBuilder();
-        sb.append("<TimeSensor");
+        sb.append("    <TimeSensor");
         sb.append(" DEF='").append(getX3dTimeSensorDEF()).append("'");
         sb.append(" cycleInterval='").append(String.valueOf(getTotalDurationSeconds())).append("'");
         sb.append(" loop='true'");
-        sb.append("/>");
+        sb.append("/>").append("\n");
         
         return sb.toString();
     }
     /**
-     * Create TimeSensor from Pdu list
-     * @return X3D TimeSensor as string
+     * Create PositionInterpolator from Pdu list
+     * @return X3D PositionInterpolator as string
      */
     public String createX3dPositionInterpolatorString()
     {
         StringBuilder sb = new StringBuilder();
-        sb.append("<PositionInterpolator");
+        sb.append("    <PositionInterpolator");
         sb.append(" DEF='").append(getX3dPositionInterpolatorDEF()).append("'");
         sb.append(" key='");
         for (int i = 0; i < timelineList.size(); i++)
@@ -394,6 +402,8 @@ public class PduTrack
         sb.append(" keyValue='");
         for (int i = 0; i < waypointsList.size(); i++)
         {
+            if (addLineBreaksWithinKeyValues)
+                sb.append("\n");
             Vector3Double nextPosition = waypointsList.get(i);
             sb.append(String.valueOf(nextPosition.getX())).append(" ")
               .append(String.valueOf(nextPosition.getY())).append(" ")
@@ -402,7 +412,137 @@ public class PduTrack
                 sb.append(",");
         }
         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 (addLineBreaksWithinKeyValues)
+                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();
+    }
+    /**
+     * Create full X3D interpolator model from Pdu list, assembling sections of scene graph
+     * @param addLineBreaksWithinKeyValues whether to insert line breaks in output arrays
+     * @return X3D model as string
+     */
+    public String createX3dModel(boolean addLineBreaksWithinKeyValues)
+    {
+        this.addLineBreaksWithinKeyValues = addLineBreaksWithinKeyValues;
+        return createX3dModel();
+    }
+    /**
+     * 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(createX3dHeaderString());
+        sb.append(createX3dTimeSensorString());
+        sb.append(createX3dPositionInterpolatorString());
+        sb.append(createX3dOrientationInterpolatorString());
+        sb.append(createX3dRoutesFooterString());
+        return sb.toString();
+    }
+    /**
+     * Create PositionInterpolator from Pdu list
+     * @return X3D PositionInterpolator as string
+     */
+    public String createX3dHeaderString()
+    {
+        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='Immersive' 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");
+        sb.append("    <meta content='PduTrackInterpolator.x3d' name='title'/>").append("\n");
+        sb.append("    <meta content='PduTrack utility open-dis7-java Library https://github.com/open-dis/open-dis7-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='Open source https://raw.githubusercontent.com/open-dis/open-dis7-java/master/license.html' name='license'/>").append("\n");
+        sb.append("  </head>").append("\n");
+        sb.append("  <Scene>").append("\n");
+        
+        return sb.toString();
+    }
+    /**
+     * Create X3D ROUTEs and footer to connect TimeSensor to interpolators
+     * @return X3D PositionInterpolator as string
+     */
+    public String createX3dRoutesFooterString()
+    {
+        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("    <LineSet vertexCount='").append(waypointsList.size()).append("'>").append("\n");
+        sb.append("      <Coordinate point='");
+        for (int i = 0; i < waypointsList.size(); i++)
+        {
+            if (addLineBreaksWithinKeyValues)
+                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("  </Scene>").append("\n");
+        sb.append("</X3D>").append("\n");
         
         return sb.toString();
     }
@@ -448,7 +588,7 @@ public class PduTrack
         System.out.println(TRACE_PREFIX + "selfTest() start...");
       
         PduTrack pduTrack = new PduTrack();
-        pduTrack.setDescriptor("PduTrack selfTest()");
+        pduTrack.setDescriptor("PduTrack Self Test");
         pduTrack.setDefaultWaypointInterval(1.0f);
         
         EntityStatePdu espdu = new EntityStatePdu();
@@ -456,13 +596,18 @@ public class PduTrack
         for (int i = 0; i < 5; i++)
         {
             espdu.setEntityLocation(i, i, i);
+            espdu.setEntityOrientation(0, (float)(45.0 * Math.PI / 180.0), 0);
             pduTrack.addPdu(espdu);
         }
+        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.createX3dModel(true)); // addLineBreaksWithinKeyValues
+        System.out.println("=================================");
+
         System.out.println(TRACE_PREFIX + "selfTest() complete.");
     }
     
diff --git a/examples/src/OpenDis7Examples/PduTrackLog.txt b/examples/src/OpenDis7Examples/PduTrackLog.txt
new file mode 100644
index 0000000000..f4fa5841b5
--- /dev/null
+++ b/examples/src/OpenDis7Examples/PduTrackLog.txt
@@ -0,0 +1,46 @@
+ant -f C:\\x-nps-gitlab\\NetworkedGraphicsMV3500\\examples -Dnb.internal.action.name=run.single -Djavac.includes=OpenDis7Examples/PduTrack.java -Drun.class=OpenDis7Examples.PduTrack run-single
+init:
+Deleting: C:\x-nps-gitlab\NetworkedGraphicsMV3500\examples\build\built-jar.properties
+deps-jar:
+Updating property file: C:\x-nps-gitlab\NetworkedGraphicsMV3500\examples\build\built-jar.properties
+Compiling 1 source file to C:\x-nps-gitlab\NetworkedGraphicsMV3500\examples\build\classes
+compile-single:
+run-single:
+*** PduTrack main() self test started...
+[PduTrack main() self test] selfTest() start...
+[PduTrack main() self test] getEspduCount()=5
+[PduTrack main() self test] getDefaultWaypointInterval()=1.0
+[PduTrack main() self test] getTotalDurationSeconds()=5.0
+=================================
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE X3D PUBLIC "ISO//Web3D//DTD X3D 4.0//EN" "https://www.web3d.org/specifications/x3d-4.0.dtd">
+<X3D profile='Immersive' version='4.0' xmlns:xsd='http://www.w3.org/2001/XMLSchema-instance' xsd:noNamespaceSchemaLocation='https://www.web3d.org/specifications/x3d-4.0.xsd'>
+  <head>
+    <meta content='PduTrackInterpolator.x3d' name='title'/>
+    <meta content='PduTrack utility open-dis7-java Library https://github.com/open-dis/open-dis7-java' name='generator'/>
+    <meta content='NPS MOVES MV3500 Networked Graphics https://gitlab.nps.edu/Savage/NetworkedGraphicsMV3500' name='reference'/>
+    <meta content='Open source https://raw.githubusercontent.com/open-dis/open-dis7-java/master/license.html' name='license'/>
+  </head>
+  <Scene>
+    <TimeSensor DEF='PduTrackSelfTestClock' cycleInterval='5.0' loop='true'/>
+    <PositionInterpolator DEF='PduTrackSelfTestPositions' key='0.0 1.0 2.0 3.0 4.0' keyValue='
+0.0 0.0 0.0,
+1.0 1.0 1.0,
+2.0 2.0 2.0,
+3.0 3.0 3.0,
+4.0 4.0 4.0'/>
+    <OrientationInterpolator DEF='PduTrackSelfTestOrientations' key='0.0 1.0 2.0 3.0 4.0' keyValue='
+0.0 1.0 0.0 0.0,
+0.0 1.0 0.0 0.0,
+0.0 1.0 0.0 0.0,
+0.0 1.0 0.0 0.0,
+0.0 1.0 0.0 0.0'/>
+    <ROUTE fromField='fraction_changed' fromNode='PduTrackSelfTestClock' toField='set_fraction' toNode='PduTrackSelfTestPositions'/>
+    <ROUTE fromField='fraction_changed' fromNode='PduTrackSelfTestClock' toField='set_fraction' toNode='PduTrackSelfTestOrientations'/>
+  </Scene>
+</X3D>
+
+=================================
+[PduTrack main() self test] selfTest() complete.
+*** PduTrack main() self test complete.
+BUILD SUCCESSFUL (total time: 1 second)
-- 
GitLab