####################################################################################################
#
# Invoking X3D model self-test:
#
#   $ python BathymetryGeneratorViaExtrusionPrototype.py
#
# Python package x3d.py package is available on PyPI for import.
#   This approach simplifies Python X3D deployment and use.
#   https://pypi.org/project/x3d
#
# Installation:
#       pip install x3d
# or
#       python -m pip install x3d
#
# Developer options for loading x3d package in other Python programs:
#
#    from x3d import *  # preferred approach, terser source that avoids x3d.* class prefixes
#
# or
#    import x3d         # traditional way to subclass x3d package, all classes require x3d.* prefix,
#                       # but python source is very verbose, for example x3d.Material x3d.Shape etc.
#                       # X3dToPython.xslt stylesheet insertPackagePrefix=true supports this option.
#
# Project home page:    # X3D Python Scene Access Interface Library (X3DPSAIL)
#                       # https://www.web3d.org/x3d/stylesheets/python/python.html
# Conversion generator: # https://www.web3d.org/x3d/stylesheets/X3dToPython.xslt
#
####################################################################################################

from x3d import *

newModel=X3D(profile='Immersive',version='3.0',
  head=head(
    children=[
    meta(content='BathymetryGeneratorViaExtrusionPrototype.x3d',name='title'),
    meta(content='This prototype generates bathymetry based on the input data, and uses Extrusion as the output geometry (with some problems as a result).',name='description'),
    meta(content='Jane Wu',name='creator'),
    meta(content='8 January 2002',name='created'),
    meta(content='28 November 2019',name='modified'),
    meta(content='bathymetry',name='subject'),
    meta(content='https://www.web3d.org/technicalinfo/specifications/vrml97/part1/nodesRef.html#Extrusion',name='reference'),
    meta(content='https://www.web3d.org/x3d/content/examples/Savage/Tools/Animation/BathymetryGeneratorViaExtrusionPrototype.x3d',name='identifier'),
    meta(content='X3D-Edit 3.2, https://www.web3d.org/x3d/tools/X3D-Edit',name='generator'),
    meta(content='../../license.html',name='license')]),
  Scene=Scene(
    children=[
    WorldInfo(title='BathymetryGeneratorViaExtrusionPrototype.x3d'),
    ProtoDeclare(name='BathymetryGenerator',
      ProtoInterface=ProtoInterface(
        field=[
        field(accessType='initializeOnly',name='positionArray',type='MFVec3f',value=[(0,0,0),(10,-4,0),(25,-6,0),(30,-8,5),(38,-15,5),(45,-18,5),(55,-22,5),(60,-25,15),(60,-27,22),(55,-30,35),(48,-35,35),(35,-35,35),(25,-45,35),(20,-55,35),(15,-70,35),(3,-70,35),(-5,-72,40),(-5,-75,50),(0,-80,55),(15,-75,55),(30,-70,55),(35,-60,55),(40,-50,55),(50,-34,55),(65,-23,70)]),
        field(accessType='initializeOnly',appinfo='for future development',name='timeArray',type='MFTime',value=[1,3,6,8,10,12,14,15,17,18,23,28,35,37,39,43,45,47,48,53,58,60,61,65,70]),
        field(accessType='initializeOnly',name='colorSchemeDepthRangeArray',type='MFVec2f',value=[(0,-10),(-10,-20),(-20,-30),(-30,-40),(-40,-50),(-50,-60),(-60,-70),(-70,-999999)]),
        field(accessType='initializeOnly',name='colorSchemeColorArray',type='MFColor',value=[(1,1,0.2),(0.6,1,1),(0,1,1),(0.2,0.6,0.2),(1,0,1),(0.56,0,0.32),(0.2,0.3,0.7),(0,0,1)]),
        field(accessType='initializeOnly',name='beamWidth',type='SFFloat',value=2),
        field(accessType='initializeOnly',name='surfaceTransparency',type='SFFloat',value=0.25),
        field(accessType='initializeOnly',name='traceEnabled',type='SFBool',value=False)]),
      ProtoBody=ProtoBody(
        children=[
        Group(
          children=[
          Transform(DEF='Bathymetry'),
          
            field=[
DEF='BathymetryScript',directOutput=True,
            field=[
            field(accessType='initializeOnly',name='positionArray',type='MFVec3f'),
            field(accessType='initializeOnly',name='timeArray',type='MFTime'),
            field(accessType='initializeOnly',name='colorSchemeDepthRangeArray',type='MFVec2f'),
            field(accessType='initializeOnly',name='colorSchemeColorArray',type='MFColor'),
            field(accessType='initializeOnly',name='beamWidth',type='SFFloat'),
            field(accessType='initializeOnly',name='transparency',type='SFFloat'),
            field(accessType='initializeOnly',name='spine',type='MFVec3f',value=[(0,0,0),(0,1,0)]),
            field(accessType='initializeOnly',name='scale',type='MFVec2f',value=[(1,1)]),
            field(accessType='initializeOnly',name='orientation',type='MFRotation',value=[(0,0,1,0)]),
            field(accessType='initializeOnly',name='bathyColor',type='SFColor',value=(1,1,1)),
            field(accessType='outputOnly',name='bathyNodes',type='MFNode'),
            field(accessType='initializeOnly',name='traceEnabled',type='SFBool'),
            field(accessType='initializeOnly',name='coordinate',type='SFVec3f',value=(0,0,0)),
            field(accessType='initializeOnly',name='previousPosition',type='SFVec3f',value=(0,0,0)),
            field(accessType='initializeOnly',name='position',type='SFVec3f',value=(0,0,0)),
            field(accessType='initializeOnly',name='bathyNodeIndex',type='SFInt32',value=0)],
            IS=IS(
              connect=[
              connect(nodeField='positionArray',protoField='positionArray'),
              connect(nodeField='timeArray',protoField='timeArray'),
              connect(nodeField='colorSchemeDepthRangeArray',protoField='colorSchemeDepthRangeArray'),
              connect(nodeField='colorSchemeColorArray',protoField='colorSchemeColorArray'),
              connect(nodeField='beamWidth',protoField='beamWidth'),
              connect(nodeField='transparency',protoField='surfaceTransparency'),
              connect(nodeField='traceEnabled',protoField='traceEnabled')]),

          sourceCode="""
ecmascript:

function initialize()
{
	bathyNodeIndex = 0;

	spineIndex = 0;
	position = positionArray[0];
	spine[spineIndex] = new SFVec3f(position.x, 0, position.z);
	scale[spineIndex] = new SFVec2f(1, Math.abs(position.y));
	spineIndex++;

	previousPosition = new SFVec3f(position.x, position.y, position.z);
	//Determine the initial depth range
	for (j = 0; j < colorSchemeDepthRangeArray.length; j++)
	{
		if (position.y >= colorSchemeDepthRangeArray[j].y)
			break;
	}
	currentDepthRangeIndex = j;

	for (i = 1; i < positionArray.length; i++)
	{			
		if (previousPosition.y == colorSchemeDepthRangeArray[currentDepthRangeIndex].y &&
		    positionArray[i].y != colorSchemeDepthRangeArray[currentDepthRangeIndex].y)
			terminateExtrusionSegmentWithCurrentPosition(currentDepthRangeIndex);

		//Update new position
		position = positionArray[i];

		//Determine the correct depth range
		if (position.y <= previousPosition.y)
		{
			for (j = currentDepthRangeIndex; j < colorSchemeDepthRangeArray.length; j++)
			{
				if (position.y >= colorSchemeDepthRangeArray[j].y)
					break;

				if (previousPosition.y != colorSchemeDepthRangeArray[currentDepthRangeIndex].y)
					terminateExtrusionSegmentWithDepthRangeBoundary(currentDepthRangeIndex);
			}
			currentDepthRangeIndex = j;
		}
		else
		{
			for (j = currentDepthRangeIndex; j > -1; j--)
			{
				if (position.y < colorSchemeDepthRangeArray[j-1].y)
					break;

				if (position.y > colorSchemeDepthRangeArray[j-1].y)
					terminateExtrusionSegmentWithDepthRangeBoundary(j-1);
			}
			currentDepthRangeIndex = j;
		}

		spine[spineIndex] = new SFVec3f(position.x, 0, position.z);
		scale[spineIndex] = new SFVec2f(1, Math.abs(position.y));
		spineIndex++;

		previousPosition = new SFVec3f(position.x, position.y, position.z);
	}
	terminateExtrusionSegmentWithCurrentPosition(currentDepthRangeIndex);
}

function terminateExtrusionSegmentWithDepthRangeBoundary(index)
{
	depthRange = colorSchemeDepthRangeArray[index];

	findCoordinate(previousPosition.x, position.x, previousPosition.y, position.y, depthRange.y);
	xPrime = coordinate;
	findCoordinate(previousPosition.z, position.z, previousPosition.y, position.y, depthRange.y);
	zPrime = coordinate;
	spine[spineIndex] = new SFVec3f(xPrime, 0, zPrime);
	scale[spineIndex] = new SFVec2f(1, Math.abs(depthRange.y));

	if (scale[scale.length-2].y > scale[scale.length-1].y)
		color = colorSchemeColorArray[index+1];
	else
		color = colorSchemeColorArray[index];

	createExtrusionShape(spine, scale, color);

	//Reset values to start the next extrustion segment
	spineIndex = 0;
	resetSpine();
	resetScale();

	//Update the current segment end as the start of the next segment
	spine[spineIndex] = new SFVec3f(xPrime, 0, zPrime);
	scale[spineIndex] = new SFVec2f(1, Math.abs(depthRange.y));
	spineIndex++;	
}

function terminateExtrusionSegmentWithCurrentPosition(index)
{
	if (scale[scale.length-1].y != Math.abs(colorSchemeDepthRangeArray[index].y))
		index--;

	if (scale[scale.length-2].y > scale[scale.length-1].y)
		color = colorSchemeColorArray[index+1];
	else
		color = colorSchemeColorArray[index];

	createExtrusionShape(spine, scale, color);

	//Reset values to start the next extrustion segment
	spineIndex = 0;
	resetSpine();
	resetScale();

	//Update the current segment end as the start of the next segment
	spine[spineIndex] = new SFVec3f(position.x, 0, position.z);
	scale[spineIndex] = new SFVec2f(1, Math.abs(position.y));
	spineIndex++;

	//Update the previousPosition
	previousPosition = new SFVec3f(position.x, position.y, position.z);
}

function findCoordinate(x1, x2, y1, y2, yPrime)
{
	coordinate = ((x1 - x2) / (y1 - y2)) * yPrime + ((x2*y1 - x1*y2) / (y1 - y2));
}

function createExtrusionShape(spine, scale, color)
{
	determineOrientation(spine);
	tracePrint('An extrusion is created whose spine is: ' + spine);
	tracePrint('and scale is: ' + scale);
	tracePrint('orientation is: ' + orientation);
	tracePrint('color is: ' + color);
	alwaysPrint('number of spine points is: ' + spine.length);
	alwaysPrint('orientation is: ' + orientation);

	//Build the VRML string
	extrusionSyntax  = 'Shape {\n';
	extrusionSyntax += '   appearance Appearance {' + '\n';
	extrusionSyntax += '      material Material {' + '\n';
	extrusionSyntax += '         diffuseColor ' + color + '\n';
	extrusionSyntax += '         transparency ' + transparency + '\n';
	extrusionSyntax += '      }' + '\n';
	extrusionSyntax += '   }' + '\n';
	extrusionSyntax += '   geometry Extrusion {' + '\n';
	extrusionSyntax += '      crossSection [' + (beamWidth/(-2)) + ', 1, ' + (beamWidth/2) + ', 1, ' + (beamWidth/(-2)) + ', 1]' + '\n';
	extrusionSyntax += '      scale ' + scale + '\n';
	extrusionSyntax += '      spine ' + spine + '\n';
	extrusionSyntax += '      orientation ' + orientation + '\n';
	extrusionSyntax += '      creaseAngle 1.57' + '\n';
	extrusionSyntax += '   }' + '\n';
	extrusionSyntax += '}';

	//Create Extrusion shape
	tracePrint (extrusionSyntax);
	bathySegment = new SFNode(extrusionSyntax);

	bathyNodes[bathyNodeIndex] = bathySegment;
	bathyNodeIndex++;
}

function determineOrientation(spine)
{
   previousZAxis = null;
   orientation = new MFRotation();
   //Special cases
   if (spine.length == 2)
   {
      if (spine[0].z == spine[1].z)
      {
         if (spine[0].x <= spine[1].x) //positive x direction
            orientation[0] = orientation[1] = new SFRotation(0, 1, 0, 1.57);
         else //negative x direction
            orientation[0] = orientation[1] = new SFRotation(0, 1, 0, -1.57);
      }
      else
      {
         if (spine[0].x == spine[1].x) //parallet to the z axis
            orientation[0] = orientation[1] = new SFRotation(0, 1, 0, 0);
         else
         {
            angleRadian = Math.atan((spine[0].x- spine[1].x) / (spine[0].z - spine[1].z));
//          angleRadian = Math.atan2((spine[0].x- spine[1].x), (spine[0].z - spine[1].z));
            
            orientation[0] = orientation[1] = new SFRotation(0, 1, 0, angleRadian);
         }
      }
      return;
   }

   for (n = 0; n < spine.length; n++)
   {
      //If spine is not closed, the Z axis used for the first spine point is the same as the Z axis for spine[1].
      //The Z axis used for the last spine point is the same as the Z axis for spine[spine.length - 2].   
      if (n == 0)
         si = 1;
      else if (n == (spine.length - 1))
         si = spine.length - 2;
      else
         si = n;

      zAxis = (spine[si+1].subtract(spine[si])).cross((spine[si-1].subtract(spine[si])));

      while (zAxis.x == 0 && zAxis.y == 0 && zAxis.z == 0)
      {
         if (previousZAxis == null)
         {
            ++si;
            if (si == (spine.length - 1)) //The entire spine is collinear
            {
               zAxis = new SFVec3f(1, 0, 0);
               break;
            }

            zAxis = (spine[si+1].subtract(spine[si])).cross((spine[si-1].subtract(spine[si])));
         }
         else
            zAxis = new SFVec3f(previousZAxis.x, previousZAxis.y, previousZAxis.z);
      }

      adjustedZAxis = zAxis;
      if (n == 0)
         previousZAxis = zAxis;
      else
      {
         dotProduct = zAxis.dot(previousZAxis);
         if (dotProduct < 0)
            adjustedZAxis = new SFVec3f(zAxis.multiply(-1).x, zAxis.multiply(-1).y, zAxis.multiply(-1).z);

         previousZAxis = adjustedZAxis;
      }
      
      zAxisNormalized = adjustedZAxis.normalize();
      theta = Math.acos(zAxisNormalized.dot(new SFVec3f(0, -1, 0)));
      if (spine[1].x < spine[0].x)
         orientation[n] = new SFRotation(0, -1, 0, theta);
      else
         orientation[n] = new SFRotation(0, 1, 0, theta);
   }
if (theta == 0)
   Browser.println ('rotation angle = ' + theta);
else if (theta > 1.57 && theta < 3.14)
   Browser.println ('rotation angle = ' + theta);
else if (theta > 3.14)
   Browser.println ('rotation angle = ' + theta);
}

function resetSpine()
{
	spine = new MFVec3f();
}

function resetScale()
{
	scale = new MFVec2f();
}

function tracePrint(string)
{
	if (traceEnabled)
		Browser.println ('[BathymetryGenerator] ' + string);
}

function alwaysPrint(string)
{
	Browser.println ('[BathymetryGenerator] ' + string);
}
"""),
          ROUTE(fromField='bathyNodes',fromNode='BathymetryScript',toField='addChildren',toNode='Bathymetry'),
          Shape(
            geometry=Extrusion(),)])])),
    Viewpoint(description='MainView',position=(0,-50,200)),
    ProtoInstance(name='BathymetryGenerator',
      fieldValue=[
      fieldValue(name='positionArray',value=[(0,0,0),(10,-4,0),(25,-6,0),(30,-8,5),(38,-15,5),(45,-18,5),(55,-22,5),(60,-25,15),(60,-27,22),(55,-30,35),(48,-35,35),(35,-35,35),(25,-45,35),(20,-55,35),(15,-70,35),(3,-70,35),(-5,-72,40),(-5,-75,50),(0,-80,55),(15,-75,55),(30,-70,55),(35,-60,55),(40,-50,55),(50,-34,55),(65,-23,70)]),
      fieldValue(name='surfaceTransparency',value=0.25),
      fieldValue(name='traceEnabled',value=True)])])
)

