{ "X3D": {
    "encoding":"UTF-8",
    "@profile":"Immersive",
    "@version":"3.2",
    "@xsd:noNamespaceSchemaLocation":"https://www.web3d.org/specifications/x3d-3.2.xsd",
    "JSON schema":"https://www.web3d.org/specifications/x3d-4.0-JSONSchema.autogenerated.json",
    "head": {
        "meta": [
          {
            "@name":"title",
            "@content":"WaypointInterpolatorPrototype.x3d"
          },
          {
            "@name":"description",
            "@content":"Prototype to provide a set of waypoints, plus either leg durations or speed, and return position/orientation interpolation values. Included example can be stopped/started via TouchSensor mouse over floor Box."
          },
          {
            "@name":"creator",
            "@content":"Don Brutzman, Curtis Blais, Jeff Weekley, Jane Wu"
          },
          {
            "@name":"created",
            "@content":"6 April 2001"
          },
          {
            "@name":"modified",
            "@content":"23 August 2023"
          },
          {
            "@name":"identifier",
            "@content":"https://www.web3d.org/x3d/content/examples/Savage/Tools/Animation/WaypointInterpolatorPrototype.x3d"
          },
          {
            "@name":"reference",
            "@content":"https://www.web3d.org/x3d/content/examples/Savage/Tools/Animation/WaypointInterpolatorExample.x3d"
          },
          {
            "@name":"warning",
            "@content":"browsers do not compute pitch angle consistently"
          },
          {
            "@name":"generator",
            "@content":"X3D-Edit 4.0, https://www.web3d.org/x3d/tools/X3D-Edit"
          },
          {
            "@name":"license",
            "@content":"../../license.html"
          },
          {
            "@name":"translated",
            "@content":"26 March 2026"
          },
          {
            "@name":"generator",
            "@content":"X3dToJson.xslt, https://www.web3d.org/x3d/stylesheets/X3dToJson.html"
          },
          {
            "@name":"reference",
            "@content":"X3D JSON encoding: https://www.web3d.org/wiki/index.php/X3D_JSON_Encoding"
          }
        ]
    },
    "Scene": {
        "-children":[
          { "WorldInfo":
            {
              "@title":"WaypointInterpolatorPrototype.x3d"
            }
          },
          { "ProtoDeclare":
            {
              "@name":"WaypointInterpolator",
              "@appinfo":"Reads waypoints and legSpeeds/legDurations/defaultSpeed to provide a customizable position/orientation interpolator.",
              "ProtoInterface": {
                  "field": [
                    {
                      "@name":"description",
                      "@accessType":"initializeOnly",
                      "@appinfo":"Short description of what is animated by this WaypointInterpolator.",
                      "@type":"SFString"
                    },
                    {
                      "@name":"waypoints",
                      "@accessType":"initializeOnly",
                      "@appinfo":"Waypoints being traversed with interpolation of intermediate positions and orientations.",
                      "@type":"MFVec3f",
                      "@value":[0,0,0,0,0,0]
                    },
                    {
                      "@name":"add_waypoint",
                      "@accessType":"inputOnly",
                      "@appinfo":"Add another single waypoint to array of waypoints recalculate interpolator values.",
                      "@type":"SFVec3f"
                    },
                    {
                      "@name":"set_waypoints",
                      "@accessType":"inputOnly",
                      "@appinfo":"Replace all waypoints recalculate interpolator values.",
                      "@type":"MFVec3f"
                    },
                    {
                      "@name":"pitchUpDownForVerticalWaypoints",
                      "@accessType":"initializeOnly",
                      "@appinfo":"Whether to pitch child geometry (such as a vehicle) up or down to match vertical slope",
                      "@type":"SFBool",
                      "@value":false
                    },
                    {
                      "@name":"legSpeeds",
                      "@accessType":"initializeOnly",
                      "@appinfo":"Units m/sec. If used, array lengths for legSpeeds and legDurations must be one less than number of waypoints.",
                      "@type":"MFFloat",
                      "-children":[
                        {
                          "#comment":"default initialization is empty array []"
                        }
                      ]
                    },
                    {
                      "@name":"legDurations",
                      "@accessType":"initializeOnly",
                      "@appinfo":"Units in seconds. If used, array lengths for legSpeeds and legDurations must be one less than number of waypoints.",
                      "@type":"MFTime",
                      "-children":[
                        {
                          "#comment":"default initialization is empty array []"
                        }
                      ]
                    },
                    {
                      "@name":"defaultSpeed",
                      "@accessType":"initializeOnly",
                      "@appinfo":"Units m/sec.",
                      "@type":"SFFloat",
                      "@value":1
                    },
                    {
                      "@name":"turningRate",
                      "@accessType":"initializeOnly",
                      "@appinfo":"turningRate (degrees/second) also determines standoff distance prior to waypoint where turn commences. If 0 turns are instantaneous.",
                      "@type":"SFFloat",
                      "@value":90
                    },
                    {
                      "@name":"totalDuration",
                      "@accessType":"outputOnly",
                      "@appinfo":"Output calculation summing all leg durations, useful for setting TimeSensor cycleInterval. Units in seconds.",
                      "@type":"SFTime"
                    },
                    {
                      "@name":"set_fraction",
                      "@accessType":"inputOnly",
                      "@appinfo":"exposed PositionInterpolator and OrientationInterpolator setting",
                      "@type":"SFFloat"
                    },
                    {
                      "@name":"position_changed",
                      "@accessType":"outputOnly",
                      "@appinfo":"exposed PositionInterpolator setting",
                      "@type":"SFVec3f"
                    },
                    {
                      "@name":"orientation_changed",
                      "@accessType":"outputOnly",
                      "@appinfo":"exposed OrientationInterpolator setting",
                      "@type":"SFRotation"
                    },
                    {
                      "@name":"lineColor",
                      "@accessType":"inputOutput",
                      "@appinfo":"default color for non-active line segments",
                      "@type":"SFColor",
                      "@value":[0.6,0.6,0.6]
                    },
                    {
                      "@name":"highlightSegmentColor",
                      "@accessType":"inputOutput",
                      "@appinfo":"active segment highlight color",
                      "@type":"SFColor",
                      "@value":[0.3,0.3,1]
                    },
                    {
                      "@name":"transparency",
                      "@accessType":"inputOutput",
                      "@appinfo":"1.0 is completely transparent, 0.0 is completely opaque.",
                      "@type":"SFFloat",
                      "@value":0
                    },
                    {
                      "@name":"labelDisplayMode",
                      "@accessType":"initializeOnly",
                      "@appinfo":"allowed values: none; waypoints (produce labels at each waypoint); or interpolation (produce single moving label at interpolator time course speed location)",
                      "@type":"SFString",
                      "@value":"waypoints"
                    },
                    {
                      "@name":"heightLabel",
                      "@accessType":"initializeOnly",
                      "@appinfo":"allowed values: altitude depth (negate Y value) none",
                      "@type":"SFString",
                      "@value":"altitude"
                    },
                    {
                      "@name":"labelOffset",
                      "@accessType":"initializeOnly",
                      "@appinfo":"heightLabel relative location",
                      "@type":"SFVec3f",
                      "@value":[0,-1,0]
                    },
                    {
                      "@name":"labelFontSize",
                      "@accessType":"initializeOnly",
                      "@appinfo":"heightLabel text size",
                      "@type":"SFFloat",
                      "@value":1
                    },
                    {
                      "@name":"labelColor",
                      "@accessType":"initializeOnly",
                      "@appinfo":"heightLabel text color",
                      "@type":"SFColor",
                      "@value":[0.8,0.8,0.8]
                    },
                    {
                      "@name":"traceEnabled",
                      "@accessType":"initializeOnly",
                      "@appinfo":"enable console output to trace script computations and prototype progress",
                      "@type":"SFBool",
                      "@value":false
                    },
                    {
                      "@name":"outputInitializationComputations",
                      "@accessType":"initializeOnly",
                      "@appinfo":"Output the number of waypoints totalDistance and totalDuration to console upon initialization",
                      "@type":"SFBool",
                      "@value":true
                    },
                    {
                      "@name":"verticalDropLineColor",
                      "@accessType":"inputOutput",
                      "@appinfo":"default color for vertical drop-line segments",
                      "@type":"SFColor",
                      "@value":[0.4,0.4,0.4]
                    },
                    {
                      "@name":"verticalDropLineTransparency",
                      "@accessType":"inputOutput",
                      "@appinfo":"1.0 is completely transparent, 0.0 is completely opaque.",
                      "@type":"SFFloat",
                      "@value":1
                    }
                  ],
                  "-children":[
                    {
                      "#comment":"Priority of use: legSpeeds (m/sec), legDurations (seconds), defaultSpeed (m/sec)"
                    },
                    {
                      "#comment":"interpolation fields"
                    },
                    {
                      "#comment":"display-related fields"
                    }
                  ]
              },
              "ProtoBody": {
                  "-children":[
                    {
                      "#comment":"First node in prototype determines node type of prototype. This prototype extends PositionInterpolator and OrientationInterpolator functionality. Nevertheless, a Group node is wrapped around all of them in order to avoid a Prototype bug in CosmoPlayer."
                    },
                    { "Group":
                      {
                        "-children":[
                          {
                            "#comment":"key, keyValue will be generated by WaypointTrackScript. set_fraction is a common input to both interpolators. Interpolator value outputs are returned via the corresponding Prototype field interfaces."
                          },
                          { "PositionInterpolator":
                            {
                              "@DEF":"WaypointPI.instance",
                              "@key":[0,0.5,1],
                              "@keyValue":[0,0,0,1,1,1,2,2,2],
                              "IS": {
                                  "connect": [
                                    {
                                      "@nodeField":"set_fraction",
                                      "@protoField":"set_fraction"
                                    },
                                    {
                                      "@nodeField":"value_changed",
                                      "@protoField":"position_changed"
                                    }
                                  ]
                              }
                            }
                          },
                          { "OrientationInterpolator":
                            {
                              "@DEF":"WaypointOI.instance",
                              "IS": {
                                  "connect": [
                                    {
                                      "@nodeField":"set_fraction",
                                      "@protoField":"set_fraction"
                                    },
                                    {
                                      "@nodeField":"value_changed",
                                      "@protoField":"orientation_changed"
                                    }
                                  ]
                              }
                            }
                          },
                          { "Group":
                            {
                              "@DEF":"CoordinateLabelsAndViewpointsGroup"
                            }
                          },
                          { "Script":
                            {
                              "@DEF":"WaypointTrackScript",
                              "@directOutput":true,
                              "field": [
                                {
                                  "@name":"description",
                                  "@accessType":"initializeOnly",
                                  "@type":"SFString"
                                },
                                {
                                  "@name":"waypoints",
                                  "@accessType":"initializeOnly",
                                  "@type":"MFVec3f"
                                },
                                {
                                  "@name":"add_waypoint",
                                  "@accessType":"inputOnly",
                                  "@type":"SFVec3f"
                                },
                                {
                                  "@name":"set_waypoints",
                                  "@accessType":"inputOnly",
                                  "@type":"MFVec3f"
                                },
                                {
                                  "@name":"pitchUpDownForVerticalWaypoints",
                                  "@accessType":"initializeOnly",
                                  "@type":"SFBool"
                                },
                                {
                                  "@name":"legSpeeds",
                                  "@accessType":"initializeOnly",
                                  "@type":"MFFloat"
                                },
                                {
                                  "@name":"legDurations",
                                  "@accessType":"initializeOnly",
                                  "@type":"MFTime"
                                },
                                {
                                  "@name":"defaultSpeed",
                                  "@accessType":"initializeOnly",
                                  "@type":"SFFloat"
                                },
                                {
                                  "@name":"turningRate",
                                  "@accessType":"initializeOnly",
                                  "@type":"SFFloat"
                                },
                                {
                                  "@name":"totalDuration",
                                  "@accessType":"outputOnly",
                                  "@type":"SFTime"
                                },
                                {
                                  "@name":"WaypointPI",
                                  "@accessType":"initializeOnly",
                                  "@type":"SFNode",
                                  "-children":[
                                    { "PositionInterpolator":
                                      {
                                        "@USE":"WaypointPI.instance"
                                      }
                                    }
                                  ]
                                },
                                {
                                  "@name":"WaypointOI",
                                  "@accessType":"initializeOnly",
                                  "@type":"SFNode",
                                  "-children":[
                                    { "OrientationInterpolator":
                                      {
                                        "@USE":"WaypointOI.instance"
                                      }
                                    }
                                  ]
                                },
                                {
                                  "@name":"pointIndices",
                                  "@accessType":"outputOnly",
                                  "@type":"MFInt32"
                                },
                                {
                                  "@name":"OutputLabelsGroup",
                                  "@accessType":"initializeOnly",
                                  "@type":"SFNode",
                                  "-children":[
                                    { "Group":
                                      {
                                        "@USE":"CoordinateLabelsAndViewpointsGroup"
                                      }
                                    }
                                  ]
                                },
                                {
                                  "@name":"set_fraction",
                                  "@accessType":"inputOnly",
                                  "@type":"SFFloat"
                                },
                                {
                                  "@name":"highlightCoordinates",
                                  "@accessType":"outputOnly",
                                  "@appinfo":"Initialized to (0 0 0 0 0 0)",
                                  "@type":"MFVec3f"
                                },
                                {
                                  "@name":"heightLabel",
                                  "@accessType":"initializeOnly",
                                  "@type":"SFString"
                                },
                                {
                                  "@name":"labelDisplayMode",
                                  "@accessType":"initializeOnly",
                                  "@type":"SFString"
                                },
                                {
                                  "@name":"labelOffset",
                                  "@accessType":"initializeOnly",
                                  "@type":"SFVec3f"
                                },
                                {
                                  "@name":"labelFontSize",
                                  "@accessType":"initializeOnly",
                                  "@type":"SFFloat"
                                },
                                {
                                  "@name":"labelColor",
                                  "@accessType":"initializeOnly",
                                  "@type":"SFColor"
                                },
                                {
                                  "@name":"labelInterpolation",
                                  "@accessType":"outputOnly",
                                  "@type":"MFString"
                                },
                                {
                                  "@name":"traceEnabled",
                                  "@accessType":"initializeOnly",
                                  "@type":"SFBool"
                                },
                                {
                                  "@name":"outputInitializationComputations",
                                  "@accessType":"initializeOnly",
                                  "@appinfo":"Output the number of waypoints totalDistance and totalDuration to console upon initialization",
                                  "@type":"SFBool"
                                },
                                {
                                  "@name":"scriptError",
                                  "@accessType":"initializeOnly",
                                  "@appinfo":"whether or not an error was detected during script processing.",
                                  "@type":"SFBool",
                                  "@value":false
                                },
                                {
                                  "@name":"previousFractionIndex",
                                  "@accessType":"initializeOnly",
                                  "@appinfo":"retain state information while constructing fraction array",
                                  "@type":"SFInt32",
                                  "@value":0
                                },
                                {
                                  "@name":"depthString",
                                  "@accessType":"initializeOnly",
                                  "@appinfo":"label",
                                  "@type":"SFString"
                                },
                                {
                                  "@name":"whichRotationVersion",
                                  "@accessType":"initializeOnly",
                                  "@appinfo":"label",
                                  "@type":"SFString"
                                },
                                {
                                  "@name":"verticalDropLineIndices",
                                  "@accessType":"outputOnly",
                                  "@type":"MFInt32"
                                },
                                {
                                  "@name":"verticalDropLinePoints",
                                  "@accessType":"outputOnly",
                                  "@type":"MFVec3f"
                                },
                                {
                                  "@name":"positionKey",
                                  "@accessType":"initializeOnly",
                                  "@type":"MFFloat",
                                  "@value":[0]
                                },
                                {
                                  "@name":"positionKeyValueArray",
                                  "@accessType":"initializeOnly",
                                  "@type":"MFVec3f"
                                },
                                {
                                  "@name":"finalPositionKey",
                                  "@accessType":"outputOnly",
                                  "@type":"MFFloat"
                                },
                                {
                                  "@name":"finalPositionKeyValueArray",
                                  "@accessType":"outputOnly",
                                  "@type":"MFVec3f"
                                },
                                {
                                  "@name":"distances",
                                  "@accessType":"initializeOnly",
                                  "@type":"MFFloat"
                                },
                                {
                                  "@name":"pointIndicesAccumulator",
                                  "@accessType":"initializeOnly",
                                  "@type":"MFInt32"
                                },
                                {
                                  "@name":"verticalDropLineIndicesAccumulator",
                                  "@accessType":"initializeOnly",
                                  "@type":"MFInt32"
                                },
                                {
                                  "@name":"verticalDropLinePointsAccumulator",
                                  "@accessType":"initializeOnly",
                                  "@type":"MFVec3f"
                                },
                                {
                                  "@name":"totalDistance",
                                  "@accessType":"initializeOnly",
                                  "@type":"SFFloat",
                                  "@value":0
                                },
                                {
                                  "@name":"orientations",
                                  "@accessType":"initializeOnly",
                                  "@type":"MFRotation"
                                },
                                {
                                  "@name":"dx",
                                  "@accessType":"initializeOnly",
                                  "@type":"SFFloat",
                                  "@value":0
                                },
                                {
                                  "@name":"dy",
                                  "@accessType":"initializeOnly",
                                  "@type":"SFFloat",
                                  "@value":0
                                },
                                {
                                  "@name":"dz",
                                  "@accessType":"initializeOnly",
                                  "@type":"SFFloat",
                                  "@value":0
                                },
                                {
                                  "@name":"legDistance",
                                  "@accessType":"initializeOnly",
                                  "@type":"SFFloat",
                                  "@value":0
                                },
                                {
                                  "@name":"heading",
                                  "@accessType":"initializeOnly",
                                  "@type":"SFFloat",
                                  "@value":0
                                },
                                {
                                  "@name":"pitchAngle",
                                  "@accessType":"initializeOnly",
                                  "@type":"SFFloat",
                                  "@value":0
                                },
                                {
                                  "@name":"orientationKey",
                                  "@accessType":"initializeOnly",
                                  "@type":"MFFloat"
                                },
                                {
                                  "@name":"newKey",
                                  "@accessType":"initializeOnly",
                                  "@type":"MFFloat"
                                },
                                {
                                  "@name":"newKeyValue",
                                  "@accessType":"initializeOnly",
                                  "@type":"MFRotation"
                                },
                                {
                                  "@name":"outputChild",
                                  "@accessType":"initializeOnly",
                                  "@type":"MFNode",
                                  "-children":[
                                    {
                                      "#comment":"NULL"
                                    }
                                  ]
                                },
                                {
                                  "@name":"rotatedVector",
                                  "@accessType":"initializeOnly",
                                  "@type":"SFVec3f",
                                  "@value":[0,0,0]
                                }
                              ],
                              "-children":[
                                {
                                  "#comment":"local variables (do not use internal var declarations) for persistence"
                                }
                              ],
                              "IS": {
                                  "connect": [
                                    {
                                      "@nodeField":"description",
                                      "@protoField":"description"
                                    },
                                    {
                                      "@nodeField":"waypoints",
                                      "@protoField":"waypoints"
                                    },
                                    {
                                      "@nodeField":"add_waypoint",
                                      "@protoField":"add_waypoint"
                                    },
                                    {
                                      "@nodeField":"set_waypoints",
                                      "@protoField":"set_waypoints"
                                    },
                                    {
                                      "@nodeField":"pitchUpDownForVerticalWaypoints",
                                      "@protoField":"pitchUpDownForVerticalWaypoints"
                                    },
                                    {
                                      "@nodeField":"legSpeeds",
                                      "@protoField":"legSpeeds"
                                    },
                                    {
                                      "@nodeField":"legDurations",
                                      "@protoField":"legDurations"
                                    },
                                    {
                                      "@nodeField":"defaultSpeed",
                                      "@protoField":"defaultSpeed"
                                    },
                                    {
                                      "@nodeField":"turningRate",
                                      "@protoField":"turningRate"
                                    },
                                    {
                                      "@nodeField":"totalDuration",
                                      "@protoField":"totalDuration"
                                    },
                                    {
                                      "@nodeField":"set_fraction",
                                      "@protoField":"set_fraction"
                                    },
                                    {
                                      "@nodeField":"heightLabel",
                                      "@protoField":"heightLabel"
                                    },
                                    {
                                      "@nodeField":"labelDisplayMode",
                                      "@protoField":"labelDisplayMode"
                                    },
                                    {
                                      "@nodeField":"labelOffset",
                                      "@protoField":"labelOffset"
                                    },
                                    {
                                      "@nodeField":"labelFontSize",
                                      "@protoField":"labelFontSize"
                                    },
                                    {
                                      "@nodeField":"labelColor",
                                      "@protoField":"labelColor"
                                    },
                                    {
                                      "@nodeField":"traceEnabled",
                                      "@protoField":"traceEnabled"
                                    },
                                    {
                                      "@nodeField":"outputInitializationComputations",
                                      "@protoField":"outputInitializationComputations"
                                    }
                                  ]
                              },
                              "#sourceCode":[
"",
"",
"ecmascript:",
"",
"function tracePrint (outputValue)",
"{",
"\tif (traceEnabled) forcePrint (outputValue);",
"}",
"function forcePrint (outputValue)",
"{",
"\t// try to ensure outputValue is converted to string despite browser idiosyncracies",
"    outputString = outputValue.toString(); // utility function according to spec",
"    if (outputString == null) outputString = outputValue; // direct cast",
"",
"    Browser.println ('[WaypointInterpolator ' + description + '] ' + outputString);",
"}",
"",
"function distance (p1, p2)",
"{",
"\treturn Math.sqrt (",
"\t\t(p2.x - p1.x) * (p2.x - p1.x) +",
"\t\t(p2.y - p1.y) * (p2.y - p1.y) +",
"\t\t(p2.z - p1.z) * (p2.z - p1.z));",
"}",
"",
"function normalize2Pi (angle)",
"{",
"\ttwoPi = 2 * Math.PI;",
"\tx = angle;",
"\twhile (x >= twoPi) x = x - twoPi;",
"\twhile (x <  0)     x = x + twoPi;",
"\treturn x;",
"}",
"",
"function normalizePi (angle)",
"{",
"\ttwoPi = 2 * Math.PI;",
"\tx = angle;",
"\twhile (x >=  Math.PI) x = x - twoPi;",
"\twhile (x <  -Math.PI) x = x + twoPi;",
"\treturn x;",
"}",
"",
"function degrees (angleRadians)",
"{",
"\treturn angleRadians * 180.0 / Math.PI;",
"}",
"",
"function radians (thetaDegrees)",
"{",
"\treturn thetaDegrees * Math.PI / 180.0;",
"}",
"",
"function initialize ()",
"{",
"\tsaveTrace    = traceEnabled;",
"  traceEnabled = true;                     // debug use",
"  outputInitializationComputations = true; // debug use",
"",
"\tscriptError = false;",
"\ttraceEnabled= false; // set traceEnabled=true for selective debug during initialization only",
"",
"\tforcePrint ('initializing new ' + waypoints.length + '-point WaypointInterpolator ' + description);",
"\ttracePrint ('Browser.name       =' + Browser.name);",
"\ttracePrint ('WaypointPI.key     =' + WaypointPI.key.toString());",
"\ttracePrint ('WaypointPI.keyValue=' + WaypointPI.keyValue.toString());",
"",
"// TODO forcePrint ('Returning, initialization trace complete.');",
"// TODO return;",
"",
"\tpreviousFractionIndex = -1;",
"\ttracePrint ('waypoints       =' + waypoints.toString());",
"\tif ((waypoints.length == 2) &&",
"\t    (waypoints[0].x == 0) && (waypoints[0].y == 0) && (waypoints[0].z == 0) &&",
"\t    (waypoints[1].x == 0) && (waypoints[1].y == 0) && (waypoints[1].z == 0))",
"\t{",
"\t\ttracePrint ('[default waypoints, no action needed]');",
"\t\treturn;",
"\t}",
"\tif (waypoints.length < 2)",
"\t{",
"\t\tforcePrint ('*** error: insufficient waypoints, WaypointInterpolator ignored ***');",
"\t\tscriptError=true;",
"\t\treturn;",
"\t}",
"\tif (\theightLabel.toLowerCase()!='altitude' &&",
"\t\theightLabel.toLowerCase()!='depth' &&",
"\t\theightLabel.toLowerCase()!='none')",
"\t{",
"\t\tforcePrint ('*** error, heightLabel =' + heightLabel + ', allowed values (none, altitude, depth) ***');",
"\t\theightLabel ='none';",
"\t}",
"",
"\tuseDefaultSpeed = false; // initialize booleans",
"\tuseLegSpeeds    = false;",
"\tuseLegDurations = false;",
"",
"\tif ((legSpeeds.length == 0) && (legDurations.length == 0)) // use defaultSpeed",
"\t{",
"\t\ttracePrint ('defaultSpeed    =' + defaultSpeed.toString() + ' meters/second');",
"\t\tif (defaultSpeed <= 0)",
"\t\t{",
"\t\t\tforcePrint ('*** error, defaultSpeed <= 0 ***');",
"\t\t\tscriptError=true;",
"\t\t\treturn;",
"\t\t}",
"\t\telse",
"\t\t{",
"\t\t\tuseDefaultSpeed = true;",
"\t\t\ttracePrint ('useDefaultSpeed = true');",
"\t\t}",
"\t}",
"\telse if (legSpeeds.length > 0)",
"\t{",
"\t\ttracePrint ('legSpeeds       =' + legSpeeds.toString() + ' meters/second');",
"\t\tif (legSpeeds.length != waypoints.length - 1)",
"\t\t{",
"\t\t\tforcePrint ('*** error, legSpeeds.length (' + legSpeeds.length + ' must be one less than waypoints.length (' + waypoints.length + ') ***');",
"\t\t\tscriptError=true;",
"\t\t\treturn;",
"\t\t}",
"\t\tfor (i = 0; i < legSpeeds.length; i++)",
"\t\t{",
"\t\t\tif (legSpeeds[i] <= 0)",
"\t\t\t{",
"\t\t\t\tforcePrint ('*** error, legSpeeds[' + i + '] zero or negative ***');",
"\t\t\t\tscriptError=true;",
"\t\t\t\treturn;",
"\t\t\t}",
"\t\t}",
"\t\tif (legDurations.length > 0)",
"\t\t\ttracePrint ('warning: legDurations ignored, useLegSpeeds=true');",
"\t\telse\ttracePrint ('useLegSpeeds=true');",
"\t\tuseLegSpeeds=true;",
"\t}",
"\telse // legDurations.length > 0",
"\t{",
"        // Xj3D X3DFieldreader.java line 1920: parse error fails to read MFTime values; PositionInterpolator.key destination uses MFFloat anyway",
"\t\tforcePrint ('legDurations    =' + legDurations.toString() + ' seconds');",
"\t\tif ((legDurations.length != 1) && (legDurations.length != waypoints.length - 1))",
"\t\t{",
"\t\t\tforcePrint ('*** error, legDurations.length must be one less than waypoints.length ***');",
"\t\t\tscriptError=true;",
"\t\t\treturn;",
"\t\t}",
"\t\tfor (i = 0; i < legDurations.length; i++)",
"\t\t{",
"\t\t\tif (legDurations[i] < 0)",
"\t\t\t{",
"\t\t\t\tlegDurations[i] = Math.abs(legDurations[i]);",
"\t\t\t\tforcePrint ('*** error, legDurations[' + i + ']= -' + legDurations[i]",
"\t\t\t\t\t+ ' is less than zero ***');",
"\t\t\t\tscriptError=true;",
"\t\t\t\treturn;",
"\t\t\t}",
"\t\t\telse if (legDurations[i] == 0)",
"\t\t\t{",
"\t\t\t\tforcePrint ('*** Warning, zero value encountered/ignored: ' +",
"\t\t\t\t'legDurations[' + i + '] =' + legDurations[i]);",
"\t\t\t}",
"\t\t}",
"\t\ttracePrint ('useLegDurations=true');",
"\t\tuseLegDurations=true;",
"\t}",
"\tpositionKeyValueArray = waypoints;",
"",
"\tfor (i = 0; i < (waypoints.length - 1); i++)",
"\t{",
"\t\tdistances[i] = Math.sqrt (",
"\t\t\t(waypoints[i+1].x - waypoints[i].x) * (waypoints[i+1].x - waypoints[i].x) +",
"\t\t\t(waypoints[i+1].y - waypoints[i].y) * (waypoints[i+1].y - waypoints[i].y) +",
"\t\t\t(waypoints[i+1].z - waypoints[i].z) * (waypoints[i+1].z - waypoints[i].z));",
"\t\ttotalDistance += distances[i];",
"\t\tpointIndicesAccumulator[i]= i;",
"\t}",
"\tforcePrint ('distances       =' + distances.toString() + ' meters');",
"\tforcePrint ('totalDistance   =' + Math.round (totalDistance * 10)/10 + ' meters');",
"\tpointIndicesAccumulator[waypoints.length - 1]= waypoints.length - 1;",
"\tpointIndicesAccumulator[waypoints.length]    = -1;",
"",
"\tfor (i = 0; i < (waypoints.length ); i++)",
"\t{",
"\t\tverticalDropLineIndicesAccumulator[3*i]    = 2*i;",
"\t\tverticalDropLineIndicesAccumulator[3*i+ 1] = 2*i + 1;",
"\t\tverticalDropLineIndicesAccumulator[3*i+ 2] = -1;",
"\t\tverticalDropLinePointsAccumulator[2*i]     = waypoints[i];",
"\t\tverticalDropLinePointsAccumulator[2*i+1]   = new SFVec3f(waypoints[i].x, 0.0, waypoints[i].z);",
"\t}",
"\tpointIndices = pointIndicesAccumulator;",
"\ttracePrint ('pointIndices    =' + pointIndices.toString());",
"\tverticalDropLineIndices = verticalDropLineIndicesAccumulator;",
"\ttracePrint ('verticalDropLineIndices  =' + verticalDropLineIndices.toString());",
"\tverticalDropLinePoints = verticalDropLinePointsAccumulator;",
"\ttracePrint ('verticalDropLinePoints =' + verticalDropLinePoints.toString());",
"",
"\ttotalDurationAccumulator = 0.0;",
"\tfor (i = 0; i < (waypoints.length - 1); i++)",
"\t{",
"\t\tif      (useDefaultSpeed)",
"\t\t{",
"\t\t\ttotalDurationAccumulator += distances[i] / defaultSpeed;",
"\t\t}",
"\t\telse if (useLegSpeeds)",
"\t\t{",
"\t\t\ttotalDurationAccumulator += distances[i] / legSpeeds[i];",
"\t\t}",
"\t\telse //  useLegDurations",
"\t\t{",
"\t\t\ttotalDurationAccumulator += legDurations[i];",
"\t\t//\tforcePrint ('legDurations[' + i + ']=' + legDurations[i]);",
"\t\t//\tforcePrint ('totalDurationAccumulator=' + totalDurationAccumulator + ' seconds');",
"\t\t}",
"\t}",
"\ttotalDuration = totalDurationAccumulator; // send SFTime eventOut",
"\thours   = Math.floor  (totalDuration / 3600.0); // % is modulo operator, provides remainder",
"\tminutes = Math.floor ((totalDuration - hours * 3600) / 60.0);",
"\tseconds = Math.round ((totalDuration - hours * 3600 - minutes * 60) * 10) / 10; // 0.1 sec resolution",
"\tif (totalDuration <= 0)",
"\t{",
"\t\tforcePrint ('*** error:  totalDuration=' + totalDuration + ' seconds (' +",
"\t  \t  hours + ' hours,' + minutes + ' minutes,' + seconds + ' seconds)');",
"\t\tscriptError=true;",
"\t\treturn;",
"\t}",
"\telse if (outputInitializationComputations)",
"\t    \t forcePrint ('totalDuration   =' + Math.round (totalDuration * 10)/10 + ' seconds (' +",
"\t  \t \t\thours + ' hours,' + minutes + ' minutes,' + seconds + ' seconds)');",
"",
"\tpositionKey[0] = 0;",
"\tfor (i = 1; i < waypoints.length; i++)",
"\t{",
"\t\tif      (useDefaultSpeed)",
"\t\t{",
"\t\t\tpositionKey[i] = i / (waypoints.length - 1); // simple fraction",
"\t\t}",
"\t\telse if (useLegSpeeds)",
"\t\t{",
"\t\t\tpositionKey[i] = ((distances[i-1] / legSpeeds[i-1]) / totalDuration) + positionKey[i-1];",
"\t\t}",
"\t\telse //  useLegDurations",
"\t\t{",
"\t\t\tpositionKey[i] = (legDurations[i-1] / totalDuration) + positionKey[i-1];",
"\t\t}",
"\t}",
"\tpositionKey[waypoints.length-1] = 1.0; // avoid roundup greater than 1.0",
"",
"\ttracePrint ('positionKey.length           =' + positionKey.length);",
"\ttracePrint ('positionKey                  =' + positionKey.toString());",
"\ttracePrint ('positionKeyValueArray.length =' + positionKeyValueArray.length);",
"\ttracePrint ('positionKeyValueArray        =' + positionKeyValueArray.toString());",
"",
"\t// directly set event",
"\tWaypointPI.key      = positionKey;",
"\tWaypointPI.keyValue = positionKeyValueArray;",
"\ttracePrint ('WaypointPI.key               =' + WaypointPI.key.toString());",
"\ttracePrint ('WaypointPI.keyValue          =' + WaypointPI.keyValue.toString());",
"",
"\t// ROUTE outputOnly event",
" \tfinalPositionKey           = positionKey;",
"\tfinalPositionKeyValueArray = positionKeyValueArray;",
"\ttracePrint ('finalPositionKey             =' + finalPositionKey.toString());",
"\ttracePrint ('finalPositionKeyValueArray   =' + finalPositionKeyValueArray.toString());",
"\ttracePrint ('WaypointPI.key               =' + WaypointPI.key.toString());",
"\ttracePrint ('WaypointPI.keyValue          =' + WaypointPI.keyValue.toString());",
"",
"\ttracePrint ('pitchUpDownForVerticalWaypoints=' + pitchUpDownForVerticalWaypoints);",
"",
"\t// different approaches to orientation calculations",
"\twhichRotationVersion ='FirstHeadingThenPitchStayVertical';",
"\t\t\t\t//'IndependentLegOrientations';",
"\t\t\t\t//'RelativeLegOrientations';",
"\t\t\t\t//'FirstHeadingThenPitchStayVertical';",
"\ttracePrint ('whichRotationVersion=' + whichRotationVersion);",
"\t// SFRotation constructor for two Vector3Arrays returns rotation from first to second",
"\t// default body axis is along X axis",
"        // TODO avoid changing value if normalized vector has length 0 (meaning no direction change)",
"        orientations = new MFRotation();",
"\torientations[0] = new SFRotation (new SFVec3f (1, 0, 0),",
"\t\twaypoints[1].subtract(waypoints[0]).normalize()); // first leg",
"\tdx = waypoints[1].x - waypoints[0].x;",
"\tdy = waypoints[1].y - waypoints[0].y;",
"\tdz = waypoints[1].z - waypoints[0].z;",
"\tlegDistance   = Math.sqrt (dx*dx + dy*dy + dz*dz);",
"\tlevelDistance = Math.sqrt (dx*dx + dz*dz);",
"\ttracePrint ('dx=' + dx + ', dy=' + dy + ', dz=' + dz + ', legDistance=' + legDistance + ', levelDistance=' + levelDistance);",
"\ttracePrint ('orientations[0] =' + orientations[0].toString());",
"",
"\tfor (i = 1; i < (waypoints.length - 1); i++) // compute orientations array",
"\t{",
"\t\tdx = waypoints[i+1].x - waypoints[i].x;",
"\t\tdy = waypoints[i+1].y - waypoints[i].y;",
"\t\tdz = waypoints[i+1].z - waypoints[i].z;",
"\t\tlegDistance   = Math.sqrt (dx*dx + dy*dy + dz*dz);",
"\t\tlevelDistance = Math.sqrt (dx*dx + dz*dz);",
"\t\ttracePrint ('dx=' + dx + ', dy=' + dy + ', dz=' + dz +",
"\t\t', legDistance='   + Math.round (  legDistance*10)/10 +",
"\t\t', levelDistance=' + Math.round (levelDistance*10)/10);",
"",
"//\t\ttracePrint ('waypoints[i  ].subtract(waypoints[i-1]) =' + waypoints[i  ].subtract(waypoints[i-1]).toString());",
"//\t\ttracePrint ('waypoints[i+1].subtract(waypoints[i])   =' + waypoints[i+1].subtract(waypoints[i]).toString());",
"//\t\ttracePrint ('dot product=' + waypoints[i+1].subtract(waypoints[i]).normalize().",
"//\t\t\t\t\t dot(waypoints[i].subtract(waypoints[i-1]).normalize()).toString());",
"",
"\t\tif (whichRotationVersion=='IndependentLegOrientations')",
"                {",
"                    tracePrint ('whichRotationVersion==IndependentLegOrientations');",
"                    // using constructor SFRotation (SFVec3f fromVector, SFVec3f toVector)",
"                    // see X3D ECMAScript binding Table 7.18 — SFRotation instance creation functions",
"                    // buggy: can twist/roll unpredictably about relative-x axis",
"                    // apparently a CosmoPlayer bug in SFRotation constructor when pointing (-1, 0, 0)",
"                    // TODO test if difference vector is zero, if so maintain previous rotation",
"                    orientations[i] = new SFRotation (",
"                            new SFVec3f (1, 0, 0),",
"                            waypoints[i+1].subtract(waypoints[i]).normalize());",
"                }",
"                else if (whichRotationVersion=='RelativeLegOrientations')",
"                {",
"                    tracePrint ('whichRotationVersion==IndependentLegOrientations');",
"                    orientations[i] = new SFRotation (",
"                            waypoints[i  ].subtract(waypoints[i-1]).normalize(),",
"                            waypoints[i+1].subtract(waypoints[i]).normalize());",
"                    // orientation multiplication (i.e. composition) is order dependent",
"                    orientations[i] = orientations[i-1].multiply (orientations[i]); // relative to previous leg",
"                }",
"                else if (whichRotationVersion=='FirstHeadingThenPitchStayVertical')",
"                {",
"                    if ( (Math.abs(legDistance)   <= 0.00001) ||",
"                        ((Math.abs(levelDistance) <= 0.00001) && (pitchUpDownForVerticalWaypoints == false)))",
"                    {",
"                            tracePrint ('whichRotationVersion==FirstHeadingThenPitchStayVertical, coincident');",
"                            if (legDistance <= 0.00001)",
"                                    tracePrint ('...staying in one place');",
"                            else",
"                                    tracePrint ('...maintaining orientation during vertical motion');",
"                            orientations[i] = orientations[i-1];",
"                    }",
"                    else if (levelDistance <= 0.00001)  // pitch up/down along vertical axis",
"                    {",
"                            tracePrint ('whichRotationVersion==FirstHeadingThenPitchStayVertical, pitch up/down along vertical axis');",
"                            // still twisting about roll axis, unfortunately...",
"                            if (waypoints[i+1].y > waypoints[i].y)  // or test dy",
"                            {",
"                                    tracePrint ('...pitching up vertical axis');",
"                                    orientations[i] = new SFRotation (",
"                                            waypoints[i].subtract(waypoints[i-1]).normalize(),",
"                                            new SFVec3f (0, 1, 0));  // relative",
"                            }",
"                            else",
"                            {",
"                                    tracePrint ('...pitching down vertical axis');",
"                                    orientations[i] = new SFRotation (",
"                                            waypoints[i].subtract(waypoints[i-1]).normalize(),",
"                                            new SFVec3f (0, -1, 0));  // relative",
"                            }",
"                            orientations[i] = orientations[i-1].multiply (orientations[i]); // relative to previous leg",
"                    }",
"                    else // carefully rotate about Y axis then pitch up/down to avoid unpredictable twists/rolls",
"                    {",
"                            tracePrint ('whichRotationVersion==FirstHeadingThenPitchStayVertical, carefully rotate about Y axis etc.');",
"                            heading = Math.atan2 (dz, dx); // atan2 returns arctangent in any of 4 quadrants",
"                            orientations[i] = new SFRotation (0, 1, 0, -heading); // note negation",
"                            // can go vertical if preferred, levelDistance == 0 cases handled above",
"                            pitchAngle  = Math.atan (dy / levelDistance); // negative angle should pitch down, note no negation",
"                            // orientation multiplication (i.e. composition) is order dependent",
"                            // !! this is the step that causes a Cosmo/Cortona sign error !!",
"                            // it is due to opposite responses to multiplication order.",
"                            tempHold = orientations[i];  // not assuming that browser self-multiplication is safe",
"                            if (Browser.name=='CosmoPlayer') // reverse multiplication order for old browser",
"                                    orientations[i] = (new SFRotation (0, 0, 1, pitchAngle)).multiply (tempHold); // mod heading",
"                            else\torientations[i] = tempHold.multiply (new SFRotation (0, 0, 1, pitchAngle));   // mod heading",
"                            tracePrint ('heading='    + Math.round (degrees (heading)   *10)/10 + ' degrees,' +",
"                                       ' pitchAngle=' + Math.round (degrees (pitchAngle)*10)/10 + ' degrees');",
"                    }",
"\t\t}",
"                else if      (Math.abs(legDistance)   <= 0.00001)",
"                {",
"                    tracePrint ('coincident waypoints, set orientations[' + i + '] = orientations[' + i-1 + ']');",
"                    orientations[i] = orientations[i-1];",
"                }",
"\t\telse ",
"                {",
"                        forcePrint ('*** unexpected case trapped, set orientations[' + i + '] = orientations[' + i-1 + ']');",
"                        orientations[i] = orientations[i-1];",
"                }",
"\t\ttracePrint ('orientations[' + i + '] =' + orientations[i].toString());",
"\t}",
"//\ttraceEnabled = true; // debug",
"",
"\t// full array trace",
"\ttracePrint ('orientations   =' + orientations.toString());",
"",
"\tif (orientations.length != (waypoints.length - 1))",
"\t{",
"\t\tforcePrint ('** computation error: orientations.length=' + orientations.length + ' mismatch with waypoints.length=' + waypoints.length);",
"\t}",
"",
"\tif (turningRate < 0)",
"\t{",
"\t\tforcePrint ('** error:  negative value for turningRate illegal, making turningRate positive');",
"\t\tturningRate = -turningRate;",
"\t}",
"\ttracePrint ('turningRate     =' + turningRate + ' degrees/second');",
"",
"\torientationKey = new MFFloat ();",
"\torientationKey[0] = 0;",
"\tfor (i = 1; i < (waypoints.length-1); i++)",
"\t{",
"\t\tdeltaAngle = orientations[i].multiply(orientations[i-1].inverse()).angle;",
"\t\tdeltaAngle = normalizePi (deltaAngle);",
"\t\tturnTime = Math.abs (deltaAngle) / radians (turningRate);",
"\t\ttracePrint ('deltaAngle[' + i + ']=' + degrees (deltaAngle) + ' degrees, turnTime=' + turnTime);",
"",
"\t\tprecedingLegDuration = (positionKey[i]   - positionKey[i-1]) * totalDuration;",
"\t\tfollowingLegDuration = (positionKey[i+1] - positionKey[i]  ) * totalDuration;",
"\t\t// turn for no more than 1/3 of preceding or following leg durations, respectively",
"\t\tprecedingTurnKeyOffset = Math.min (turnTime/2, precedingLegDuration/3) / totalDuration;",
"\t\tfollowingTurnKeyOffset = Math.min (turnTime/2, followingLegDuration/3) / totalDuration;",
"\t\ttracePrint ('precedingTurnKeyOffset=' + (precedingTurnKeyOffset * totalDuration) + ' seconds');",
"\t\ttracePrint ('followingTurnKeyOffset=' + (followingTurnKeyOffset * totalDuration) + ' seconds');",
"",
"\t\torientationKey[3*i - 2] = positionKey[i] - precedingTurnKeyOffset;",
"\t\torientationKey[3*i - 1] = positionKey[i];",
"\t\torientationKey[3*i]     = positionKey[i] + followingTurnKeyOffset;",
"\t\tif (orientationKey[3*i - 2] <= positionKey[i-1]) // interpolate preceding key if needed",
"\t\t{",
"\t\t\torientationKey[3*i - 2] = positionKey[i-1] + ((positionKey[i] - positionKey[i-1]) * 2 / 3);",
"\t\t}",
"\t\tif (orientationKey[3*i] >= positionKey[i+1]) // interpolate following key if needed",
"\t\t{",
"\t\t\torientationKey[3*i]     = positionKey[i] + ((positionKey[i+1] - positionKey[i])   * 1 / 3);",
"\t\t}",
"\t\tif ((orientationKey[3*i - 2] > orientationKey[3*i - 1]) || (orientationKey[3*i - 1] > orientationKey[3*i]))",
"\t\t{",
"\t\t\tforcePrint ('** error computing orientationKey [' + (3*i - 2) + '..' + (3*i) + ']');",
"\t\t}",
"\t}",
"\torientationKey[3*(waypoints.length-1)-2] = 1.0; // avoid roundup greater than 1",
"\ttracePrint ('orientationKey.length =' + orientationKey.length);",
"\ttracePrint ('orientationKey        =' + orientationKey.toString());",
"",
"\t//",
"\tfor (i = 2; i < (orientationKey.length-1); i++)",
"\t{",
"\t   if (orientationKey [i-1] > orientationKey [i])",
"\t\tforcePrint ('*** error,' +",
"\t\t'orientationKey [' + (i-1) + ']=' + orientationKey [i-1].toString() + ',' +",
"\t\t'orientationKey [' + (i) + ']='   + orientationKey [i].toString() +",
"\t\t' values are not monotonically increasing ***');",
"\t   if ((orientationKey [i] < 0) || (orientationKey [i] > 1))",
"\t\tforcePrint ('*** error, orientationKey [' + i + ']=' + orientationKey [i].toString() +",
"\t\t' value is out of range [0..1] ***');",
"\t}",
"\ttracePrint ('check orientationKey complete, dynamically building orientationKeyValueArray next');",
"\torientationKeyValueArray = new MFRotation ();",
"\torientationKeyValueArray[0] = orientations[0];",
"\torientationKeyValueArray[1] = orientations[0];",
"\tfor (i = 1; i < (waypoints.length - 1); i++)",
"\t{",
"\t//\tspherical linear interpolation (slerp) 0.5 interpolates halfway between adjacent orientations",
"\t\torientationKeyValueArray[3*i - 1] = orientations[i-1].slerp(orientations[i], 0.5);",
"\t\torientationKeyValueArray[3*i]     = orientations[i];",
"\t\torientationKeyValueArray[3*i + 1] = orientations[i]; // straight-line track, same orientation",
"\t}",
"\ttracePrint ('orientationKeyValueArray.length =' + orientationKeyValueArray.length);",
"\ttracePrint ('orientationKeyValueArray        =' + orientationKeyValueArray.toString());",
"",
"\t// eliminate orientationKey triplicates (smaller arrays overcome CosmoPlayer overflow bug)",
"\tnewKey      = new MFFloat ();",
"\tnewKey      [0] = orientationKey [0];",
"\tnewKey      [1] = orientationKey [1];",
"\tnewKeyValue = new MFRotation ();",
"\tnewKeyValue [0] = orientationKeyValueArray [0];",
"\tnewKeyValue [1] = orientationKeyValueArray [1];",
"\tindex = 2; // keep first two orientations identical, index is for next value",
"        for (i = 2; i < (orientationKeyValueArray.length-3) ; i++)",
"\t{",
"\t   dotProductBA      =  orientationKeyValueArray [i-1].getAxis().dot(orientationKeyValueArray [i-2].getAxis());",
"\t   dotProductCB      =  orientationKeyValueArray [i].getAxis().dot(orientationKeyValueArray [i-1].getAxis());",
"\t   angleDifferenceBA = normalizePi(",
"\t   \tnormalize2Pi (orientationKeyValueArray [i-1].angle) -",
"\t   \tnormalize2Pi (orientationKeyValueArray [i-2].angle)) * 180 / Math.PI;",
"\t   angleDifferenceCB = normalizePi(",
"\t   \tnormalize2Pi (orientationKeyValueArray [i].angle) -",
"\t   \tnormalize2Pi (orientationKeyValueArray [i-1].angle)) * 180 / Math.PI;",
"",
"\t   if (i < 10) // too many outputs clobbers the trace console",
"\t   {",
" \t     tracePrint ('orientationKeyValueArray [' + (i-2) + ']=' + orientationKeyValueArray [i-2].toString());",
" \t     tracePrint ('orientationKeyValueArray [' + (i-1) + ']=' + orientationKeyValueArray [i-1].toString());",
" \t     tracePrint ('orientationKeyValueArray [' + (i  ) + ']=' + orientationKeyValueArray [i  ].toString());",
"\t     tracePrint ('dotProductBA     =' + dotProductBA +     ', dotProductCB     =' + dotProductCB);",
"\t     tracePrint ('angleDifferenceBA=' + angleDifferenceBA + ', angleDifferenceBC=' + angleDifferenceCB + ' degrees');",
"\t   }",
"",
"//         // depth check also needed!  but positionKey is already optimized/compressed, so how to check?",
"//\t   if ((Math.abs (dotProductCB - 1)  < 0.01) &&",
"//\t       (Math.abs (dotProductBA - 1)  < 0.01) &&",
"//\t       (Math.abs (angleDifferenceCB) < 1.0 ) &&",
"//\t       (Math.abs (angleDifferenceBA) < 1.0 ))  // degrees",
"//\t   {",
"//\t\t// replace key time with later value",
"//\t\ttracePrint ('... matching this orientationKey time,' +",
"//\t\t'updating key' + newKey [index-1] + ' to' + orientationKey [i]);",
"//\t\tnewKey      [index-1] = orientationKey [i];",
"//\t\t// don't update orientation in order to avoid creeping matches",
"//\t   }",
"//\t   else",
"//\t   {",
"\t\tnewKey      [index] = orientationKey [i];",
"\t\tnewKeyValue [index] = orientationKeyValueArray [i];",
"\t\tindex ++;",
"\t\ttracePrint ('...  keeping this orientationKeyValue');",
"//\t   }",
"\t   if (newKey [index-2] > newKey [index-1])",
"\t\tforcePrint ('*** error,' +",
"\t\t'newKey [' + (index-2) + ']=' + newKey [index-2].toString() + ',' +",
"\t\t'newKey [' + (index-1) + ']=' + newKey [index-1].toString() +",
"\t\t' values are not monotonically increasing ***');",
"\t   if ((newKey [index-1] < 0) || (newKey [index-1] > 1))",
"\t\tforcePrint ('*** error, newKey [' + (index-1) + ']=' + newKey [index-1].toString() +",
"\t\t' value is out of range [0..1] ***');",
"\t}",
"\tnewKey      [index] = orientationKey [orientationKeyValueArray.length-2]; // match finals values",
"\tnewKeyValue [index] = orientationKeyValueArray [orientationKeyValueArray.length-2];",
"\tindex++;",
"\tnewKey      [index] = orientationKey [orientationKeyValueArray.length-1]; // match finals values",
"\tnewKeyValue [index] = orientationKeyValueArray [orientationKeyValueArray.length-1];",
"\ttracePrint ('orientation newKey.length      =' + newKey.length);",
"\ttracePrint ('orientation newKey             =' + newKey.toString());",
"\ttracePrint ('orientation newKeyValue.length =' + newKeyValue.length);",
"\ttracePrint ('orientation newKeyValue        =' + newKeyValue.toString());",
"",
"\tWaypointOI.key      = newKey;",
"\tWaypointOI.keyValue = newKeyValue;",
"\ttracePrint ('WaypointOI.key                 =' + WaypointOI.key.toString());",
"\ttracePrint ('WaypointOI.keyValue            =' + WaypointOI.keyValue.toString());",
"",
"\ttracePrint ('labelDisplayMode=' + labelDisplayMode);",
"\tif (labelDisplayMode.toLowerCase() =='waypoints')",
"\t{",
"\t  // create text labels for each waypoint",
"\t  outputChild = new MFNode ();",
"\t  outputVrmlString ='';",
"\t  for (i = 0; i < waypoints.length; i++)",
"\t  {",
"\t\ttextOffset = waypoints[i].add(labelOffset);",
"\t\tif ((i == waypoints.length-1) && (waypoints[i].x == waypoints[0].x) &&",
"\t\t\t(waypoints[i].y == waypoints[0].y) && (waypoints[i].z == waypoints[0].z))",
"\t\t    // double offset for endpoint when waypoints are a loop",
"\t\t    textOffset = textOffset.subtract(new SFVec3f (0, 3 * labelFontSize, 0));",
"\t\thours   = Math.floor  (totalDuration * positionKey[i] / 3600.0); // % is modulo operator, provides remainder",
"\t\tminutes = Math.floor ((totalDuration * positionKey[i] - hours * 3600.0) / 60.0);",
"\t\tseconds = Math.round  (totalDuration * positionKey[i] - hours * 3600.0 - minutes * 60.0);",
"\t\twhile (minutes >= 60)",
"\t\t{",
"\t\t\tminutes -= 60;",
"\t\t\thours   += 1;",
"\t\t}",
"\t\twhile (seconds >= 60)",
"\t\t{",
"\t\t\tseconds -= 60;",
"\t\t\tminutes += 1;",
"\t\t}",
"\t\tif (hours   < 10) hours   ='0' + hours;",
"\t\tif (minutes < 10) minutes ='0' + minutes;",
"\t\tif (seconds < 10) seconds ='0' + seconds;",
"\t\tlocationX =  Math.round (waypoints[i].x);",
"\t\tdepth     = -Math.round (waypoints[i].y * 10) / 10;",
"\t\tlocationZ =  Math.round (waypoints[i].z);",
"\t\tif      (heightLabel.toLowerCase()=='altitude')",
"\t\t\tdepthString = (-depth) + ' ';",
"\t\telse if (heightLabel.toLowerCase()=='depth')",
"\t\t\tdepthString = depth + ' ';",
"\t\telse if (heightLabel.toLowerCase()=='none')",
"\t\t\tdepthString =' ';",
"\t\telse\tdepthString =' ';",
"\t\toutputVrmlString +=",
"\t\t\t 'Transform { translation' + textOffset + '\n'",
"\t\t\t+ ' children LOD { range [' + 150 * labelFontSize + ' ]\n'",
"\t\t\t+ '  level [\n'",
"\t\t\t+ '   Billboard { axisOfRotation 0 1 0 \n'",
"\t\t\t+ '    children Shape {\n'",
"\t\t\t+ '\tgeometry Text {\n'",
"\t\t\t+ '\t   string [ \"' + hours + ':' + minutes + ':' + seconds + '\"\n'",
"\t\t\t+ '\t            \"' + locationX + ' ' + depthString +  locationZ + ' ' + '\" ]\n'",
"\t\t\t+ '\t   fontStyle DEF WPIFontStyle FontStyle {\n'",
"\t\t\t+ '\t\tsize' + labelFontSize + '\n'",
"\t\t\t+ '\t\tjustify [ \"MIDDLE\" \"MIDDLE\" ]\n'",
"\t\t\t+ '\t   }\n'",
"\t\t\t+ '\t}\n'",
"\t\t\t+ '\tappearance DEF WPIAppearance Appearance {\n'",
"\t\t\t+ '\t   material Material { diffuseColor' + labelColor + ' }\n'",
"\t\t\t+ '\t}\n'",
"\t\t\t+ '    }\n'",
"\t\t\t+ '   }\n'",
"\t\t\t+ '  WorldInfo { } ]\n'",
"\t\t\t+ ' }\n'",
"\t\t\t+ '}\n';",
"\t  }",
"\t  tracePrint ('outputVrmlString=' + outputVrmlString);",
"",
"\t  outputChild = Browser.createVrmlFromString (outputVrmlString);",
"\t  OutputLabelsGroup.addChildren = outputChild;",
"",
"//\t  tracePrint ('OutputLabelsGroup.children =');",
"//\t  tracePrint (outputChild + '  ' + OutputLabelsGroup.children.toString());",
"\t}",
"\telse if (labelDisplayMode.toLowerCase() =='interpolation')",
"\t{",
"\t\t// updates occur when fraction changes",
"\t}",
"\telse if ((labelDisplayMode.toLowerCase() !='none') && (labelDisplayMode !=''))",
"\t{",
"\t  forcePrint ('*** illegal value labelDisplayMode=' + labelDisplayMode + ', ignored');",
"\t}",
"",
"\tif (outputInitializationComputations)",
"        {",
"\t     tracePrint ('initialization complete');",
"\t     forcePrint ('=======================================');",
"        }",
"        traceEnabled = saveTrace;",
"",
"} // end of initialize() method",
"",
"function set_fraction (fractionValue, timeStamp)",
"{",
"\ttracePrint ('fractionValue=' + fractionValue);",
"\ttracePrint ('previousFractionIndex=' + previousFractionIndex);",
"\ttracePrint ('WaypointPI.value_changed=' + WaypointPI.value_changed.toString());",
"\ttracePrint ('WaypointOI.value_changed=' + WaypointOI.value_changed.toString());",
"",
"\tif (scriptError==true)",
"    {",
"        tracePrint ('scriptError==true, no response by set_fraction()');",
"        return;",
"    }",
"\t//\ttracePrint ('WaypointPI.key               =' + WaypointPI.key.toString());",
"\t//\ttracePrint ('WaypointPI.keyValue          =' + WaypointPI.keyValue.toString());",
"",
"//\twide input range supported by interpolators,",
"//\tusually no range check on fractionValue.",
"//\thowever WaypointInterpolator input range is [0..1], so check",
"\tif ((fractionValue < 0) || (fractionValue > 1))",
"\t{",
"\t\tforcePrint ('*** error:  set_fraction=' + fractionValue + ' out of range [0..1], ignored');",
"\t\treturn;",
"\t}",
"",
"\tif (previousFractionIndex == -1)",
"\t{",
"\t\tpreviousFractionIndex = 0; // start",
"\t\twhile (fractionValue >= positionKey[previousFractionIndex+1])",
"\t\t{",
"\t\t\tpreviousFractionIndex ++;",
"\t\t\tif (previousFractionIndex >= waypoints.length - 2) break;",
"\t\t}",
"\t\thighlightCoordinates = new MFVec3f (waypoints[previousFractionIndex],",
"\t\t\twaypoints[previousFractionIndex +1]);",
"\t\ttracePrint ('highlightCoordinates=' + highlightCoordinates.toString());",
"\t}",
"\telse if (waypoints.length == 2)",
"\t{",
"\t\t// only one segment, no action required",
"\t}",
"\telse if (previousFractionIndex == waypoints.length - 2) // last leg",
"\t{",
"\t  if (fractionValue < positionKey[previousFractionIndex]) // looped",
"\t  {",
"\t\tpreviousFractionIndex = 0; // start",
"\t\twhile (fractionValue >= positionKey[previousFractionIndex+1])",
"\t\t{",
"\t\t\tpreviousFractionIndex ++;",
"\t\t\tif (previousFractionIndex >= waypoints.length - 2) break;",
"\t\t}",
"\t\thighlightCoordinates = new MFVec3f (waypoints[previousFractionIndex],",
"\t\t\twaypoints[previousFractionIndex +1]);",
"\t\ttracePrint ('highlightCoordinates=' + highlightCoordinates.toString());",
"\t  }",
"\t}",
"\telse if (fractionValue >= positionKey[previousFractionIndex+1])",
"\t{",
"\t\tpreviousFractionIndex++;",
"\t\twhile (fractionValue >= positionKey[previousFractionIndex+1])",
"\t\t{",
"\t\t\tpreviousFractionIndex ++;",
"\t\t\tif (previousFractionIndex >= waypoints.length - 2) break;",
"\t\t}",
"\t\tif (previousFractionIndex > waypoints.length - 2) previousFractionIndex = 0;",
"\t\thighlightCoordinates = new MFVec3f (",
"\t\t\twaypoints[previousFractionIndex],",
"\t\t\twaypoints[previousFractionIndex+1]);",
"\t\ttracePrint ('highlightCoordinates=' + highlightCoordinates.toString());",
"\t}",
"\t// else previousFractionIndex ought to be OK",
"",
"\tif (labelDisplayMode =='interpolation')",
"\t{",
"\t\thours   = Math.floor  (totalDuration * fractionValue / 3600.0); // % is modulo operator, provides remainder",
"\t\tminutes = Math.floor ((totalDuration * fractionValue - hours * 3600) / 60.0);",
"\t\tseconds = Math.round  (totalDuration * fractionValue - hours * 3600 - minutes * 60);",
"\t\twhile (minutes > 60)",
"\t\t{",
"\t\t\tminutes -= 60;",
"\t\t\thours   += 1;",
"\t\t}",
"\t\twhile (seconds > 60)",
"\t\t{",
"\t\t\tseconds -= 60;",
"\t\t\tminutes += 1;",
"\t\t}",
"\t\tif (hours   < 10) hours   ='0' + hours;",
"\t\tif (minutes < 10) minutes ='0' + minutes;",
"\t\tif (seconds < 10) seconds ='0' + seconds;",
"",
"\t\t// compute course and pitch",
"\t\tcurrentAxis     = WaypointOI.value_changed.getAxis().normalize();",
"\t\tcurrentRotation = WaypointOI.value_changed;",
"   //   forcePrint ('=====currentRotation=' + currentRotation.toString() + ', currentAxis=' + currentAxis.toString());",
"",
"\t\trotatedVector = currentRotation.multVec (new SFVec3f (1, 0, 0)); // rotate x-centered body",
"\t\tdx = rotatedVector.x;",
"\t\tdy = rotatedVector.y;",
"\t\tdz = rotatedVector.z;",
"\t\tlevelDistance = Math.sqrt (dx*dx + dz*dz);",
"\t\theading = Math.atan2 (dz, dx); // atan2 returns arctangent in any of 4 quadrants",
"\t\tif (levelDistance > 0)",
"\t\t\tpitchAngle =  Math.atan (dy / levelDistance); // negative angle should pitch down, note no negation",
"\t\telse if (dy > 0)",
"\t\t\tpitchAngle =  1.57;",
"\t\telse    pitchAngle = -1.57;",
"",
"\t//\tforcePrint ('rotatedVector=' + rotatedVector.toString());",
"\t//\tforcePrint ('heading=' + degrees(heading) + ', pitchAngle=' + degrees(pitchAngle));",
"",
"\t\tcourse = Math.round (normalize2Pi ( heading)    * 180 / Math.PI);",
"\t\tpitch  = Math.round (normalizePi  ( pitchAngle) * 180 / Math.PI);",
"\t\t// format angles in degrees",
"\t\tif      (course <  10) course = '0' + '0' + course;",
"\t\telse if (course < 100) course = '0' + course;",
"",
"\t//\ttracePrint ('course=' + course + ', pitch=' + pitch);",
"",
"\t\tlocationX =  Math.round (WaypointPI.value_changed.x);",
"\t\tdepth     = -Math.round (WaypointPI.value_changed.y * 10) / 10;",
"\t\tlocationZ =  Math.round (WaypointPI.value_changed.z);",
"\t\tif      (heightLabel.toLowerCase()=='altitude')",
"\t\t\tdepthString =', altitude ' + (-depth) + 'm';",
"\t\telse if (heightLabel.toLowerCase()=='depth')",
"\t\t\tdepthString =', depth '    + depth + 'm';",
"\t\telse if (heightLabel.toLowerCase()=='none')",
"\t\t\tdepthString ='';",
"\t\telse\tdepthString ='';",
"\t  \tlabelInterpolation  = new MFString (",
"\t\t\tdescription,",
"\t\t\t(hours + ':' + minutes + ':' + seconds + ', course=' + course + ', pitch=' + pitch),",
"\t\t\t('location=(' + locationX + ' ' + locationZ + depthString + ')'));",
"\t//\ttracePrint ('labelInterpolation=' + labelInterpolation);",
"\t}",
"        tracePrint ('=====');",
"\treturn;",
"}",
"",
"function add_waypoint (newWaypointsArray, timeStamp)",
"{",
"\t// EcmaScript automatically increases array size",
"\t// when setting an element one past final element",
"\twaypoints[waypoints.length] = newWaypointsArray;",
"",
"\t// initialization code is complicated! so we won't try to shortcut/optimize it, instead just rerun it",
"\tinitialize ();",
"}",
"",
"function set_waypoints (newWaypointsArray, timeStamp)",
"{",
"\twaypoints = newWaypointsArray;",
"\tinitialize ();",
"}",
"",
""
]
                            }
                          },
                          { "ROUTE":
                            {
                              "@fromField":"finalPositionKey",
                              "@fromNode":"WaypointTrackScript",
                              "@toField":"key",
                              "@toNode":"WaypointPI.instance"
                            }
                          },
                          { "ROUTE":
                            {
                              "@fromField":"finalPositionKeyValueArray",
                              "@fromNode":"WaypointTrackScript",
                              "@toField":"keyValue",
                              "@toNode":"WaypointPI.instance"
                            }
                          },
                          {
                            "#comment":"IndexedLineSet connects waypoints for easy visibility. Set transparency=1 to hide."
                          },
                          { "Shape":
                            {
                              "@DEF":"VerticalDropLineShape",
                              "-geometry":
                                { "IndexedLineSet":
                                  {
                                    "@DEF":"VerticalDropLine",
                                    "-coord":
                                      { "Coordinate":
                                        {
                                          "@DEF":"VerticalDropLineCoordinates"
                                        }
                                      }
                                  }
                                },
                              "-appearance":
                                { "Appearance":
                                  {
                                    "-material":
                                      { "Material":
                                        {
                                          "@DEF":"VerticalDropLineMaterial",
                                          "IS": {
                                              "connect": [
                                                {
                                                  "@nodeField":"emissiveColor",
                                                  "@protoField":"verticalDropLineColor"
                                                },
                                                {
                                                  "@nodeField":"transparency",
                                                  "@protoField":"verticalDropLineTransparency"
                                                }
                                              ]
                                          }
                                        }
                                      }
                                  }
                                }
                            }
                          },
                          { "ROUTE":
                            {
                              "@fromField":"verticalDropLineIndices",
                              "@fromNode":"WaypointTrackScript",
                              "@toField":"set_coordIndex",
                              "@toNode":"VerticalDropLine"
                            }
                          },
                          { "ROUTE":
                            {
                              "@fromField":"verticalDropLinePoints",
                              "@fromNode":"WaypointTrackScript",
                              "@toField":"point",
                              "@toNode":"VerticalDropLineCoordinates"
                            }
                          },
                          { "Shape":
                            {
                              "@DEF":"HighlightShape",
                              "-geometry":
                                { "IndexedLineSet":
                                  {
                                    "@DEF":"HighlightSegment",
                                    "@coordIndex":[0,1,-1],
                                    "-coord":
                                      { "Coordinate":
                                        {
                                          "@DEF":"HighlightSegmentCoordinates",
                                          "@point":[0,0,0,0,0,0]
                                        }
                                      }
                                  }
                                },
                              "-appearance":
                                { "Appearance":
                                  {
                                    "-material":
                                      { "Material":
                                        {
                                          "@DEF":"HighlightSegmentMaterial",
                                          "@diffuseColor":[0,0,0],
                                          "@emissiveColor":[0.2,0.2,0.2],
                                          "IS": {
                                              "connect": [
                                                {
                                                  "@nodeField":"emissiveColor",
                                                  "@protoField":"highlightSegmentColor"
                                                },
                                                {
                                                  "@nodeField":"transparency",
                                                  "@protoField":"transparency"
                                                }
                                              ]
                                          }
                                        }
                                      }
                                  }
                                }
                            }
                          },
                          { "ROUTE":
                            {
                              "@fromField":"highlightCoordinates",
                              "@fromNode":"WaypointTrackScript",
                              "@toField":"point",
                              "@toNode":"HighlightSegmentCoordinates"
                            }
                          },
                          { "Shape":
                            {
                              "@DEF":"WaypointLineShape",
                              "-geometry":
                                { "IndexedLineSet":
                                  {
                                    "@DEF":"WaypointLine",
                                    "-coord":
                                      { "Coordinate":
                                        {
                                          "@DEF":"WaypointLineCoordinates",
                                          "IS": {
                                              "connect": [
                                                {
                                                  "@nodeField":"point",
                                                  "@protoField":"waypoints"
                                                }
                                              ]
                                          }
                                        }
                                      }
                                  }
                                },
                              "-appearance":
                                { "Appearance":
                                  {
                                    "-material":
                                      { "Material":
                                        {
                                          "@DEF":"WaypointTrackMaterial",
                                          "@emissiveColor":[0.8,0.8,0.8],
                                          "IS": {
                                              "connect": [
                                                {
                                                  "@nodeField":"emissiveColor",
                                                  "@protoField":"lineColor"
                                                },
                                                {
                                                  "@nodeField":"transparency",
                                                  "@protoField":"transparency"
                                                }
                                              ]
                                          }
                                        }
                                      }
                                  }
                                }
                            }
                          },
                          { "ROUTE":
                            {
                              "@fromField":"pointIndices",
                              "@fromNode":"WaypointTrackScript",
                              "@toField":"set_coordIndex",
                              "@toNode":"WaypointLine"
                            }
                          },
                          {
                            "#comment":"Draw highlight segment before and after waypoint lines in case of order dependency"
                          },
                          {
                            "#comment":"TODO!! throws Xj3D exception! <Shape USE='HighlightShape'/>"
                          },
                          { "Transform":
                            {
                              "@DEF":"MovingVehicleLabel",
                              "-children":[
                                {
                                  "#comment":"no need to externally ROUTE position and orientation interpolator key/keyValue results, since prototype is using pass-by-reference node update"
                                },
                                {
                                  "#comment":"Nevertheless, must ROUTE position and orientation interpolated text label"
                                },
                                { "ROUTE":
                                  {
                                    "@fromField":"value_changed",
                                    "@fromNode":"WaypointPI.instance",
                                    "@toField":"translation",
                                    "@toNode":"MovingVehicleLabel"
                                  }
                                },
                                { "ROUTE":
                                  {
                                    "@fromField":"value_changed",
                                    "@fromNode":"WaypointOI.instance",
                                    "@toField":"rotation",
                                    "@toNode":"MovingVehicleLabel"
                                  }
                                },
                                { "Transform":
                                  {
                                    "@DEF":"MovingVehicleLabelOffset",
                                    "IS": {
                                        "connect": [
                                          {
                                            "@nodeField":"translation",
                                            "@protoField":"labelOffset"
                                          }
                                        ]
                                    },
                                    "-children":[
                                      { "Billboard":
                                        {
                                          "-children":[
                                            { "Shape":
                                              {
                                                "-geometry":
                                                  { "Text":
                                                    {
                                                      "@DEF":"MovingVehicleLabelText",
                                                      "-fontStyle":
                                                        { "FontStyle":
                                                          {
                                                            "@DEF":"MovingVehicleLabelFont",
                                                            "@justify":["MIDDLE","MIDDLE"],
                                                            "IS": {
                                                                "connect": [
                                                                  {
                                                                    "@nodeField":"size",
                                                                    "@protoField":"labelFontSize"
                                                                  }
                                                                ]
                                                            }
                                                          }
                                                        }
                                                    }
                                                  },
                                                "-appearance":
                                                  { "Appearance":
                                                    {
                                                      "-material":
                                                        { "Material":
                                                          {
                                                            "@DEF":"MovingVehicleLabelMaterial",
                                                            "IS": {
                                                                "connect": [
                                                                  {
                                                                    "@nodeField":"diffuseColor",
                                                                    "@protoField":"labelColor"
                                                                  }
                                                                ]
                                                            }
                                                          }
                                                        }
                                                    }
                                                  }
                                              }
                                            },
                                            { "ROUTE":
                                              {
                                                "@fromField":"labelInterpolation",
                                                "@fromNode":"WaypointTrackScript",
                                                "@toField":"string",
                                                "@toNode":"MovingVehicleLabelText"
                                              }
                                            }
                                          ]
                                        }
                                      }
                                    ]
                                  }
                                }
                              ]
                            }
                          }
                        ]
                      }
                    }
                  ]
              }
            }
          },
          {
            "#comment":"======================================"
          },
          { "Anchor":
            {
              "@description":"WaypointInterpolator Example",
              "@url":["WaypointInterpolatorExample.x3d","https://www.web3d.org/x3d/content/examples/Savage/Tools/Animation/WaypointInterpolatorExample.x3d","WaypointInterpolatorExample.wrl","https://www.web3d.org/x3d/content/examples/Savage/Tools/Animation/WaypointInterpolatorExample.wrl"],
              "-children":[
                { "Shape":
                  {
                    "-geometry":
                      { "Text":
                        {
                          "@string":["WaypointInterpolatorPrototype","defines a prototype","Click on this text to see","WaypointInterpolatorExample"," scene"],
                          "-fontStyle":
                            { "FontStyle":
                              {
                                "@justify":["MIDDLE","MIDDLE"]
                              }
                            }
                        }
                      },
                    "-appearance":
                      { "Appearance":
                        {
                          "-material":
                            { "Material":
                              {
                                "@diffuseColor":[1,1,0.2]
                              }
                            }
                        }
                      }
                  }
                },
                { "Shape":
                  {
                    "-geometry":
                      { "Box":
                        {
                          "@size":[12,6.0,0.001]
                        }
                      },
                    "-appearance":
                      { "Appearance":
                        {
                          "-material":
                            { "Material":
                              {
                                "@diffuseColor":[1,1,1],
                                "@transparency":1
                              }
                            }
                        }
                      }
                  }
                }
              ]
            }
          }
        ]
    }
  }
}