X3D Model Documentation: WaypointInterpolatorPrototype.x3d

  1  <?xml version="1.0" encoding="UTF-8"?>
  2  <!DOCTYPE X3D PUBLIC "ISO//Web3D//DTD X3D 3.2//EN" "https://www.web3d.org/specifications/x3d-3.2.dtd">
  3  <X3D profile='Immersive' version='3.2 xmlns:xsd='http://www.w3.org/2001/XMLSchema-instance' xsd:noNamespaceSchemaLocation='https://www.web3d.org/specifications/x3d-3.2.xsd'>
  4       <head>
  5            <meta name='titlecontent=' WaypointInterpolatorPrototype.x3d '/>
  6            <meta name='descriptioncontent='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.'/>
  7            <meta name='creatorcontent='Don Brutzman, Curtis Blais, Jeff Weekley, Jane Wu'/>
  8            <meta name='createdcontent='6 April 2001'/>
  9            <meta name='modifiedcontent='23 August 2023'/>
 10            <meta name='identifiercontent=' https://www.web3d.org/x3d/content/examples/Savage/Tools/Animation/WaypointInterpolatorPrototype.x3d '/>
 11            <meta name='referencecontent=' https://www.web3d.org/x3d/content/examples/Savage/Tools/Animation/WaypointInterpolatorExample.x3d '/>
 12            <meta name=' warning content=' browsers do not compute pitch angle consistently '/>
 13            <meta name='generatorcontent='X3D-Edit 4.0, https://www.web3d.org/x3d/tools/X3D-Edit'/>
 14            <meta name='licensecontent='../../license.html'/>
 15       </head>
<!--

<!--
Event Graph ROUTE Table shows event connections.
-->
<!-- to top DEF nodes index: CoordinateLabelsAndViewpointsGroup, HighlightSegment, HighlightSegmentCoordinates, HighlightSegmentMaterial, HighlightShape, MovingVehicleLabel, MovingVehicleLabelFont, MovingVehicleLabelMaterial, MovingVehicleLabelOffset, MovingVehicleLabelText, VerticalDropLine, VerticalDropLineCoordinates, VerticalDropLineMaterial, VerticalDropLineShape, WaypointLine, WaypointLineCoordinates, WaypointLineShape, WaypointOI.instance, WaypointPI.instance, WaypointTrackMaterial, WaypointTrackScript

Index for ProtoDeclare definition: WaypointInterpolator
-->
 16       <Scene>
 17            <WorldInfo title='WaypointInterpolatorPrototype.x3d'/>
 18            <ProtoDeclare name='WaypointInterpolatorappinfo='Reads waypoints and legSpeeds/legDurations/defaultSpeed to provide a customizable position/orientation interpolator.'>
 19                 <ProtoInterface>
 20                      <field name='descriptiontype='SFStringaccessType='initializeOnly'
                     appinfo='Short description of what is animated by this WaypointInterpolator.'/>
 21                      <field name='waypointstype='MFVec3fvalue='0 0 0 0 0 0accessType='initializeOnly'
                     appinfo='Waypoints being traversed with interpolation of intermediate positions and orientations.'/>
 22                      <field name='add_waypointtype='SFVec3faccessType='inputOnly'
                     appinfo='Add another single waypoint to array of waypoints recalculate interpolator values.'/>
 23                      <field name='set_waypointstype='MFVec3faccessType='inputOnly'
                     appinfo='Replace all waypoints recalculate interpolator values.'/>
 24                      <field name='pitchUpDownForVerticalWaypointstype='SFBoolvalue='falseaccessType='initializeOnly'
                     appinfo='Whether to pitch child geometry (such as a vehicle) up or down to match vertical slope'/>
 25                      <!-- Priority of use: legSpeeds (m/sec), legDurations (seconds), defaultSpeed (m/sec) -->
 26                      <field name='legSpeedstype='MFFloataccessType='initializeOnly'
                     appinfo='Units m/sec. If used, array lengths for legSpeeds and legDurations must be one less than number of waypoints.'>
 27                           <!-- default initialization is empty array [] -->
 28                      </field>
 29                      <field name='legDurationstype='MFTimeaccessType='initializeOnly'
                     appinfo='Units in seconds. If used, array lengths for legSpeeds and legDurations must be one less than number of waypoints.'>
 30                           <!-- default initialization is empty array [] -->
 31                      </field>
 32                      <field name='defaultSpeedtype='SFFloatvalue='1accessType='initializeOnly'
                     appinfo='Units m/sec.'/>
 33                      <field name='turningRatetype='SFFloatvalue='90accessType='initializeOnly'
                     appinfo='turningRate (degrees/second) also determines standoff distance prior to waypoint where turn commences. If 0 turns are instantaneous.'/>
 34                      <field name='totalDurationtype='SFTimeaccessType='outputOnly'
                     appinfo='Output calculation summing all leg durations, useful for setting TimeSensor cycleInterval. Units in seconds.'/>
 35                      <!-- interpolation fields -->
 36                      <field name='set_fractiontype='SFFloataccessType='inputOnly'
                     appinfo='exposed PositionInterpolator and OrientationInterpolator setting'/>
 37                      <field name='position_changedtype='SFVec3faccessType='outputOnly'
                     appinfo='exposed PositionInterpolator setting'/>
 38                      <field name='orientation_changedtype='SFRotationaccessType='outputOnly'
                     appinfo='exposed OrientationInterpolator setting'/>
 39                      <!-- display-related fields -->
 40                      <field name='lineColortype='SFColorvalue='0.6 0.6 0.6accessType='inputOutput'
                     appinfo='default color for non-active line segments'/>
 41                      <field name='highlightSegmentColortype='SFColorvalue='0.3 0.3 1accessType='inputOutput'
                     appinfo='active segment highlight color'/>
 42                      <field name='transparencytype='SFFloatvalue='0accessType='inputOutput'
                     appinfo='1.0 is completely transparent, 0.0 is completely opaque.'/>
 43                      <field name='labelDisplayModetype='SFStringvalue='waypointsaccessType='initializeOnly'
                     appinfo='allowed values: none; waypoints (produce labels at each waypoint); or interpolation (produce single moving label at interpolator time course speed location)'/>
 44                      <field name='heightLabeltype='SFStringvalue='altitudeaccessType='initializeOnly'
                     appinfo='allowed values: altitude depth (negate Y value) none'/>
 45                      <field name='labelOffsettype='SFVec3fvalue='0 -1 0accessType='initializeOnly'
                     appinfo='heightLabel relative location'/>
 46                      <field name='labelFontSizetype='SFFloatvalue='1accessType='initializeOnly'
                     appinfo='heightLabel text size'/>
 47                      <field name='labelColortype='SFColorvalue='0.8 0.8 0.8accessType='initializeOnly'
                     appinfo='heightLabel text color'/>
 48                      <field name='traceEnabledtype='SFBoolvalue='falseaccessType='initializeOnly'
                     appinfo='enable console output to trace script computations and prototype progress'/>
 49                      <field name='outputInitializationComputationstype='SFBoolvalue='trueaccessType='initializeOnly'
                     appinfo='Output the number of waypoints totalDistance and totalDuration to console upon initialization'/>
 50                      <field name='verticalDropLineColortype='SFColorvalue='0.4 0.4 0.4accessType='inputOutput'
                     appinfo='default color for vertical drop-line segments'/>
 51                      <field name='verticalDropLineTransparencytype='SFFloatvalue='1accessType='inputOutput'
                     appinfo='1.0 is completely transparent, 0.0 is completely opaque.'/>
 52                 </ProtoInterface>
 53                 <ProtoBody>
 54                      <!-- 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. -->
 55                      <Group>
 56                           <!-- 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. -->
 57 
                         <!-- PositionInterpolator WaypointPI.instance is a DEF node that has 1 USE node: USE_1
                         <!-- ROUTE information for WaypointPI.instance node:  [from WaypointTrackScript.finalPositionKey to key ] [from WaypointTrackScript.finalPositionKeyValueArray to keyValue ] [from value_changed to MovingVehicleLabel.translation ] -->
                         <PositionInterpolator DEF='WaypointPI.instancekey='0 0.5 1keyValue='0 0 0 1 1 1 2 2 2'>
 58                                <IS>
 59                                     <connect nodeField='set_fractionprotoField='set_fraction'/>
 60                                     <connect nodeField='value_changedprotoField='position_changed'/>
 61                                </IS>
 62                           </PositionInterpolator>
 63 
                         <!-- OrientationInterpolator WaypointOI.instance is a DEF node that has 1 USE node: USE_1
                         <!-- ROUTE information for WaypointOI.instance node:  [from value_changed to MovingVehicleLabel.rotation ] -->
                         <OrientationInterpolator DEF='WaypointOI.instance'>
 64                                <IS>
 65                                     <connect nodeField='set_fractionprotoField='set_fraction'/>
 66                                     <connect nodeField='value_changedprotoField='orientation_changed'/>
 67                                </IS>
 68                           </OrientationInterpolator>
 69 
                         <!-- Group CoordinateLabelsAndViewpointsGroup is a DEF node that has 1 USE node: USE_1 -->
                         <Group DEF='CoordinateLabelsAndViewpointsGroup'/>
 70 
 71                                <field name='descriptiontype='SFStringaccessType='initializeOnly'/>
 72                                <field name='waypointstype='MFVec3faccessType='initializeOnly'/>
 73                                <field name='add_waypointtype='SFVec3faccessType='inputOnly'/>
 74                                <field name='set_waypointstype='MFVec3faccessType='inputOnly'/>
 75                                <field name='pitchUpDownForVerticalWaypointstype='SFBoolaccessType='initializeOnly'/>
 76                                <field name='legSpeedstype='MFFloataccessType='initializeOnly'/>
 77                                <field name='legDurationstype='MFTimeaccessType='initializeOnly'/>
 78                                <field name='defaultSpeedtype='SFFloataccessType='initializeOnly'/>
 79                                <field name='turningRatetype='SFFloataccessType='initializeOnly'/>
 80                                <field name='totalDurationtype='SFTimeaccessType='outputOnly'/>
 81                                <field name='WaypointPItype='SFNodeaccessType='initializeOnly'>
 82                                     <PositionInterpolator USE='WaypointPI.instance'/>
 83                                </field>
 84                                <field name='WaypointOItype='SFNodeaccessType='initializeOnly'>
 85                                     <OrientationInterpolator USE='WaypointOI.instance'/>
 86                                </field>
 87                                <field name='pointIndicestype='MFInt32accessType='outputOnly'/>
 88                                <field name='OutputLabelsGrouptype='SFNodeaccessType='initializeOnly'>
 89                                     <Group USE='CoordinateLabelsAndViewpointsGroup'/>
 90                                </field>
 91                                <field name='set_fractiontype='SFFloataccessType='inputOnly'/>
 92                                <field name='highlightCoordinatestype='MFVec3faccessType='outputOnly'
                               appinfo='Initialized to (0 0 0 0 0 0)'/>
 93                                <field name='heightLabeltype='SFStringaccessType='initializeOnly'/>
 94                                <field name='labelDisplayModetype='SFStringaccessType='initializeOnly'/>
 95                                <field name='labelOffsettype='SFVec3faccessType='initializeOnly'/>
 96                                <field name='labelFontSizetype='SFFloataccessType='initializeOnly'/>
 97                                <field name='labelColortype='SFColoraccessType='initializeOnly'/>
 98                                <field name='labelInterpolationtype='MFStringaccessType='outputOnly'/>
 99                                <field name='traceEnabledtype='SFBoolaccessType='initializeOnly'/>