### X3D model conversion complete ###

####################################################################################################
# Self-test diagnostics
####################################################################################################

print('Self-test diagnostics for BathymetryGeneratorViaExtrusionPrototype.py:')
if        metaDiagnostics(newModel): # built-in utility method in X3D class
    print(metaDiagnostics(newModel)) # display meta info, hint, warning, error, TODO values in this model
# print('check newModel.XML() serialization...')
newModelXML= newModel.XML() # test export method XML() for exceptions during export
newModel.XMLvalidate()
# print(newModelXML) # diagnostic

try:
#   print('check newModel.VRML() serialization...')
    newModelVRML=newModel.VRML() # test export method VRML() for exceptions during export
    # print(prependLineNumbers(newModelVRML)) # debug
    print("Python-to-VRML export of VRML output successful", flush=True)
except Exception as err: # usually BaseException
    # https://stackoverflow.com/questions/18176602/how-to-get-the-name-of-an-exception-that-was-caught-in-python
    print("*** Python-to-VRML export of VRML output failed:", type(err).__name__, err)
    if newModelVRML: # may have failed to generate
        print(prependLineNumbers(newModelVRML, err.lineno))

try:
#   print('check newModel.JSON() serialization...')
    newModelJSON=newModel.JSON() # test export method JSON() for exceptions during export
#   print(prependLineNumbers(newModelJSON)) # debug
    print("Python-to-JSON export of JSON output successful (under development)")
except Exception as err: # usually SyntaxError
    print("*** Python-to-JSON export of JSON output failed:", type(err).__name__, err)
    if newModelJSON: # may have failed to generate
        print(prependLineNumbers(newModelJSON,err.lineno))

print("python BathymetryGeneratorViaExtrusionPrototype.py load and self-test diagnostics complete.")