100                                <field name='outputInitializationComputationstype='SFBoolaccessType='initializeOnly'
                               appinfo='Output the number of waypoints totalDistance and totalDuration to console upon initialization'/>
101                                <!-- local variables (do not use internal var declarations) for persistence -->
102                                <field name='scriptErrortype='SFBoolvalue='falseaccessType='initializeOnly'
                               appinfo='whether or not an error was detected during script processing.'/>
103                                <field name='previousFractionIndextype='SFInt32value='0accessType='initializeOnly'
                               appinfo='retain state information while constructing fraction array'/>
104                                <field name='depthStringtype='SFStringaccessType='initializeOnly'
                               appinfo='label'/>
105                                <field name='whichRotationVersiontype='SFStringaccessType='initializeOnly'
                               appinfo='label'/>
106                                <field name='verticalDropLineIndicestype='MFInt32accessType='outputOnly'/>
107                                <field name='verticalDropLinePointstype='MFVec3faccessType='outputOnly'/>
108                                <field name='positionKeytype='MFFloatvalue='0accessType='initializeOnly'/>
109                                <field name='positionKeyValueArraytype='MFVec3faccessType='initializeOnly'/>
110                                <field name='finalPositionKeytype='MFFloataccessType='outputOnly'/>
111                                <field name='finalPositionKeyValueArraytype='MFVec3faccessType='outputOnly'/>
112                                <field name='distancestype='MFFloataccessType='initializeOnly'/>
113                                <field name='pointIndicesAccumulatortype='MFInt32accessType='initializeOnly'/>
114                                <field name='verticalDropLineIndicesAccumulatortype='MFInt32accessType='initializeOnly'/>
115                                <field name='verticalDropLinePointsAccumulatortype='MFVec3faccessType='initializeOnly'/>
116                                <field name='totalDistancetype='SFFloatvalue='0accessType='initializeOnly'/>
117                                <field name='orientationstype='MFRotationaccessType='initializeOnly'/>
118                                <field name='dxtype='SFFloatvalue='0accessType='initializeOnly'/>
119                                <field name='dytype='SFFloatvalue='0accessType='initializeOnly'/>
120                                <field name='dztype='SFFloatvalue='0accessType='initializeOnly'/>
121                                <field name='legDistancetype='SFFloatvalue='0accessType='initializeOnly'/>
122                                <field name='headingtype='SFFloatvalue='0accessType='initializeOnly'/>
123                                <field name='pitchAngletype='SFFloatvalue='0accessType='initializeOnly'/>
124                                <field name='orientationKeytype='MFFloataccessType='initializeOnly'/>
125                                <field name='newKeytype='MFFloataccessType='initializeOnly'/>
126                                <field name='newKeyValuetype='MFRotationaccessType='initializeOnly'/>
127                                <field name='outputChildtype='MFNodeaccessType='initializeOnly'>
128                                     <!-- NULL -->
129                                </field>
130                                <field name='rotatedVectortype='SFVec3fvalue='0 0 0accessType='initializeOnly'/>
131                                <IS>
132                                     <connect nodeField='descriptionprotoField='description'/>
133                                     <connect nodeField='waypointsprotoField='waypoints'/>
134                                     <connect nodeField='add_waypointprotoField='add_waypoint'/>
135                                     <connect nodeField='set_waypointsprotoField='set_waypoints'/>
136                                     <connect nodeField='pitchUpDownForVerticalWaypointsprotoField='pitchUpDownForVerticalWaypoints'/>
137                                     <connect nodeField='legSpeedsprotoField='legSpeeds'/>
138                                     <connect nodeField='legDurationsprotoField='legDurations'/>
139                                     <connect nodeField='defaultSpeedprotoField='defaultSpeed'/>
140                                     <connect nodeField='turningRateprotoField='turningRate'/>
141                                     <connect nodeField='totalDurationprotoField='totalDuration'/>
142                                     <connect nodeField='set_fractionprotoField='set_fraction'/>
143                                     <connect nodeField='heightLabelprotoField='heightLabel'/>
144                                     <connect nodeField='labelDisplayModeprotoField='labelDisplayMode'/>
145                                     <connect nodeField='labelOffsetprotoField='labelOffset'/>
146                                     <connect nodeField='labelFontSizeprotoField='labelFontSize'/>
147                                     <connect nodeField='labelColorprotoField='labelColor'/>
148                                     <connect nodeField='traceEnabledprotoField='traceEnabled'/>
149                                     <connect nodeField='outputInitializationComputationsprotoField='outputInitializationComputations'/>
150                                </IS>
  <![CDATA[
            
ecmascript:

function tracePrint (outputValue)
{
	if (traceEnabled) forcePrint (outputValue);
}
function forcePrint (outputValue)
{
	// 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)
{
	return Math.sqrt (
		(p2.x - p1.x) * (p2.x - p1.x) +
		(p2.y - p1.y) * (p2.y - p1.y) +
		(p2.z - p1.z) * (p2.z - p1.z));
}

function normalize2Pi (angle)
{
	twoPi = 2 * Math.PI;
	x = angle;
	while (x >= twoPi) x = x - twoPi;
	while (x <  0)     x = x + twoPi;
	return x;
}

function normalizePi (angle)
{
	twoPi = 2 * Math.PI;
	x = angle;
	while (x >=  Math.PI) x = x - twoPi;
	while (x <  -Math.PI) x = x + twoPi;
	return x;
}

function degrees (angle)
{
	return angle * 180.0 / Math.PI;
}

function radians (theta)
{
	return theta * Math.PI / 180.0;
}

function initialize ()
{
	saveTrace   = traceEnabled;
        traceEnabled = true;                     // debug use
        outputInitializationComputations = true; // debug use
        
	scriptError = false;
	traceEnabled= false; // set traceEnabled=true for selective debug during initialization only

	forcePrint ('initializing new ' + waypoints.length + '-point WaypointInterpolator ' + description);
	tracePrint ('Browser.name       =' + Browser.name);
	tracePrint ('WaypointPI.key     =' + WaypointPI.key.toString());
	tracePrint ('WaypointPI.keyValue=' + WaypointPI.keyValue.toString());
        
// TODO forcePrint ('Returning, initialization trace complete.');
// TODO return;

	previousFractionIndex = -1;
	tracePrint ('waypoints       =' + waypoints.toString());
	if ((waypoints.length == 2) &&
	    (waypoints[0].x == 0) && (waypoints[0].y == 0) && (waypoints[0].z == 0) &&
	    (waypoints[1].x == 0) && (waypoints[1].y == 0) && (waypoints[1].z == 0))
	{
		tracePrint ('[default waypoints, no action needed]');
		return;
	}
	if (waypoints.length < 2)
	{
		forcePrint ('*** error: insufficient waypoints, WaypointInterpolator ignored ***');
		scriptError=true;
		return;
	}
	if (	heightLabel.toLowerCase()!='altitude' &&
		heightLabel.toLowerCase()!='depth' &&
		heightLabel.toLowerCase()!='none')
	{
		forcePrint ('*** error, heightLabel =' + heightLabel + ', allowed values (none, altitude, depth) ***');
		heightLabel ='none';
	}

	useDefaultSpeed = false; // initialize booleans
	useLegSpeeds    = false;
	useLegDurations = false;

	if ((legSpeeds.length == 0) && (legDurations.length == 0)) // use defaultSpeed
	{
		tracePrint ('defaultSpeed    =' + defaultSpeed.toString() + ' meters/second');
		if (defaultSpeed <= 0)
		{
			forcePrint ('*** error, defaultSpeed <= 0 ***');
			scriptError=true;
			return;
		}
		else
		{
			useDefaultSpeed = true;
			tracePrint ('useDefaultSpeed = true');
		}
	}
	else if (legSpeeds.length > 0)
	{
		tracePrint ('legSpeeds       =' + legSpeeds.toString() + ' meters/second');
		if (legSpeeds.length != waypoints.length - 1)
		{
			forcePrint ('*** error, legSpeeds.length (' + legSpeeds.length + ' must be one less than waypoints.length (' + waypoints.length + ') ***');
			scriptError=true;
			return;
		}
		for (i = 0; i < legSpeeds.length; i++)
		{
			if (legSpeeds[i] <= 0)
			{
				forcePrint ('*** error, legSpeeds[' + i + '] zero or negative ***');
				scriptError=true;
				return;
			}
		}
		if (legDurations.length > 0)
			tracePrint ('warning: legDurations ignored, useLegSpeeds=true');
		else	tracePrint ('useLegSpeeds=true');
		useLegSpeeds=true;
	}
	else // legDurations.length > 0
	{
                // Xj3D X3DFieldreader.java line 1920: parse error fails to read MFTime values; PositionInterpolator.key destination uses MFFloat anyway
		forcePrint ('legDurations    =' + legDurations.toString() + ' seconds');
		if ((legDurations.length != 1) && (legDurations.length != waypoints.length - 1))
		{
			forcePrint ('*** error, legDurations.length must be one less than waypoints.length ***');
			scriptError=true;
			return;
		}
		for (i = 0; i < legDurations.length; i++)
		{
			if (legDurations[i] < 0)
			{
				legDurations[i] = Math.abs(legDurations[i]);
				forcePrint ('*** error, legDurations[' + i + ']= -' + legDurations[i]
					+ ' is less than zero ***');
				scriptError=true;
				return;
			}
			else if (legDurations[i] == 0)
			{
				forcePrint ('*** Warning, zero value encountered/ignored: ' +
				'legDurations[' + i + '] =' + legDurations[i]);
			}
		}
		tracePrint ('useLegDurations=true');
		useLegDurations=true;
	}
	positionKeyValueArray = waypoints;

	for (i = 0; i < (waypoints.length - 1); i++)
	{
		distances[i] = Math.sqrt (
			(waypoints[i+1].x - waypoints[i].x) * (waypoints[i+1].x - waypoints[i].x) +
			(waypoints[i+1].y - waypoints[i].y) * (waypoints[i+1].y - waypoints[i].y) +
			(waypoints[i+1].z - waypoints[i].z) * (waypoints[i+1].z - waypoints[i].z));
		totalDistance += distances[i];
		pointIndicesAccumulator[i]= i;
	}
	forcePrint ('distances       =' + distances.toString() + ' meters');
	forcePrint ('totalDistance   =' + Math.round (totalDistance * 10)/10 + ' meters');
	pointIndicesAccumulator[waypoints.length - 1]= waypoints.length - 1;
	pointIndicesAccumulator[waypoints.length]    = -1;

	for (i = 0; i < (waypoints.length ); i++)
	{
		verticalDropLineIndicesAccumulator[3*i]    = 2*i;
		verticalDropLineIndicesAccumulator[3*i+ 1] = 2*i + 1;
		verticalDropLineIndicesAccumulator[3*i+ 2] = -1;
		verticalDropLinePointsAccumulator[2*i]     = waypoints[i];
		verticalDropLinePointsAccumulator[2*i+1]   = new SFVec3f(waypoints[i].x, 0.0, waypoints[i].z);
	}
	pointIndices = pointIndicesAccumulator;
	tracePrint ('pointIndices    =' + pointIndices.toString());
	verticalDropLineIndices = verticalDropLineIndicesAccumulator;
	tracePrint ('verticalDropLineIndices  =' + verticalDropLineIndices.toString());
	verticalDropLinePoints = verticalDropLinePointsAccumulator;
	tracePrint ('verticalDropLinePoints =' + verticalDropLinePoints.toString());

	totalDurationAccumulator = 0.0;
	for (i = 0; i < (waypoints.length - 1); i++)
	{
		if      (useDefaultSpeed)
		{
			totalDurationAccumulator += distances[i] / defaultSpeed;
		}
		else if (useLegSpeeds)
		{
			totalDurationAccumulator += distances[i] / legSpeeds[i];
		}
		else //  useLegDurations
		{
			totalDurationAccumulator += legDurations[i];
		//	forcePrint ('legDurations[' + i + ']=' + legDurations[i]);
		//	forcePrint ('totalDurationAccumulator=' + totalDurationAccumulator + ' seconds');
		}
	}
	totalDuration = totalDurationAccumulator; // send SFTime eventOut
	hours   = Math.floor  (totalDuration / 3600.0); // % is modulo operator, provides remainder
	minutes = Math.floor ((totalDuration - hours * 3600) / 60.0);
	seconds = Math.round ((totalDuration - hours * 3600 - minutes * 60) * 10) / 10; // 0.1 sec resolution
	if (totalDuration <= 0)
	{
		forcePrint ('*** error:  totalDuration=' + totalDuration + ' seconds (' +
	  	  hours + ' hours,' + minutes + ' minutes,' + seconds + ' seconds)');
		scriptError=true;
		return;
	}
	else if (outputInitializationComputations)
	    	 forcePrint ('totalDuration   =' + Math.round (totalDuration * 10)/10 + ' seconds (' +
	  	 		hours + ' hours,' + minutes + ' minutes,' + seconds + ' seconds)');

	positionKey[0] = 0;
	for (i = 1; i < waypoints.length; i++)
	{
		if      (useDefaultSpeed)
		{
			positionKey[i] = i / (waypoints.length - 1); // simple fraction
		}
		else if (useLegSpeeds)
		{
			positionKey[i] = ((distances[i-1] / legSpeeds[i-1]) / totalDuration) + positionKey[i-1];
		}
		else //  useLegDurations
		{
			positionKey[i] = (legDurations[i-1] / totalDuration) + positionKey[i-1];
		}
	}
	positionKey[waypoints.length-1] = 1.0; // avoid roundup greater than 1.0

	tracePrint ('positionKey.length           =' + positionKey.length);
	tracePrint ('positionKey                  =' + positionKey.toString());
	tracePrint ('positionKeyValueArray.length =' + positionKeyValueArray.length);
	tracePrint ('positionKeyValueArray        =' + positionKeyValueArray.toString());

	// directly set event
	WaypointPI.key      = positionKey;
	WaypointPI.keyValue = positionKeyValueArray;
	tracePrint ('WaypointPI.key               =' + WaypointPI.key.toString());
	tracePrint ('WaypointPI.keyValue          =' + WaypointPI.keyValue.toString());

	// ROUTE outputOnly event
 	finalPositionKey           = positionKey;
	finalPositionKeyValueArray = positionKeyValueArray;
	tracePrint ('finalPositionKey             =' + finalPositionKey.toString());
	tracePrint ('finalPositionKeyValueArray   =' + finalPositionKeyValueArray.toString());
	tracePrint ('WaypointPI.key               =' + WaypointPI.key.toString());
	tracePrint ('WaypointPI.keyValue          =' + WaypointPI.keyValue.toString());

	tracePrint ('pitchUpDownForVerticalWaypoints=' + pitchUpDownForVerticalWaypoints);

	// different approaches to orientation calculations
	whichRotationVersion ='FirstHeadingThenPitchStayVertical';
				//'IndependentLegOrientations';
				//'RelativeLegOrientations';
				//'FirstHeadingThenPitchStayVertical';
	tracePrint ('whichRotationVersion=' + whichRotationVersion);
	// SFRotation constructor for two Vector3Arrays returns rotation from first to second
	// default body axis is along X axis
        // TODO avoid changing value if normalized vector has length 0 (meaning no direction change)
        orientations = new MFRotation();
	orientations[0] = new SFRotation (new SFVec3f (1, 0, 0),
		waypoints[1].subtract(waypoints[0]).normalize()); // first leg
	dx = waypoints[1].x - waypoints[0].x;
	dy = waypoints[1].y - waypoints[0].y;
	dz = waypoints[1].z - waypoints[0].z;
	legDistance   = Math.sqrt (dx*dx + dy*dy + dz*dz);
	levelDistance = Math.sqrt (dx*dx + dz*dz);
	tracePrint ('dx=' + dx + ', dy=' + dy + ', dz=' + dz + ', legDistance=' + legDistance + ', levelDistance=' + levelDistance);
	tracePrint ('orientations[0] =' + orientations[0].toString());

	for (i = 1; i < (waypoints.length - 1); i++) // compute orientations array
	{
		dx = waypoints[i+1].x - waypoints[i].x;
		dy = waypoints[i+1].y - waypoints[i].y;
		dz = waypoints[i+1].z - waypoints[i].z;
		legDistance   = Math.sqrt (dx*dx + dy*dy + dz*dz);
		levelDistance = Math.sqrt (dx*dx + dz*dz);
		tracePrint ('dx=' + dx + ', dy=' + dy + ', dz=' + dz +
		', legDistance='   + Math.round (  legDistance*10)/10 +
		', levelDistance=' + Math.round (levelDistance*10)/10);

//		tracePrint ('waypoints[i  ].subtract(waypoints[i-1]) =' + waypoints[i  ].subtract(waypoints[i-1]).toString());
//		tracePrint ('waypoints[i+1].subtract(waypoints[i])   =' + waypoints[i+1].subtract(waypoints[i]).toString());
//		tracePrint ('dot product=' + waypoints[i+1].subtract(waypoints[i]).normalize().
//					 dot(waypoints[i].subtract(waypoints[i-1]).normalize()).toString());

		if (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	orientations[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');
                    }
		}
                else if      (Math.abs(legDistance)   <= 0.00001)
                {
                    tracePrint ('coincident waypoints, set orientations[' + i + '] = orientations[' + i-1 + ']');
                    orientations[i] = orientations[i-1];
                }
		else 
                {
                        forcePrint ('*** unexpected case trapped, set orientations[' + i + '] = orientations[' + i-1 + ']');
                        orientations[i] = orientations[i-1];
                }
		tracePrint ('orientations[' + i + '] =' + orientations[i].toString());
	}
//	traceEnabled = true; // debug

	// full array trace
	tracePrint ('orientations   =' + orientations.toString());

	if (orientations.length != (waypoints.length - 1))
	{
		forcePrint ('** computation error: orientations.length=' + orientations.length + ' mismatch with waypoints.length=' + waypoints.length);
	}

	if (turningRate < 0)
	{
		forcePrint ('** error:  negative value for turningRate illegal, making turningRate positive');
		turningRate = -turningRate;
	}
	tracePrint ('turningRate     =' + turningRate + ' degrees/second');

	orientationKey = new MFFloat ();
	orientationKey[0] = 0;
	for (i = 1; i < (waypoints.length-1); i++)
	{
		deltaAngle = orientations[i].multiply(orientations[i-1].inverse()).angle;
		deltaAngle = normalizePi (deltaAngle);
		turnTime = Math.abs (deltaAngle) / radians (turningRate);
		tracePrint ('deltaAngle[' + i + ']=' + degrees (deltaAngle) + ' degrees, turnTime=' + turnTime);

		precedingLegDuration = (positionKey[i]   - positionKey[i-1]) * totalDuration;
		followingLegDuration = (positionKey[i+1] - positionKey[i]  ) * totalDuration;
		// turn for no more than 1/3 of preceding or following leg durations, respectively
		precedingTurnKeyOffset = Math.min (turnTime/2, precedingLegDuration/3) / totalDuration;
		followingTurnKeyOffset = Math.min (turnTime/2, followingLegDuration/3) / totalDuration;
		tracePrint ('precedingTurnKeyOffset=' + (precedingTurnKeyOffset * totalDuration) + ' seconds');
		tracePrint ('followingTurnKeyOffset=' + (followingTurnKeyOffset * totalDuration) + ' seconds');

		orientationKey[3*i - 2] = positionKey[i] - precedingTurnKeyOffset;
		orientationKey[3*i - 1] = positionKey[i];
		orientationKey[3*i]     = positionKey[i] + followingTurnKeyOffset;
		if (orientationKey[3*i - 2] <= positionKey[i-1]) // interpolate preceding key if needed
		{
			orientationKey[3*i - 2] = positionKey[i-1] + ((positionKey[i] - positionKey[i-1]) * 2 / 3);
		}
		if (orientationKey[3*i] >= positionKey[i+1]) // interpolate following key if needed
		{
			orientationKey[3*i]     = positionKey[i] + ((positionKey[i+1] - positionKey[i])   * 1 / 3);
		}
		if ((orientationKey[3*i - 2] > orientationKey[3*i - 1]) || (orientationKey[3*i - 1] > orientationKey[3*i]))
		{
			forcePrint ('** error computing orientationKey [' + (3*i - 2) + '..' + (3*i) + ']');
		}
	}
	orientationKey[3*(waypoints.length-1)-2] = 1.0; // avoid roundup greater than 1
	tracePrint ('orientationKey.length =' + orientationKey.length);
	tracePrint ('orientationKey        =' + orientationKey.toString());

	//
	for (i = 2; i < (orientationKey.length-1); i++)
	{
	   if (orientationKey [i-1] > orientationKey [i])
		forcePrint ('*** error,' +
		'orientationKey [' + (i-1) + ']=' + orientationKey [i-1].toString() + ',' +
		'orientationKey [' + (i) + ']='   + orientationKey [i].toString() +
		' values are not monotonically increasing ***');
	   if ((orientationKey [i] < 0) || (orientationKey [i] > 1))
		forcePrint ('*** error, orientationKey [' + i + ']=' + orientationKey [i].toString() +
		' value is out of range [0..1] ***');
	}
	tracePrint ('check orientationKey complete, dynamically building orientationKeyValueArray next');
	orientationKeyValueArray = new MFRotation ();
	orientationKeyValueArray[0] = orientations[0];
	orientationKeyValueArray[1] = orientations[0];
	for (i = 1; i < (waypoints.length - 1); i++)
	{
	//	spherical linear interpolation (slerp) 0.5 interpolates halfway between adjacent orientations
		orientationKeyValueArray[3*i - 1] = orientations[i-1].slerp(orientations[i], 0.5);
		orientationKeyValueArray[3*i]     = orientations[i];
		orientationKeyValueArray[3*i + 1] = orientations[i]; // straight-line track, same orientation
	}
	tracePrint ('orientationKeyValueArray.length =' + orientationKeyValueArray.length);
	tracePrint ('orientationKeyValueArray        =' + orientationKeyValueArray.toString());

	// eliminate orientationKey triplicates (smaller arrays overcome CosmoPlayer overflow bug)
	newKey      = new MFFloat ();
	newKey      [0] = orientationKey [0];
	newKey      [1] = orientationKey [1];
	newKeyValue = new MFRotation ();
	newKeyValue [0] = orientationKeyValueArray [0];
	newKeyValue [1] = orientationKeyValueArray [1];
	index = 2; // keep first two orientations identical, index is for next value
        for (i = 2; i < (orientationKeyValueArray.length-3) ; i++)
	{
	   dotProductBA      =  orientationKeyValueArray [i-1].getAxis().dot(orientationKeyValueArray [i-2].getAxis());
	   dotProductCB      =  orientationKeyValueArray [i].getAxis().dot(orientationKeyValueArray [i-1].getAxis());
	   angleDifferenceBA = normalizePi(
	   	normalize2Pi (orientationKeyValueArray [i-1].angle) -
	   	normalize2Pi (orientationKeyValueArray [i-2].angle)) * 180 / Math.PI;
	   angleDifferenceCB = normalizePi(
	   	normalize2Pi (orientationKeyValueArray [i].angle) -
	   	normalize2Pi (orientationKeyValueArray [i-1].angle)) * 180 / Math.PI;

	   if (i < 10) // too many outputs clobbers the trace console
	   {
 	     tracePrint ('orientationKeyValueArray [' + (i-2) + ']=' + orientationKeyValueArray [i-2].toString());
 	     tracePrint ('orientationKeyValueArray [' + (i-1) + ']=' + orientationKeyValueArray [i-1].toString());
 	     tracePrint ('orientationKeyValueArray [' + (i  ) + ']=' + orientationKeyValueArray [i  ].toString());
	     tracePrint ('dotProductBA     =' + dotProductBA +     ', dotProductCB     =' + dotProductCB);
	     tracePrint ('angleDifferenceBA=' + angleDifferenceBA + ', angleDifferenceBC=' + angleDifferenceCB + ' degrees');
	   }

//         // depth check also needed!  but positionKey is already optimized/compressed, so how to check?
//	   if ((Math.abs (dotProductCB - 1)  < 0.01) &&
//	       (Math.abs (dotProductBA - 1)  < 0.01) &&
//	       (Math.abs (angleDifferenceCB) < 1.0 ) &&
//	       (Math.abs (angleDifferenceBA) < 1.0 ))  // degrees
//	   {
//		// replace key time with later value
//		tracePrint ('... matching this orientationKey time,' +
//		'updating key' + newKey [index-1] + ' to' + orientationKey [i]);
//		newKey      [index-1] = orientationKey [i];
//		// don't update orientation in order to avoid creeping matches
//	   }
//	   else
//	   {
		newKey      [index] = orientationKey [i];
		newKeyValue [index] = orientationKeyValueArray [i];
		index ++;
		tracePrint ('...  keeping this orientationKeyValue');
//	   }
	   if (newKey [index-2] > newKey [index-1])
		forcePrint ('*** error,' +
		'newKey [' + (index-2) + ']=' + newKey [index-2].toString() + ',' +
		'newKey [' + (index-1) + ']=' + newKey [index-1].toString() +
		' values are not monotonically increasing ***');
	   if ((newKey [index-1] < 0) || (newKey [index-1] > 1))
		forcePrint ('*** error, newKey [' + (index-1) + ']=' + newKey [index-1].toString() +
		' value is out of range [0..1] ***');
	}
	newKey      [index] = orientationKey [orientationKeyValueArray.length-2]; // match finals values
	newKeyValue [index] = orientationKeyValueArray [orientationKeyValueArray.length-2];
	index++;
	newKey      [index] = orientationKey [orientationKeyValueArray.length-1]; // match finals values
	newKeyValue [index] = orientationKeyValueArray [orientationKeyValueArray.length-1];
	tracePrint ('orientation newKey.length      =' + newKey.length);
	tracePrint ('orientation newKey             =' + newKey.toString());
	tracePrint ('orientation newKeyValue.length =' + newKeyValue.length);
	tracePrint ('orientation newKeyValue        =' + newKeyValue.toString());

	WaypointOI.key      = newKey;
	WaypointOI.keyValue = newKeyValue;
	tracePrint ('WaypointOI.key                 =' + WaypointOI.key.toString());
	tracePrint ('WaypointOI.keyValue            =' + WaypointOI.keyValue.toString());

	tracePrint ('labelDisplayMode=' + labelDisplayMode);
	if (labelDisplayMode.toLowerCase() =='waypoints')
	{
	  // create text labels for each waypoint
	  outputChild = new MFNode ();
	  outputVrmlString ='';
	  for (i = 0; i < waypoints.length; i++)
	  {
		textOffset = waypoints[i].add(labelOffset);
		if ((i == waypoints.length-1) && (waypoints[i].x == waypoints[0].x) &&
			(waypoints[i].y == waypoints[0].y) && (waypoints[i].z == waypoints[0].z))
		    // double offset for endpoint when waypoints are a loop
		    textOffset = textOffset.subtract(new SFVec3f (0, 3 * labelFontSize, 0));
		hours   = Math.floor  (totalDuration * positionKey[i] / 3600.0); // % is modulo operator, provides remainder
		minutes = Math.floor ((totalDuration * positionKey[i] - hours * 3600.0) / 60.0);
		seconds = Math.round  (totalDuration * positionKey[i] - hours * 3600.0 - minutes * 60.0);
		while (minutes >= 60)
		{
			minutes -= 60;
			hours   += 1;
		}
		while (seconds >= 60)
		{
			seconds -= 60;
			minutes += 1;
		}
		if (hours   < 10) hours   ='0' + hours;
		if (minutes < 10) minutes ='0' + minutes;
		if (seconds < 10) seconds ='0' + seconds;
		locationX =  Math.round (waypoints[i].x);
		depth     = -Math.round (waypoints[i].y * 10) / 10;
		locationZ =  Math.round (waypoints[i].z);
		if      (heightLabel.toLowerCase()=='altitude')
			depthString = (-depth) + ' ';
		else if (heightLabel.toLowerCase()=='depth')
			depthString = depth + ' ';
		else if (heightLabel.toLowerCase()=='none')
			depthString =' ';
		else	depthString =' ';
		outputVrmlString +=
			 'Transform { translation' + textOffset + '\n'
			+ ' children LOD { range [' + 150 * labelFontSize + ' ]\n'
			+ '  level [\n'
			+ '   Billboard { axisOfRotation 0 1 0 \n'
			+ '    children Shape {\n'
			+ '	geometry Text {\n'
			+ '	   string [ \"' + hours + ':' + minutes + ':' + seconds + '\"\n'
			+ '	            \"' + locationX + ' ' + depthString +  locationZ + ' ' + '\" ]\n'
			+ '	   fontStyle DEF WPIFontStyle FontStyle {\n'
			+ '		size' + labelFontSize + '\n'
			+ '		justify [ \"MIDDLE\" \"MIDDLE\" ]\n'
			+ '	   }\n'
			+ '	}\n'
			+ '	appearance DEF WPIAppearance Appearance {\n'
			+ '	   material Material { diffuseColor' + labelColor + ' }\n'
			+ '	}\n'
			+ '    }\n'
			+ '   }\n'
			+ '  WorldInfo { } ]\n'
			+ ' }\n'
			+ '}\n';
	  }
	  tracePrint ('outputVrmlString=' + outputVrmlString);

	  outputChild = Browser.createVrmlFromString (outputVrmlString);
	  OutputLabelsGroup.addChildren = outputChild;

//	  tracePrint ('OutputLabelsGroup.children =');
//	  tracePrint (outputChild + '  ' + OutputLabelsGroup.children.toString());
	}
	else if (labelDisplayMode.toLowerCase() =='interpolation')
	{
		// updates occur when fraction changes
	}
	else if ((labelDisplayMode.toLowerCase() !='none') && (labelDisplayMode !=''))
	{
	  forcePrint ('*** illegal value labelDisplayMode=' + labelDisplayMode + ', ignored');
	}
        
	if (outputInitializationComputations)
        {
	     tracePrint ('initialization complete');
	     forcePrint ('=======================================');
        }
        traceEnabled = saveTrace;
        
} // end of initialize() method

function set_fraction (fractionValue, timeStamp)
{
	tracePrint ('fractionValue=' + fractionValue);
	tracePrint ('previousFractionIndex=' + previousFractionIndex);
	tracePrint ('WaypointPI.value_changed=' + WaypointPI.value_changed.toString());
	tracePrint ('WaypointOI.value_changed=' + WaypointOI.value_changed.toString());

	if (scriptError==true)
    {
        tracePrint ('scriptError==true, no response by set_fraction()');
        return;
    }
	//	tracePrint ('WaypointPI.key               =' + WaypointPI.key.toString());
	//	tracePrint ('WaypointPI.keyValue          =' + WaypointPI.keyValue.toString());

//	wide input range supported by interpolators,
//	usually no range check on fractionValue.
//	however WaypointInterpolator input range is [0..1], so check
	if ((fractionValue < 0) || (fractionValue > 1))
	{
		forcePrint ('*** error:  set_fraction=' + fractionValue + ' out of range [0..1], ignored');
		return;
	}

	if (previousFractionIndex == -1)
	{
		previousFractionIndex = 0; // start
		while (fractionValue >= positionKey[previousFractionIndex+1])
		{
			previousFractionIndex ++;
			if (previousFractionIndex >= waypoints.length - 2) break;
		}
		highlightCoordinates = new MFVec3f (waypoints[previousFractionIndex],
			waypoints[previousFractionIndex +1]);
		tracePrint ('highlightCoordinates=' + highlightCoordinates.toString());
	}
	else if (waypoints.length == 2)
	{
		// only one segment, no action required
	}
	else if (previousFractionIndex == waypoints.length - 2) // last leg
	{
	  if (fractionValue < positionKey[previousFractionIndex]) // looped
	  {
		previousFractionIndex = 0; // start
		while (fractionValue >= positionKey[previousFractionIndex+1])
		{
			previousFractionIndex ++;
			if (previousFractionIndex >= waypoints.length - 2) break;
		}
		highlightCoordinates = new MFVec3f (waypoints[previousFractionIndex],
			waypoints[previousFractionIndex +1]);
		tracePrint ('highlightCoordinates=' + highlightCoordinates.toString());
	  }
	}
	else if (fractionValue >= positionKey[previousFractionIndex+1])
	{
		previousFractionIndex++;
		while (fractionValue >= positionKey[previousFractionIndex+1])
		{
			previousFractionIndex ++;
			if (previousFractionIndex >= waypoints.length - 2) break;
		}
		if (previousFractionIndex > waypoints.length - 2) previousFractionIndex = 0;
		highlightCoordinates = new MFVec3f (
			waypoints[previousFractionIndex],
			waypoints[previousFractionIndex+1]);
		tracePrint ('highlightCoordinates=' + highlightCoordinates.toString());
	}
	// else previousFractionIndex ought to be OK

	if (labelDisplayMode =='interpolation')
	{
		hours   = Math.floor  (totalDuration * fractionValue / 3600.0); // % is modulo operator, provides remainder
		minutes = Math.floor ((totalDuration * fractionValue - hours * 3600) / 60.0);
		seconds = Math.round  (totalDuration * fractionValue - hours * 3600 - minutes * 60);
		while (minutes > 60)
		{
			minutes -= 60;
			hours   += 1;
		}
		while (seconds > 60)
		{
			seconds -= 60;
			minutes += 1;
		}
		if (hours   < 10) hours   ='0' + hours;
		if (minutes < 10) minutes ='0' + minutes;
		if (seconds < 10) seconds ='0' + seconds;

		// compute course and pitch
		currentAxis     = WaypointOI.value_changed.getAxis().normalize();
		currentRotation = WaypointOI.value_changed;
   //   forcePrint ('=====currentRotation=' + currentRotation.toString() + ', currentAxis=' + currentAxis.toString());

		rotatedVector = currentRotation.multVec (new SFVec3f (1, 0, 0)); // rotate x-centered body
		dx = rotatedVector.x;
		dy = rotatedVector.y;
		dz = rotatedVector.z;
		levelDistance = Math.sqrt (dx*dx + dz*dz);
		heading = Math.atan2 (dz, dx); // atan2 returns arctangent in any of 4 quadrants
		if (levelDistance > 0)
			pitchAngle =  Math.atan (dy / levelDistance); // negative angle should pitch down, note no negation
		else if (dy > 0)
			pitchAngle =  1.57;
		else    pitchAngle = -1.57;

	//	forcePrint ('rotatedVector=' + rotatedVector.toString());
	//	forcePrint ('heading=' + degrees(heading) + ', pitchAngle=' + degrees(pitchAngle));

		course = Math.round (normalize2Pi ( heading)    * 180 / Math.PI);
		pitch  = Math.round (normalizePi  ( pitchAngle) * 180 / Math.PI);
		// format angles in degrees
		if      (course <  10) course = '0' + '0' + course;
		else if (course < 100) course = '0' + course;

	//	tracePrint ('course=' + course + ', pitch=' + pitch);

		locationX =  Math.round (WaypointPI.value_changed.x);
		depth     = -Math.round (WaypointPI.value_changed.y * 10) / 10;
		locationZ =  Math.round (WaypointPI.value_changed.z);
		if      (heightLabel.toLowerCase()=='altitude')
			depthString =', altitude ' + (-depth) + 'm';
		else if (heightLabel.toLowerCase()=='depth')
			depthString =', depth '    + depth + 'm';
		else if (heightLabel.toLowerCase()=='none')
			depthString ='';
		else	depthString ='';
	  	labelInterpolation  = new MFString (
			description,
			(hours + ':' + minutes + ':' + seconds + ', course=' + course + ', pitch=' + pitch),
			('location=(' + locationX + ' ' + locationZ + depthString + ')'));
	//	tracePrint ('labelInterpolation=' + labelInterpolation);
	}
        tracePrint ('=====');
	return;
}

function add_waypoint (newWaypointsArray, timeStamp)
{
	// EcmaScript automatically increases array size
	// when setting an element one past final element
	waypoints[waypoints.length] = newWaypointsArray;

	// initialization code is complicated! so we won't try to shortcut/optimize it, instead just rerun it
	initialize ();
}

function set_waypoints (newWaypointsArray, timeStamp)
{
	waypoints = newWaypointsArray;
	initialize ();
}

          
]]>
152                           </Script>
153                           < ROUTE  fromNode='WaypointTrackScript' fromField='finalPositionKey' toNode='WaypointPI.instance' toField='key'/>
154                           < ROUTE  fromNode='WaypointTrackScript' fromField='finalPositionKeyValueArray' toNode='WaypointPI.instance' toField='keyValue'/>
155                           <!-- IndexedLineSet connects waypoints for easy visibility. Set transparency=1 to hide. -->
156                           <Shape DEF='VerticalDropLineShape'>
157 
                              <!-- ROUTE information for VerticalDropLine node:  [from WaypointTrackScript.verticalDropLineIndices to set_coordIndex ] -->
                              <IndexedLineSet DEF='VerticalDropLine'>
158 
                                   <!-- ROUTE information for VerticalDropLineCoordinates node:  [from WaypointTrackScript.verticalDropLinePoints to point ] -->
                                   <Coordinate DEF='VerticalDropLineCoordinates'/>
159                                </IndexedLineSet>
160                                <Appearance>
161                                     <Material DEF='VerticalDropLineMaterial'>
162                                          <IS>
163                                               <connect nodeField='emissiveColorprotoField='verticalDropLineColor'/>
164                                               <connect nodeField='transparencyprotoField='verticalDropLineTransparency'/>
165                                          </IS>
166                                     </Material>
167                                </Appearance>
168                           </Shape>
169                           < ROUTE  fromNode='WaypointTrackScript' fromField='verticalDropLineIndices' toNode='VerticalDropLine' toField='set_coordIndex'/>
170                           < ROUTE  fromNode='WaypointTrackScript' fromField='verticalDropLinePoints' toNode='VerticalDropLineCoordinates' toField='point'/>
171                           <Shape DEF='HighlightShape'>
172                                <IndexedLineSet DEF='HighlightSegmentcoordIndex='0 1 -1'>
173 
                                   <!-- ROUTE information for HighlightSegmentCoordinates node:  [from WaypointTrackScript.highlightCoordinates to point ] -->
                                   <Coordinate DEF='HighlightSegmentCoordinatespoint='0 0 0 0 0 0'/>
174                                </IndexedLineSet>
175                                <Appearance>
176                                     <Material DEF='HighlightSegmentMaterialdiffuseColor='0 0 0emissiveColor='0.2 0.2 0.2'>
177                                          <IS>
178                                               <connect nodeField='emissiveColorprotoField='highlightSegmentColor'/>
179                                               <connect nodeField='transparencyprotoField='transparency'/>
180                                          </IS>
181                                     </Material>
182                                </Appearance>
183                           </Shape>
184                           < ROUTE  fromNode='WaypointTrackScript' fromField='highlightCoordinates' toNode='HighlightSegmentCoordinates' toField='point'/>
185                           <Shape DEF='WaypointLineShape'>
186 
                              <!-- ROUTE information for WaypointLine node:  [from WaypointTrackScript.pointIndices to set_coordIndex ] -->
                              <IndexedLineSet DEF='WaypointLine'>
187                                     <Coordinate DEF='WaypointLineCoordinates'>
188                                          <IS>
189                                               <connect nodeField='pointprotoField='waypoints'/>
190                                          </IS>
191                                     </Coordinate>
192                                </IndexedLineSet>
193                                <Appearance>
194                                     <Material DEF='WaypointTrackMaterialemissiveColor='0.8 0.8 0.8'>
195                                          <IS>
196                                               <connect nodeField='emissiveColorprotoField='lineColor'/>
197                                               <connect nodeField='transparencyprotoField='transparency'/>
198                                          </IS>
199                                     </Material>
200                                </Appearance>
201                           </Shape>
202                           < ROUTE  fromNode='WaypointTrackScript' fromField='pointIndices' toNode='WaypointLine' toField='set_coordIndex'/>
203                           <!-- Draw highlight segment before and after waypoint lines in case of order dependency -->
204                           <!-- TODO!! throws Xj3D exception! <Shape USE='HighlightShape'/> -->
205 
                         <!-- ROUTE information for MovingVehicleLabel node:  [from WaypointPI.instance.value_changed to translation ] [from WaypointOI.instance.value_changed to rotation ] -->
                         <Transform DEF='MovingVehicleLabel'>
206                                <!-- no need to externally ROUTE position and orientation interpolator key/keyValue results, since prototype is using pass-by-reference node update -->
207                                <!-- Nevertheless, must ROUTE position and orientation interpolated text label -->
208                                < ROUTE  fromNode='WaypointPI.instance' fromField='value_changed' toNode='MovingVehicleLabel' toField='translation'/>
209                                < ROUTE  fromNode='WaypointOI.instance' fromField='value_changed' toNode='MovingVehicleLabel' toField='rotation'/>
210                                <Transform DEF='MovingVehicleLabelOffset'>
211                                     <IS>
212                                          <connect nodeField='translationprotoField='labelOffset'/>
213                                     </IS>
214                                     <Billboard>
215                                          <Shape>
216 
                                             <!-- ROUTE information for MovingVehicleLabelText node:  [from WaypointTrackScript.labelInterpolation to string ] -->
                                             <Text DEF='MovingVehicleLabelText'>
217                                                    <FontStyle DEF='MovingVehicleLabelFontjustify='"MIDDLE" "MIDDLE"'>
218                                                         <IS>
219                                                              <connect nodeField='sizeprotoField='labelFontSize'/>
220                                                         </IS>
221                                                    </FontStyle>
222                                               </Text>
223                                               <Appearance>
224                                                    <Material DEF='MovingVehicleLabelMaterial'>
225                                                         <IS>
226                                                              <connect nodeField='diffuseColorprotoField='labelColor'/>
227                                                         </IS>
228                                                    </Material>
229                                               </Appearance>
230                                          </Shape>
231                                          < ROUTE  fromNode='WaypointTrackScript' fromField='labelInterpolation' toNode='MovingVehicleLabelText' toField='string'/>
232                                     </Billboard>
233                                </Transform>
234                           </Transform>
235                      </Group>
236                 </ProtoBody>
237            </ProtoDeclare>
238            <!-- ====================================== -->
239            <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" '>
240                 <Shape>
241                      <Text string='"WaypointInterpolatorPrototype" "defines a prototype" "" "Click on this text to see" "WaypointInterpolatorExample" " scene"'>
242                           <FontStyle justify='"MIDDLE" "MIDDLE"'/>
243                      </Text>
244                      <Appearance>
245                           <Material diffuseColor='1 1 0.2'/>
246                      </Appearance>
247                 </Shape>
248                 <Shape>
249                      <Box size='12 6.0 0.001'/>
250                      <Appearance>
251                           <Material diffuseColor='1 1 1transparency='1'/>
252                      </Appearance>
253                 </Shape>
254            </Anchor>
255       </Scene>
256  </X3D>
<!--

<!--
Event Graph ROUTE Table shows event connections.
-->
<!-- to top DEF nodes index: CoordinateLabelsAndViewpointsGroup, HighlightSegment, HighlightSegmentCoordinates, HighlightSegmentMaterial, HighlightShape, MovingVehicleLabel, MovingVehicleLabelFont, MovingVehicleLabelMaterial, MovingVehicleLabelOffset, MovingVehicleLabelText, VerticalDropLine, VerticalDropLineCoordinates, VerticalDropLineMaterial, VerticalDropLineShape, WaypointLine, WaypointLineCoordinates, WaypointLineShape, WaypointOI.instance, WaypointPI.instance, WaypointTrackMaterial, WaypointTrackScript

Index for ProtoDeclare definition: WaypointInterpolator
-->
X3D Tooltips element index: Anchor, Appearance, Billboard, Box, connect, Coordinate, field, FontStyle, Group, head, IndexedLineSet, IS, Material, meta, OrientationInterpolator, PositionInterpolator, ProtoBody, ProtoDeclare, ProtoInterface, ROUTE, Scene, Script, Shape, Text, Transform, WorldInfo, X3D, plus documentation for accessType definitions, type definitions, XML data types, and field types

Event Graph ROUTE Table entries with 9 ROUTE connections total, showing X3D event-model relationships for this scene.

Each row shows an event cascade that may occur during a single timestamp interval between frame renderings, as part of the X3D execution model.

WaypointTrackScript
Script
finalPositionKey
MFFloat

ROUTE
event to
(1)
WaypointPI.instance
PositionInterpolator
key
MFFloat
then
 
 
 
WaypointPI.instance
PositionInterpolator
value_changed
SFVec3f

ROUTE
event to
(2)
MovingVehicleLabel
Transform
translation
SFVec3f
WaypointTrackScript
Script
finalPositionKeyValueArray
MFVec3f

ROUTE
event to
(1)
WaypointPI.instance
PositionInterpolator
keyValue
MFVec3f
then
 
 
 
WaypointPI.instance
PositionInterpolator
value_changed
SFVec3f

ROUTE
event to
(2)
MovingVehicleLabel
Transform
translation
SFVec3f
WaypointTrackScript
Script
verticalDropLineIndices
MFInt32

ROUTE
event to
(1)
VerticalDropLine
IndexedLineSet
set_coordIndex
MFInt32
WaypointTrackScript
Script
verticalDropLinePoints
MFVec3f

ROUTE
event to
(1)
VerticalDropLineCoordinates
Coordinate
point
MFVec3f
WaypointTrackScript
Script
highlightCoordinates
MFVec3f

ROUTE
event to
(1)
HighlightSegmentCoordinates
Coordinate
point
MFVec3f
WaypointTrackScript
Script
pointIndices
MFInt32

ROUTE
event to
(1)
WaypointLine
IndexedLineSet
set_coordIndex
MFInt32
WaypointTrackScript
Script
labelInterpolation
MFString

ROUTE
event to
(1)
MovingVehicleLabelText
Text
string
MFString

line 239
Anchor
description='WaypointInterpolator Example' 
User-interaction hint for this node. 

Additional guidance on X3D animation can be found in the 10-Step Animation Design Process and Event Tracing hint sheets. Have fun with X3D! 😀

-->
<!-- Online at
https://www.web3d.org/x3d/content/examples/Savage/Tools/Animation/WaypointInterpolatorPrototypeIndex.html -->
<!-- Version control at
https://sourceforge.net/p/x3d/code/HEAD/tree/www.web3d.org/x3d/content/examples/Savage/Tools/Animation/WaypointInterpolatorPrototype.x3d -->

<!-- Color-coding legend: X3D terminology <X3dNode  DEF='idNamefield='value'/> matches XML terminology <XmlElement  DEF='idNameattribute='value'/>
(Light-blue background: event-based behavior node or statement) (Grey background inside box: inserted documentation) (Magenta background: X3D Extensibility)
    <ProtoDeclare name='ProtoName'> <field name='fieldName'/> </ProtoDeclare> -->

to top <!-- For additional help information about X3D scenes, please see X3D Tooltips, X3D Resources, and X3D Scene Authoring Hints. -->