A JSON Serializer with Referential Integrity

Below is a JSON serializer implemented in JavaScript, along with an automated test of that serializer.

When run, the program creates a complex test object, serializes it to a string in JSON format, deserializes that string back to an object, re-serializes the deserialized object back to a string, compares the original serialized string to the re-serialized string, and, if the two strings are identical, prints a confirmation message to the debugging console to that effect.

To see the code in action, copy it into an .html file and open that file in a web browser that runs JavaScript.

This serializer provides some enhancements over other serializers I have written and used in previous projects. First, the code is somewhat cleaner. Second, the actual objects being serialized are not modified in any way during the serialization process–instead, a tree of separate “SerializationNode” objects is built that shadows the structure of the original object and its descendant objects. Third, this serializer does not require a static list of “known types” to be specified beforehand in order to assign class prototypes to generic deserialized objects, instead using the “eval” function to obtain these prototypes dynamically when needed. Fourth, this serializer can serialize objects that contain circular references, which is something that even the built-in JSON.stringify() can’t do by itself.

Fifth, and most significantly, this serializer maintains “referential integrity” of the serialized objects. That is, if objects A and B both reference a single object C before the serialization, then the subsequently deserialized versions of A and B will both reference the same deserialized instance of object C, rather than each getting their own copy of C. This breaking of referential integrity has played havoc with the architecture of past projects, requiring me to completely redesign otherwise working systems to support serialization. Hopefully this new serializer will make that sort of thing unnecessary.


<html>
<body>
<script type="text/javascript">

// main

function testSerializer()
{
	// Create a complex object to serialize.
	var one = new SerializationTestObject
	(
		true, // booleanValue
		1, // numberValue
		"One", // stringValue,
		function (a, b)
		{
			return a - b
		},
		// arrayValue
		[
			new SerializationTestObject
			(
				true,
				2, 
				"Two",
				null, // functionValue
				[] // arrayValue
			),

			new SerializationTestObject
			(
				true,
				3, 
				"Three",
				null, // functionValue
				[] // arrayValue
			),
		]
	);

	var four = new SerializationTestObject
	(
		true,
		4, 
		"Four",
		null, // functionValue
		null // arrayValue
	);

	// Create two references to a single object.
	
	one.arrayValue[0].arrayValue.push(four);
	one.arrayValue[1].arrayValue.push(four);	

	// Create a circular reference.
	one.arrayValue.push(objectToSerialize);

	var serializer = new Serializer
	([
		// known types
		SerializationTestObject
	]);

	var objectToSerialize = one;
	var objectSerialized = serializer.serialize(objectToSerialize);
	var objectDeserialized = serializer.deserialize(objectSerialized);
	var objectReserialized = serializer.serialize(objectDeserialized);

	if (objectSerialized != objectReserialized)
	{
		console.log("Test failed!");
	}
	else
	{
		console.log("Test passed!");
	}
}

// classes

function SerializationTestObject
(
	booleanValue, numberValue, stringValue, functionValue, arrayValue
)
{
	this.booleanValue = booleanValue;
	this.numberValue = numberValue;
	this.stringValue = stringValue;
	this.functionValue = functionValue;
	this.arrayValue = arrayValue;
}
{
	SerializationTestObject.prototype.add = function(a, b)
	{
		// This is part of the prototype,
		// and thus should not be serialized!
		return a + b;
	}
}

function Serializer()
{
	// do nothing
}
{
	Serializer.prototype.deserialize = function(stringToDeserialize)
	{
		var nodeRoot = JSON.parse(stringToDeserialize);
		nodeRoot.__proto__ = SerializerNode.prototype;
		nodeRoot.prototypesAssign();
		var returnValue = nodeRoot.unwrap([]);

		return returnValue;
	}

	Serializer.prototype.serialize = function(objectToSerialize)
	{
		var nodeRoot = new SerializerNode(objectToSerialize);

		nodeRoot.wrap([], []);

		var nodeRootSerialized = JSON.stringify
		(
			nodeRoot, 
			null, // ? 
			4 // pretty-print indent size
		);

		return nodeRootSerialized;
	}
}

function SerializerNode(objectWrapped)
{
	this.objectWrappedTypeName = null;
	this.id = null;
	this.isReference = null;

	this.objectWrapped = objectWrapped;
}
{
	SerializerNode.prototype.wrap = function
	(
		objectsAlreadyWrapped, objectIndexToNodeLookup
	)
	{
		if (this.objectWrapped != null)
		{			
			var typeName = this.objectWrapped.constructor.name;

			var objectIndexExisting = 
				objectsAlreadyWrapped.indexOf(this.objectWrapped);
				
			if (objectIndexExisting >= 0)
			{
				var nodeForObjectExisting = objectIndexToNodeLookup[objectIndexExisting];
				this.id = nodeForObjectExisting.id;
				this.isReference = true;
				this.objectWrapped = null;
			}
			else
			{
				this.isReference = false;
				var objectIndex = objectsAlreadyWrapped.length;
				this.id = objectIndex;
				objectsAlreadyWrapped.push(this.objectWrapped);
				objectIndexToNodeLookup[objectIndex] = this;

				this.objectWrappedTypeName = typeName;
	
				if (typeName == "Function")
				{
					this.objectWrapped = this.objectWrapped.toString();
				}
				else
				{
					this.children = {};
	
					for (var propertyName in this.objectWrapped)
					{
						if (this.objectWrapped.__proto__[propertyName] == null)
						{
							var propertyValue = this.objectWrapped[propertyName];

							if (propertyValue == null)
							{
								child = null;
							}
							else 
							{			
								var propertyValueTypeName = propertyValue.constructor.name;

								if 
								(
									propertyValueTypeName == "Boolean"
									|| propertyValueTypeName == "Number"
									|| propertyValueTypeName == "String"
								)
								{
									child = propertyValue;
								}
								else
								{
									child = new SerializerNode
									(
										propertyValue
									);
								}

							}

							this.children[propertyName] = child;
						}
					}

					delete this.objectWrapped;
	
					for (var childName in this.children)
					{
						var child = this.children[childName];
						if (child != null)
						{
							var childTypeName = child.constructor.name;
							if (childTypeName == "SerializerNode")
							{
								child.wrap
								(
									objectsAlreadyWrapped,
									objectIndexToNodeLookup
								);
							}
						}
					}
				}
			}

		} // end if objectWrapped != null

		return this;		

	} // end method

	SerializerNode.prototype.prototypesAssign = function()
	{
		if (this.children != null)
		{
			for (var childName in this.children)
			{
				var child = this.children[childName];
				if (child != null)
				{
					var childTypeName = child.constructor.name;
					if (childTypeName == "Object")
					{
						child.__proto__ = SerializerNode.prototype;
						child.prototypesAssign();
					}
				}
			}
		}
	}

	SerializerNode.prototype.unwrap = function(nodesAlreadyProcessed)
	{
		if (this.isReference == true)
		{
			var nodeExisting = nodesAlreadyProcessed[this.id];
			this.objectWrapped = nodeExisting.objectWrapped;
		}
		else
		{
			nodesAlreadyProcessed[this.id] = this;
			var typeName = this.objectWrappedTypeName;
			if (typeName == null)
			{
				// Value is null.  Do nothing.
			}
			else if (typeName == "Array")
			{
				this.objectWrapped = [];
			}
			else if (typeName == "Function")
			{
				this.objectWrapped = eval("(" + this.objectWrapped + ")");
			}
			else if 
			(
				typeName == "Boolean" 
				|| typeName == "Number" 
				|| typeName == "String"
			)
			{
				// Primitive types. Do nothing.
			}
			else
			{
				this.objectWrapped = {};
				var objectWrappedType = eval("(" + typeName + ")");
				this.objectWrapped.__proto__ = objectWrappedType.prototype;
			}

	
			if (this.children != null)
			{
				for (var childName in this.children)
				{
					var child = this.children[childName];
			
					if (child != null)
					{
						if (child.constructor.name == "SerializerNode")
						{
							child = child.unwrap
							(
								nodesAlreadyProcessed
							);
						}
					}

					this.objectWrapped[childName] = child;
				}
			}

		}

		return this.objectWrapped;
	}


}

// run

testSerializer();

</script>
</body>
</html>

Posted in Uncategorized | Tagged , , , | Leave a comment

A Rudimentary Mesh Editor in JavaScript

The code below implements a rudimentary editor for three-dimensional forms called “meshes”. To see it in action, copy it into an .html file and open that file in a web browser that runs JavaScript. Or, for an online version, visit “https://thiscouldbebetter.neocities.org/mesheditor.html“.

MeshEditor.png


<html>
<body>

<div id="divMain"></div>
<div id="divUI">
	<div id="divView" style="border:1px solid">
		<label>View:</label>
		<div style="border:1px solid">
			<label>Camera:</label>
			<button onclick="buttonViewUp_Clicked();">^</button>
			<button onclick="buttonViewDown_Clicked();">v</button>
			<button onclick="buttonViewLeft_Clicked();">&lt;</button>
			<button onclick="buttonViewRight_Clicked();">&gt;</button>
			<button onclick="buttonViewIn_Clicked();">+</button>
			<button onclick="buttonViewOut_Clicked();">-</button>
			<button onclick="buttonViewFront_Clicked();">Front</button>	
			<button onclick="buttonViewSide_Clicked();">Side</button>	
			<button onclick="buttonViewTop_Clicked();">Top</button>			
			<button onclick="buttonViewSelected_Clicked();">Selected</button>			
		</div>
		<div style="border:1px solid">
			
			<label>Highlight:</label>
			<label>Vertices:</label>
			<input id="checkboxHighlightVertices" type="checkbox" checked="true" onchange="checkboxHighlightVertices_Changed();"></input>
			<label>Faces:</label>
			<input id="checkboxHighlightFaces" type="checkbox" checked="true" onchange="checkboxHighlightFaces_Changed();"></input>
		</div>
	</div>
	<div id="divSelect" style="border:1px solid">
		<label>Select:</label>
		<button onclick="buttonSelectAtCursor_Clicked();">At Cursor</button>
		<button onclick="buttonSelectAll_Clicked();">All</button>
		<button onclick="buttonSelectNone_Clicked();">None</button>
		<button onclick="buttonDeselectAtCursor_Clicked();">Deselect At Cursor</button>
	</div>
	<div id="divVertexOperations" style="border:1px solid">
		<label>Vertices:</label>
		<button onclick="buttonVertexAddAtCursor_Clicked();">Add at Cursor</button>
		<button onclick="buttonVertexDeleteSelected_Clicked();">Delete Selected</button>	
		<button onclick="buttonVertexConnectSelected_Clicked();">Connect Selected</button>		
		<button onclick="buttonVertexFaceBuildWithSelected_Clicked();">Face from Selected</button>				
	</div>
	
	<div id="divTransform" style="border:1px solid">
		<label>Transform:</label>
		<div id="divTransformParameters">
			<div>
				<label>Relative to:</label>
				<select id="selectTransformCenter">
					<option>Origin</option>				
					<option>Median of Selected</option>
					<option>Cursor</option>
				</select>
			</div>
			<div>
				<label>Axes:</label>
				<select id="selectTransformAxes">
					<option>All</option>
					<option>X</option>
					<option>Y</option>
					<option>Z</option>
					<option>Camera Forward</option>
				</select>
			</div>
			<div>
				<label>Operation:</label>
				<select id="selectTransformOperation">
					<option>Translate</option>
					<option>Rotate</option>
					<option>Scale</option>
				</select>
			</div>
			<div>
				<label>Amount:</label>
				<input id="inputTransformAmount" type="number" value="0"></input>
			</div>
			<button onclick="buttonTransform_Clicked();">Transform Selected</button>
		</div>
	</div>
	<div style="border:1px solid">
		<label>File:</label>
		<button onclick="buttonFileSave_Clicked();">Save</button>
		<button onclick="buttonFileLoad_Clicked();">Load:</button>
		<input id="inputFileToLoad" type="file"></input>
	</div>
</div>

<script type="text/javascript">

// ui events

function buttonDeselectAtCursor_Clicked()
{
	Globals.Instance.session.deselectAtCursor();
}

function buttonFileLoad_Clicked()
{
	var inputFileToLoad = document.getElementById("inputFileToLoad");
	var fileToLoad = inputFileToLoad.files[0];
	if (fileToLoad == null)
	{
		alert("No file specified!")
	}
	else
	{
		Globals.Instance.session.sceneLoadFromFile(fileToLoad);
	}
}

function buttonFileSave_Clicked()
{
	Globals.Instance.session.sceneSaveToFile();
}

function buttonSelectAll_Clicked()
{
	Globals.Instance.session.selectAll();
}

function buttonSelectAtCursor_Clicked()
{
	Globals.Instance.session.selectAtCursor();
}

function buttonSelectNone_Clicked()
{
	Globals.Instance.session.selectNone();
}

function buttonTransform_Clicked()
{
	var session = Globals.Instance.session;

	var centerName = document.getElementById("selectTransformCenter").value;

	var center;
	if (centerName == "Origin")
	{
		center = new Coords(0, 0, 0);
	}
	else if (centerName == "Median of Selected")
	{
		var scene = session.scene;
		center = scene.selection.medianForMesh(scene.mesh);
	}
	else if (centerName == "Cursor")
	{
		center = session.scene.cursor.pos;
	}
	
	var axes = document.getElementById("selectTransformAxes").value;
	var operation = document.getElementById("selectTransformOperation").value;
	var amount = parseFloat(document.getElementById("inputTransformAmount").value);
	if (isNaN(amount) == true)
	{
		amount = 0;
	}
	
	var amountAsCoords;
	var axis;
	if (axes == "All")
	{
		amountAsCoords = new Coords(1, 1, 1);
		axis = null;
	}
	else if (axes == "X")
	{
		amountAsCoords = new Coords(1, 0, 0);
		axis = new Coords(1, 0, 0);		
	}
	else if (axes == "Y")
	{
		amountAsCoords = new Coords(0, 1, 0);
		axis = new Coords(0, 1, 0);		
	}
	else if (axes == "Z")
	{
		amountAsCoords = new Coords(0, 0, 1);
		axis = new Coords(0, 0, 1);
	}
	else if (axes == "Camera Forward")
	{
		var camera = session.scene.camera;
		var cameraForward = camera.loc.orientation.forward;
		amountAsCoords = cameraForward.clone().normalize();	
		axis = cameraForward.clone();
	}	
	
	amountAsCoords.multiplyScalar(amount);

	var transform = null;
	if (operation == "Translate")
	{
		if (axes != "All")
		{
			transform = new Transform_Translate(amountAsCoords);
		}
	}
	else if (operation == "Rotate")
	{
		if (axes != "All")
		{
			transform = new Transform_Rotate(axis, amount);
		}
	}
	else if (operation == "Scale")
	{
		transform = new Transform_Scale(amountAsCoords);
	}

	if (transform == null)
	{
		alert("Invalid parameters for transform!");
	}
	else
	{
		session.transformWithCenterApplyToSelected(transform, center);
		session.update();
	}
}

function buttonVertexAddAtCursor_Clicked()
{
	var session = Globals.Instance.session;
	var scene = session.scene;
	var vertexNew = scene.cursor.pos.clone();
	var mesh = scene.mesh;
	mesh.vertices.push(vertexNew);
	session.selectNone();
	session.selectAtCursor();
	session.update();
}

function buttonVertexConnectSelected_Clicked()
{
	var session = Globals.Instance.session;
	var scene = session.scene;
	var mesh = scene.mesh;
	var selection = scene.selection;
	mesh.verticesConnectWithEdge(selection.vertexIndices);
	session.update();
}

function buttonVertexDeleteSelected_Clicked()
{
	var session = Globals.Instance.session;
	var scene = session.scene;
	var mesh = scene.mesh;
	var verticesToRemove = scene.selection.verticesForMesh(mesh);
	mesh.verticesRemove(verticesToRemove);
	session.selectNone();
	session.update();
}

function buttonVertexFaceFromSelected_Clicked()
{
	var session = Globals.Instance.session;
	var scene = session.scene;
	var mesh = scene.mesh;
	var selection = scene.selection;
	mesh.edgesConnectWithFace(selection.vertexIndices);
	session.update();
}

function buttonViewDown_Clicked()
{
	var session = Globals.Instance.session;
	session.viewMoveDown();
	session.update();
}

function buttonViewFront_Clicked()
{
	var session = Globals.Instance.session;
	session.viewSetFront();
	session.update();
}

function buttonViewIn_Clicked()
{
	var session = Globals.Instance.session;
	session.viewMoveIn();
	session.update();	
}

function buttonViewLeft_Clicked()
{
	var session = Globals.Instance.session;
	session.viewMoveLeft();
	session.update();
}

function buttonViewOut_Clicked()
{
	var session = Globals.Instance.session;
	session.viewMoveOut();
	session.update();
}

function buttonViewRight_Clicked()
{
	var session = Globals.Instance.session;
	session.viewMoveRight();
	session.update();
}

function buttonViewSelected_Clicked()
{
	var session = Globals.Instance.session;
	session.viewSetSelected();
	session.update();
}

function buttonViewSide_Clicked()
{
	var session = Globals.Instance.session;
	session.viewSetSide();
	session.update();
}

function buttonViewTop_Clicked()
{
	var session = Globals.Instance.session;
	session.viewSetTop();
	session.update();
}

function buttonViewUp_Clicked()
{
	var session = Globals.Instance.session;
	session.viewMoveUp();
	session.update();	
}

function checkboxHighlightVertices_Changed()
{
	var highlightVertices = document.getElementById("checkboxHighlightVertices").checked;	
	var session = Globals.Instance.session;
	session.scene.highlightVertices = highlightVertices;
	session.update();	
}

function checkboxHighlightFaces_Changed()
{
	var highlightFaces = document.getElementById("checkboxHighlightFaces").checked;	
	var session = Globals.Instance.session;
	session.scene.highlightFaces = highlightFaces;
	session.update();	
}


// main

function main()
{
	var camera = new Camera
	(
		new Coords(100, 100, 0), // viewSize
		50, // focalLength
		new Location
		(
			new Coords(-2, -2, -2), // pos
			Orientation.fromForwardAndDown
			(
				new Coords(1, 1, 1), // forward
				new Coords(0, 0, 1) // down
			)
		)
	);

	var display = new Display
	(
		camera.viewSize
	);
	
	var mesh = MeshBuilder.cube();
	
	var scene = new Scene
	(
		camera,
		mesh
	);

	Globals.Instance.initialize
	(
		display,
		scene
	);
}

// extensions

function ArrayExtensions()
{
	// extension class
}
{
	Array.prototype.contains = function(element)
	{
		return (this.indexOf(element) >= 0);
	}

	Array.prototype.remove = function(element)
	{
		this.splice(this.indexOf(element), 1);
		return this;
	}
	
	Array.prototype.removeAt = function(index)
	{
		this.splice(index, 1);
		return this;
	}
	
}

// classes

function Bounds(min, max)
{
	this.min = min;
	this.max = max;
	this.size = new Coords();
	this.sizeRecalculate();
}
{
	// static methods
	
	Bounds.ofPoints = function(pointsToFindBoundsOf)
	{
		var point0 = pointsToFindBoundsOf[0];
		var min = point0.clone();
		var max = point0.clone();
		
		for (var i = 1; i < pointsToFindBoundsOf.length; i++)
		{
			var point = pointsToFindBoundsOf[i];
			if (point.x < min.x)
			{
				min.x = point.x;
			}
			else if (point.x > max.x)
			{
				max.x = point.x;
			}

			if (point.y < min.y)
			{
				min.y = point.y;
			}
			else if (point.y > max.y)
			{
				max.y = point.y;
			}

			if (point.z < min.z)
			{
				min.z = point.z;
			}
			else if (point.z > max.z)
			{
				max.z = point.z;
			}
		}
		
		var returnValue = new Bounds(min, max);
		
		return returnValue;
	}

	// instance methods
	
	Bounds.prototype.center = function()
	{
		return this.min.clone().add(this.max).divideScalar(2);
	}

	Bounds.prototype.sizeRecalculate = function()
	{
		this.size.overwriteWith(this.max).subtract(this.min);
	}

	Bounds.prototype.trimCoords = function(coordsToTrim)
	{
		if (coordsToTrim.x < this.min.x)
		{
			coordsToTrim.x = this.min.x;
		}
		else if (coordsToTrim.x > this.max.x)
		{
			coordsToTrim.x = this.max.x;
		}
		
		if (coordsToTrim.y < this.min.y)
		{
			coordsToTrim.y = this.min.y;
		}
		else if (coordsToTrim.y > this.max.y)
		{
			coordsToTrim.y = this.max.y;
		}

		if (coordsToTrim.z < this.min.z)
		{
			coordsToTrim.z = this.min.z;
		}
		else if (coordsToTrim.z > this.max.z)
		{
			coordsToTrim.z = this.max.z;
		}		
		
		return coordsToTrim;
	}
}

function Camera(viewSize, focalLength, loc)
{
	this.viewSize = viewSize;
	this.focalLength = focalLength;
	this.loc = loc;
	
	this.viewSizeHalf = this.viewSize.clone().divideScalar(2);
			
	this.constraints = 
	[
		new Constraint_Upright(),	
		new Constraint_LookAt(new Coords(0, 0, 0)),
		new Constraint_KeepDistance(new Coords(0, 0, 0), this.loc.pos.magnitude()),
	];
	
	this.reinitialize();
}
{
	Camera.prototype.constraintsApply = function()
	{
		for (var i = 0; i < this.constraints.length; i++)
		{
			var constraint = this.constraints[i];
			constraint.constrainLoc(this.loc)
		}
	}
		
	Camera.prototype.transformCoordsWorldToView = function(coordsToTransform)
	{
		return this.transformWorldToView.transformCoords(coordsToTransform);
	}
	
	Camera.prototype.transformCoordsViewToWorld = function(coordsToTransform)
	{
		return this.transformViewToWorld.transformCoords(coordsToTransform);
	}	

	Camera.prototype.transformDistanceWorldToView = function(distanceToTransform, distanceFromCamera)
	{
		var returnValue = distanceToTransform * this.focalLength * this.focalLength / distanceFromCamera;
		return returnValue;
	}
	
	// serialization
	
	Camera.prototype.reinitialize = function()
	{
		// hack
	
		this.loc.orientation.axesReset();
	
		this.transformWorldToView = new Transform_Multiple
		([
			new Transform_TranslateInverse(this.loc.pos),
			new Transform_Orient(this.loc.orientation),
			new Transform_Perspective(this.focalLength),
			new Transform_Translate(this.viewSizeHalf),
		]);
		
		this.transformViewToWorld = new Transform_Multiple
		([
			new Transform_TranslateInverse(this.viewSizeHalf),	
			new Transform_PerspectiveInverse(this.focalLength),
			new Transform_OrientInverse(this.loc.orientation),
			new Transform_Translate(this.loc.pos),
		]);		
	}

	
}

function Constraint_KeepDistance(targetPos, distanceToMaintain)
{
	this.targetPos = targetPos;
	this.distanceToMaintain = distanceToMaintain;
}
{
	Constraint_KeepDistance.prototype.constrainLoc = function(loc)
	{
		var displacement = loc.pos.subtract // No clone needed.
		(
			this.targetPos
		);
		
		displacement.normalize().multiplyScalar
		(
			this.distanceToMaintain
		).add
		(
			this.targetPos
		);		
	}
}


function Constraint_LookAt(targetPos)
{
	this.targetPos = targetPos;
}
{
	Constraint_LookAt.prototype.constrainLoc = function(loc)
	{
		var orientation = loc.orientation;
		orientation.forward.overwriteWith
		(
			this.targetPos
		).subtract
		(
			loc.pos
		)
		
		orientation.orthogonalizeAxes().normalizeAxes();		
	}
}

function Constraint_Upright()
{
	// do nothing
}
{
	Constraint_Upright.prototype.constrainLoc = function(loc)
	{
		var orientation = loc.orientation;
		if (orientation.forward.equals(Coords.Instances.ZeroZeroOne) == false)
		{
			orientation.down.overwriteWithXYZ(0, 0, 1);
		}
	}
}

function Coords(x, y, z)
{
	this.x = x;
	this.y = y;
	this.z = z;
}
{
	// instances
	
	Coords.Instances = new Coords_Instances()
	
	function Coords_Instances()
	{
		this.Ones = new Coords(1, 1, 1);
		this.ZeroZeroOne = new Coords(0, 0, 1);	
		this.Zeroes = new Coords(0, 0, 0);
	}

	// methods

	Coords.prototype.add = function(other)
	{
		this.x += other.x;
		this.y += other.y;
		this.z += other.z;
		return this;
	}

	Coords.prototype.clone = function()
	{
		return new Coords(this.x, this.y, this.z);
	}
	
	Coords.prototype.crossProduct = function(other)
	{
		return this.overwriteWithXYZ 
		(
			this.y * other.z - this.z * other.y,
			this.z * other.x - this.x * other.z,
			this.x * other.y - this.y * other.x
		);
	}
	
	Coords.prototype.divide = function(other)
	{
		this.x /= other.x;
		this.y /= other.y;
		this.z /= other.z;
		return this;
	}
	
	Coords.prototype.divideScalar = function(scalar)
	{
		this.x /= scalar;
		this.y /= scalar;
		this.z /= scalar;
		return this;
	}
	
	Coords.prototype.dotProduct = function(other)
	{
		return this.x * other.x + this.y * other.y + this.z * other.z;
	}
	
	Coords.prototype.equals = function(other)
	{
		var returnValue = 
		(
			this.x == other.x
			&& this.y == other.y
			&& this.z == other.z
		);
		
		return returnValue;
	}
	
	Coords.prototype.invert = function()
	{
		return this.multiplyScalar(-1);
	}

	Coords.prototype.magnitude = function()
	{
		return Math.sqrt(this.x * this.x + this.y * this.y + this.z * this.z);
	}
	
	Coords.prototype.multiply = function(other)
	{
		this.x *= other.x;
		this.y *= other.y;
		this.z *= other.z;
		return this;
	}
	
	Coords.prototype.multiplyScalar = function(scalar)
	{
		this.x *= scalar;
		this.y *= scalar;
		this.z *= scalar;
		return this;
	}	
	
	Coords.prototype.normalize = function()
	{
		return this.divideScalar(this.magnitude());
	}
	
	Coords.prototype.overwriteWith = function(other)
	{
		this.x = other.x;
		this.y = other.y;
		this.z = other.z;
		return this;
	}	
	
	Coords.prototype.overwriteWithXYZ = function(x, y, z)
	{
		this.x = x;
		this.y = y;
		this.z = z;
		return this;
	}		
	
	Coords.prototype.subtract = function(other)
	{
		this.x -= other.x;
		this.y -= other.y;
		this.z -= other.z;
		return this;
	}
}

function Cursor()
{
	this.radius = 5;
	this.pos = new Coords(0, 0, 0);
}
{
	Cursor.prototype.drawToDisplayForCamera = function(display, camera)
	{
		var drawPos = display.drawPos;
		camera.transformCoordsWorldToView
		(
			drawPos.overwriteWith
			(
				this.pos
			)
		);
		display.drawCircle(drawPos, this.radius, null, "Black");
	}
}

function Display(viewSize)
{
	this.viewSize = viewSize;
	
	this.colorFore = "LightGray";
	this.colorBack = "White";
	
	// helper variables
	this.drawPos = new Coords();
}
{
	Display.prototype.clear = function()
	{
		this.drawRectangle(Coords.Instances.Zeroes, this.viewSize, this.colorBack, this.colorFore);
	}

	Display.prototype.drawCircle = function(center, radius, colorFill, colorBorder)
	{
		this.graphics.beginPath();
		
		this.graphics.arc
		(
			center.x, center.y,
			radius,
			0, Math.PI * 2 // start and stop angles
		);
		
		if (colorFill != null)
		{
			this.graphics.fillStyle = colorFill;
			this.graphics.fill();
		}
		
		if (colorBorder != null)
		{
			this.graphics.strokeStyle = colorBorder;		
			this.graphics.stroke();
		}
	}

	Display.prototype.drawLine = function(fromPos, toPos)
	{
		this.graphics.beginPath();
		this.graphics.moveTo(fromPos.x, fromPos.y);
		this.graphics.lineTo(toPos.x, toPos.y);
		this.graphics.stroke();
	}
	
	Display.prototype.drawRectangle = function(pos, size, colorFill, colorBorder)
	{
		if (colorFill != null)
		{
			this.graphics.fillStyle = colorFill;
			this.graphics.fillRect
			(
				pos.x, pos.y,
				size.x, size.y
			);
		}
		
		if (colorBorder != null)
		{
			this.graphics.strokeStyle = colorBorder;
			this.graphics.strokeRect
			(
				pos.x, pos.y,
				size.x, size.y
			);
		}
	}
	
	Display.prototype.initialize = function()
	{
		this.canvas = document.createElement("canvas");
		this.canvas.width = this.viewSize.x;
		this.canvas.height = this.viewSize.y;
		
		this.graphics = this.canvas.getContext("2d");
		
		var divMain = document.getElementById("divMain");
		divMain.appendChild(this.canvas);
	}
}

function Edge(vertexIndices)
{
	this.vertexIndices = vertexIndices;
}
{
	// helper variables

	Edge.drawPosFrom = new Coords();
	Edge.drawPosTo = new Coords();

	// methods

	Edge.prototype.vertices = function(mesh)
	{
		var returnValues = [];
	
		var vertices = mesh.vertices;
		for (var i = 0; i < this.vertexIndices.length; i++)
		{
			var vertexIndex = this.vertexIndices[i];
			var vertex = vertices[vertexIndex];
			returnValues.push(vertex);
		}
		
		return returnValues;	
	}
	
	// drawable
	
	Edge.prototype.drawToDisplayForCameraAndMesh = function(display, camera, mesh)
	{
		var fromPos = Edge.drawPosFrom;
		var toPos = Edge.drawPosTo;
		
		var vertices = this.vertices(mesh);
		
		fromPos.overwriteWith(vertices[0]);
		camera.transformCoordsWorldToView(fromPos);
		
		toPos.overwriteWith(vertices[1]);
		camera.transformCoordsWorldToView(toPos);
		
		display.drawLine(fromPos, toPos);
	}
}

function Face(vertexIndices)
{
	this.vertexIndices = vertexIndices;
}
{
	Face.prototype.medianForMesh = function(mesh)
	{
		var vertices = this.vertices(mesh);
		var median = vertices[0].clone();
		for (var i = 1; i < vertices.length; i++)
		{
			var vertex = vertices[i];
			median.add(vertex);
		}
		median.divideScalar(vertices.length);
		
		return median;
	}

	Face.prototype.plane = function(mesh)
	{
		var vertices = this.vertices(mesh);
		var plane = Plane.fromPoints(vertices);
		return plane;
	}

	Face.prototype.vertices = function(mesh)
	{
		var returnValues = [];
	
		var vertices = mesh.vertices;
		for (var i = 0; i < this.vertexIndices.length; i++)
		{
			var vertexIndex = this.vertexIndices[i];
			var vertex = vertices[vertexIndex];
			returnValues.push(vertex);
		}
		
		return returnValues;
	}
	
	// drawable
	
	Face.prototype.drawToDisplayForCameraAndMesh = function(display, camera, mesh)
	{
		var median = this.medianForMesh(mesh);
		var normal = this.plane(mesh).normal;

		var fromPos = median;

		var normalIndicatorLength = 1;
		var toPos = normal.multiplyScalar(normalIndicatorLength).add(median);

		camera.transformCoordsWorldToView(fromPos);
		camera.transformCoordsWorldToView(toPos);
		
		display.drawRectangle(fromPos, Coords.Instances.Ones, null, display.colorFore);
		display.drawLine(fromPos, toPos);
	}

}

function FileHelper()
{
	// do nothing
}
{
    FileHelper.prototype.loadFileAsBinaryString = function(systemFileToLoad, callback, contextForCallback)
    {
        var fileReader = new FileReader();
        fileReader.systemFile = systemFileToLoad;
        fileReader.callback = callback;
        fileReader.contextForCallback = contextForCallback;
        fileReader.onload = this.loadFile_FileLoaded.bind(this);
        fileReader.readAsBinaryString(systemFileToLoad);
    }
	
    FileHelper.prototype.loadFileAsText = function(systemFileToLoad, callback, contextForCallback)
    {
        var fileReader = new FileReader();
        fileReader.systemFile = systemFileToLoad;
        fileReader.callback = callback;
        fileReader.contextForCallback = contextForCallback;
        fileReader.onload = this.loadFile_FileLoaded.bind(this);
        fileReader.readAsText(systemFileToLoad);
    }
	 
    FileHelper.prototype.loadFile_FileLoaded = function(fileLoadedEvent)
    {
        var fileReader = fileLoadedEvent.target;
        var contentsOfFileLoaded = fileReader.result;
        var fileName = fileReader.systemFile.name;
 
        var callback = fileReader.callback;
        var contextForCallback = fileReader.contextForCallback;
        callback.call(contextForCallback, contentsOfFileLoaded);
    }
 
    FileHelper.prototype.saveBinaryStringToFileWithName = function(fileAsBinaryString, fileName)
    {
        var fileAsArrayBuffer = new ArrayBuffer(fileAsBinaryString.length);
        var fileAsArrayUnsigned = new Uint8Array(fileAsArrayBuffer);
        for (var i = 0; i < fileAsBinaryString.length; i++) 
        {
            fileAsArrayUnsigned[i] = fileAsBinaryString.charCodeAt(i);
        }
 
        var fileAsBlob = new Blob([fileAsArrayBuffer], {type:'unknown/unknown'});
 
        var link = document.createElement("a");
        link.href = window.URL.createObjectURL(fileAsBlob);
        link.download = fileName;
        link.click();
    }
	
	FileHelper.prototype.saveTextStringToFileWithName = function(textToSave, fileNameToSaveAs)
	{
		var textToSaveAsBlob = new Blob([textToSave], {type:"text/plain"});	 
        var link = document.createElement("a");
        link.href = window.URL.createObjectURL(textToSaveAsBlob);
        link.download = fileNameToSaveAs;
        link.click();
	}
}

function Globals()
{
	// do nothing
}
{
	// instance
	
	Globals.Instance = new Globals();
	
	// methods

	Globals.prototype.initialize = function(display, scene)
	{
		this.display = display;
		this.session = new Session(scene);
		
		this.inputHelper = new InputHelper();
		
		this.display.initialize();		
		this.session.initialize();		
		this.update();
		
		this.inputHelper.initialize();
	}
	
	Globals.prototype.update = function()
	{
		this.session.update();
	}
}

function InputHelper()
{
	this.keyPressed = null;
	this.isMouseButtonPressed = false;
	this.mouseClickPos = new Coords();
}
{
	InputHelper.prototype.initialize = function()
	{
		document.body.onkeydown = this.handleEventKeyDown.bind(this);
		document.body.onkeyup = this.handleEventKeyUp.bind(this);

		var canvas = Globals.Instance.display.canvas;
		canvas.onmousedown = this.handleEventMouseDown.bind(this);
		canvas.onmouseup = this.handleEventMouseUp.bind(this);
		
		var divMainBounds = divMain.getBoundingClientRect();
		this.mouseClickPosOffset = new Coords
		(
			divMainBounds.left,
			divMainBounds.top,
			0
		);
	}
	
	// events
	
	InputHelper.prototype.handleEventKeyDown = function(event)
	{
		this.keyPressed = event.key;
		Globals.Instance.update();
	}
	
	InputHelper.prototype.handleEventKeyUp = function(event)
	{
		this.keyPressed = null;
	}
	
	InputHelper.prototype.handleEventMouseDown = function(event)
	{
		this.isMouseButtonPressed = true;
		this.mouseClickPos.overwriteWithXYZ
		(
			event.x, event.y, 0
		).subtract
		(
			this.mouseClickPosOffset
		);
		Globals.Instance.update();
	}
	
	InputHelper.prototype.handleEventMouseUp = function(event)
	{
		this.isMouseButtonPressed = false;
	}
	
	
}

function Location(pos, orientation)
{
	this.pos = pos;
	this.orientation = orientation;
}

function Mesh(vertices, faces)
{
	this.vertices = vertices;
	this.faces = faces;
	
	this.edgesBuild();
}
{
	// helper variables
	
	Mesh.drawPos = new Coords();

	// instance methods

	Mesh.prototype.edgesBuild = function()
	{
		this.edges = [];
		var edgeLookup = [];
		
		for (var f = 0; f < this.faces.length; f++)
		{
			var face = this.faces[f];
			var faceVertexIndices = face.vertexIndices;
			
			for (var vi = 0; vi < faceVertexIndices.length; vi++)
			{			
				var vertexIndex = faceVertexIndices[vi];
				var vertex = this.vertices[vertexIndex];

				var viNext = NumberHelper.wrapValueToRangeMax(vi + 1, faceVertexIndices.length);
				var vertexIndexNext = faceVertexIndices[viNext];
				var vertexNext = this.vertices[vertexIndexNext];
				
				var vertexIndicesSorted;
				if (vertexIndex < vertexIndexNext)
				{
					vertexIndicesSorted = [vertexIndex, vertexIndexNext];
				}
				else
				{
					vertexIndicesSorted = [vertexIndexNext, vertexIndex];
				}
				
				var vertexIndexLesser = vertexIndicesSorted[0];
				var vertexIndexGreater = vertexIndicesSorted[1];
				
				var edgesWithVertexIndexLesser = edgeLookup[vertexIndexLesser];
				if (edgesWithVertexIndexLesser == null)
				{
					edgesWithVertexIndexLesser = [];
					edgeLookup[vertexIndexLesser] = edgesWithVertexIndexLesser;
				}
				var edgeExisting = edgesWithVertexIndexLesser[vertexIndexGreater];
				if (edgeExisting == null)
				{
					var edgeNew = new Edge([vertexIndexLesser, vertexIndexGreater])
					edgesWithVertexIndexLesser.push(edgeNew);
					
					this.edges.push(edgeNew);
				}
				else
				{
					// todo
				}
			}
		}
	}
	
	Mesh.prototype.edgesConnectWithFace = function(vertexIndicesForEdges)
	{
		if (vertexIndicesToConnect.length < 3 || vertexIndicesToConnect.length > 4)
		{
			alert("Either 3 or 4 vertices must be selected!");
		}
		else
		{
			var face = new Face(vertexIndicesToConnect.slice(0));
			this.faces.add(face);
			this.edgesBuild();
		}
	}
	

	Mesh.prototype.vertexRemove = function(vertexToRemove)
	{
		// todo
		var vertexIndexBeingRemoved = this.vertices.indexOf(vertexToRemove);
		this.vertices.remove(vertexToRemove);
		
		var facesToRemove = [];
		
		for (var f = 0; f < this.faces.length; f++)
		{
			var face = this.faces[f];
			var faceVertexIndices = face.vertexIndices;
			for (var vi = 0; vi < faceVertexIndices.length; vi++)
			{
				var faceVertexIndex = faceVertexIndices[vi];
				if (faceVertexIndex == vertexIndexBeingRemoved)
				{
					facesToRemove.push(face);
					break;
				}
				else if (faceVertexIndex > vertexIndexBeingRemoved)
				{
					faceVertexIndices[vi] = faceVertexIndex - 1;
				}
			}
		}
		
		for (var fi = 0; fi < facesToRemove.length; fi++)
		{
			var faceToRemove = facesToRemove[fi];
			this.faces.remove(faceToRemove);
		}
		
		this.edgesBuild();
	}
	
	Mesh.prototype.verticesConnectWithEdge = function(vertexIndicesToConnect)
	{
		if (vertexIndicesToConnect.length != 2)
		{
			alert("Exactly 2 vertices must be selected!");
		}
		else
		{
			var edge = new Edge(vertexIndicesToConnect);
			this.edges.push(edge);
		}
	}
	
	Mesh.prototype.verticesRemove = function(verticesToRemove)
	{
		for (var i = 0; i < verticesToRemove.length; i++)
		{
			var vertex = verticesToRemove[i];
			
			this.vertexRemove(vertex);
		}
	}
		
	// drawable
	
	Mesh.prototype.drawToDisplayForScene = function(display, scene)
	{
		var camera = scene.camera;
	
		for (var i = 0; i < this.edges.length; i++)
		{
			var edge = this.edges[i];
			edge.drawToDisplayForCameraAndMesh(display, camera, this);
		}
		
		var drawPos = Mesh.drawPos;
		var vertexHandleRadiusActual = .25;
		
		if (scene.highlightVertices == true)
		{
			for (var i = 0; i < this.vertices.length; i++)
			{
				var vertex = this.vertices[i];
				drawPos.overwriteWith(vertex);
				camera.transformCoordsWorldToView(drawPos);
				var vertexHandleRadiusApparent = camera.transformDistanceWorldToView
				(
					vertexHandleRadiusActual, drawPos.z
				);
				display.drawCircle(drawPos, vertexHandleRadiusApparent, null, display.colorFore);
			}
		}
		
		if (scene.highlightFaces == true)
		{
			for (var i = 0; i < this.faces.length; i++)
			{
				var face = this.faces[i];
				face.drawToDisplayForCameraAndMesh(display, camera, this);
			}
		}
	}
}

function MeshBuilder()
{
	// static class
}
{
	MeshBuilder.cube = function()
	{
		var returnValue = new Mesh
		(
			// vertices
			[
				// top
				new Coords(-1, -1, -1), // 0 - nw
				new Coords(1, -1, -1), // 1 - ne
				new Coords(1, 1, -1), // 2 - se
				new Coords(-1, 1, -1), // 3 - sw
				
				// bottom
				new Coords(-1, -1, 1), // 4 - nw
				new Coords(1, -1, 1), // 5 - ne
				new Coords(1, 1, 1), // 6 - se
				new Coords(-1, 1, 1), // 7 - sw
			],
			// faces
			[
				new Face([0, 3, 2, 1]), // top
				new Face([1, 2, 6, 5]), // east
				new Face([2, 3, 7, 6]), // south 
				new Face([3, 0, 4, 7]), // west
				new Face([0, 1, 5, 4]), // north
				new Face([4, 5, 6, 7]), // bottom
			]
		);
		
		return returnValue;
	}
}

function NumberHelper()
{
	// static class
}
{
	NumberHelper.wrapValueToRangeMax = function(valueToWrap, max)
	{
		while (valueToWrap < 0)
		{
			valueToWrap += max; // rangeSize == max
		}
	
		while (valueToWrap >= max)
		{
			valueToWrap -= max;
		}
		
		return valueToWrap;
	}
}

function Orientation(forward, right, down)
{
	this.forward = forward;
	this.right = right;
	this.down = down;
	this.axesReset();
}
{
	// static methods

	Orientation.fromForwardAndDown = function(forward, down)
	{
		var returnValue = new Orientation(forward.clone(), new Coords(), down.clone());
		returnValue.orthogonalizeAxes().normalizeAxes();
		return returnValue;
	}
	
	Orientation.prototype.axesReset = function()
	{
		this.axes = [this.forward, this.right, this.down];	
	}
	
	// instance methods
	
	Orientation.prototype.normalizeAxes = function()
	{
		for (var i = 0; i < this.axes.length; i++)
		{
			this.axes[i].normalize();
		}
		
		return this;
	}
	
	Orientation.prototype.orthogonalizeAxes = function()
	{
		this.right.overwriteWith(this.down).crossProduct(this.forward);
		this.down.overwriteWith(this.forward).crossProduct(this.right);
		return this;
	}
	
	Orientation.prototype.overwriteWith = function(other)
	{
		this.forward.overwriteWith(other.forward);
		this.right.overwriteWith(other.right);
		this.down.overwriteWith(other.down);
		return this;
	}
}

function Plane(normal, distanceFromOrigin)
{
	this.normal = normal;
	this.distanceFromOrigin = distanceFromOrigin;
}
{
	// helper variables
	Plane.displacementFromPoint0To1 = new Coords();
	Plane.displacementFromPoint1To2 = new Coords();

	// static methods

	Plane.fromPoints = function(points)
	{
		var displacementFromPoint0To1 = Plane.displacementFromPoint0To1;
		var displacementFromPoint1To2 = Plane.displacementFromPoint1To2;
		
		var point0 = points[0];
		var point1 = points[1];
		var point2 = points[2];
		
		displacementFromPoint0To1.overwriteWith(point1).subtract(point0);
		displacementFromPoint1To2.overwriteWith(point2).subtract(point1);
		
		var normal = new Coords().overwriteWith
		(
			displacementFromPoint0To1
		).crossProduct
		(
			displacementFromPoint1To2
		).normalize();
		
		var distanceFromOrigin = normal.dotProduct(point0);
		
		var returnValue = new Plane(normal, distanceFromOrigin);
		
		return returnValue;
	}
}

function Scene(camera, mesh)
{
	this.camera = camera;
	this.mesh = mesh;

	this.selection = new Selection();
	this.cursor = new Cursor();
	
	this.highlightVertices = true;
	this.highlightFaces = true;
}
{
	Scene.prototype.drawToDisplay = function(display)
	{
		this.mesh.drawToDisplayForScene(display, this);
		this.selection.drawToDisplayForCameraAndMesh(display, this.camera, this.mesh);
		this.cursor.drawToDisplayForCamera(display, this.camera);
	}
		
	Scene.prototype.update = function()
	{
		this.update_Input();
		this.camera.constraintsApply();
		this.drawToDisplay(Globals.Instance.display);
	}
	
	Scene.prototype.update_Input = function()
	{
		var session = Globals.Instance.session;
		var inputHelper = Globals.Instance.inputHelper;
		var keyPressed = inputHelper.keyPressed;
		if (keyPressed != null)
		{
			inputHelper.keyPressed = null;
					
			if (keyPressed.startsWith("Arrow") == true)
			{				
				if (keyPressed == "ArrowDown")
				{
					session.viewMoveDown();
				}
				else if (keyPressed == "ArrowLeft")
				{
					session.viewMoveLeft();
				}
				else if (keyPressed == "ArrowRight")
				{
					session.viewMoveRight();
				}				
				else if (keyPressed == "ArrowUp")
				{
					session.viewMoveUp();
				}				
			}
			else if (keyPressed == "=") // +
			{
				session.viewMoveIn();
			}
			else if (keyPressed == "-")
			{
				session.viewMoveOut();	
			}

		}	
		
		if (inputHelper.isMouseButtonPressed == true)
		{
			inputHelper.isMouseButtonPressed = false;
			var cursorPosApparent = this.camera.transformCoordsWorldToView(this.cursor.pos.clone());			
			var cursorPosNext = inputHelper.mouseClickPos.clone();
			cursorPosNext.z = cursorPosApparent.z;
			this.camera.transformCoordsViewToWorld(cursorPosNext);
			this.cursor.pos.overwriteWith(cursorPosNext);
		}
	}
}

function Selection()
{
	this.vertexIndices = [];
}
{	
	Selection.prototype.medianForMesh = function(mesh)
	{
		var returnValue = null;
		if (this.vertexIndicesSelected.length > 0)
		{
			var verticesSelected = this.verticesForMesh(mesh);
			var bounds = Bounds.ofPoints(verticesSelected);
			returnValue = bounds.center();
		}
		return returnValue;
	}
	
	Selection.prototype.verticesForMesh = function(mesh)
	{		
		var returnValues = [];
	
		for (var i = 0; i < this.vertexIndices.length; i++)
		{
			var vertexIndex = this.vertexIndices[i];
			var vertex = mesh.vertices[vertexIndex];
			returnValues.push(vertex);
		}
		
		return returnValues;
	}

	// drawable

	Selection.prototype.drawToDisplayForCameraAndMesh = function(display, camera, mesh)
	{
		var drawPos = display.drawPos;
		var vertexHandleRadiusActual = .25;
		
		for (var i = 0; i < this.vertexIndices.length; i++)
		{
			var vertexIndex = this.vertexIndices[i];
			var vertex = mesh.vertices[vertexIndex];
			drawPos.overwriteWith(vertex);
			camera.transformCoordsWorldToView(drawPos);
			var vertexHandleRadiusApparent = camera.transformDistanceWorldToView
			(
				vertexHandleRadiusActual, drawPos.z
			);
			display.drawCircle(drawPos, vertexHandleRadiusApparent, display.colorFore, null);
		}
	}	
}


function Serializer(knownTypes)
{
	this.knownTypes = knownTypes;

	for (var i = 0; i < this.knownTypes.length; i++)
	{
		var knownType = this.knownTypes[i];
		this.knownTypes[knownType.name] = knownType;
	}
}

{
	// internal classes
	
	function _ArrayWrapper(arrayToWrap)
	{
		this.arrayWrapped = arrayToWrap;
	}
	
	function _FunctionWrapper(functionBody)
	{
		this.functionBody = functionBody;
	}
	
	// methods
	
	Serializer.prototype.deserialize = function(stringToDeserialize)
	{
		var objectDeserialized = JSON.parse(stringToDeserialize);
		this.unwrapArraysRecursively(objectDeserialized);
		this.unwrapFunctionsRecursively(objectDeserialized);
		this.setPrototypeRecursively(objectDeserialized);
		this.deleteClassNameRecursively(objectDeserialized);

		return objectDeserialized;
	}

	Serializer.prototype.serialize = function(objectToSerialize)
	{
		this.wrapFunctionsRecursively(objectToSerialize);
		this.wrapArraysRecursively(objectToSerialize);		
		this.setClassNameRecursively(objectToSerialize);
		var returnValue = JSON.stringify(objectToSerialize);
		this.unwrapArraysRecursively(objectToSerialize);		
		this.unwrapFunctionsRecursively(objectToSerialize);
		this.deleteClassNameRecursively(objectToSerialize);

		return returnValue;
	}
	
	// class names
	
	Serializer.prototype.deleteClassNameRecursively = function(objectToDeleteClassNameOn)
	{
		if (objectToDeleteClassNameOn == null)
		{
			return; //throw "Unrecognized type!"
		}
		
		var className = objectToDeleteClassNameOn.constructor.name;
		if (this.knownTypes[className] != null)
		{
			delete objectToDeleteClassNameOn.className;

			for (var childPropertyName in objectToDeleteClassNameOn)
			{
				var childProperty = objectToDeleteClassNameOn[childPropertyName];
				this.deleteClassNameRecursively(childProperty);
			}
		}
		else if (className == "Array")
		{
			delete objectToDeleteClassNameOn.className;
			for (var i = 0; i < objectToDeleteClassNameOn.length; i++)
			{
				var element = objectToDeleteClassNameOn[i];
				this.deleteClassNameRecursively(element);
			}
		}
	}
	
	Serializer.prototype.setClassNameRecursively = function(objectToSetClassNameOn)
	{
		if (objectToSetClassNameOn == null)
		{
			return; // throw "Unrecognized type!"
		}
		var className = objectToSetClassNameOn.constructor.name;
		
		if (this.knownTypes[className] != null)
		{
			objectToSetClassNameOn.className = className;

			for (var childPropertyName in objectToSetClassNameOn)
			{
				var childProperty = objectToSetClassNameOn[childPropertyName];
				this.setClassNameRecursively(childProperty);
			}
		}
		else if (className == "Array")
		{
			for (var i = 0; i < objectToSetClassNameOn.length; i++)
			{
				var element = objectToSetClassNameOn[i];
				this.setClassNameRecursively(element);
			}
		}
		else if (className == "_ArrayWrapper")
		{
			objectToSetClassNameOn.className = className;

			var arrayWrapped = objectToSetClassNameOn.arrayWrapped;
			this.setClassNameRecursively(arrayWrapped);
		}
	}
	
	// prototypes
	
	Serializer.prototype.setPrototypeRecursively = function(objectToSetPrototypeOn)
	{
		if (objectToSetPrototypeOn == null)
		{
			return; // throw "Unrecognized type!"
		}
	
		var className = objectToSetPrototypeOn.className;
		var typeOfObjectToSetPrototypeOn = this.knownTypes[className];

		if (typeOfObjectToSetPrototypeOn != null)
		{
			objectToSetPrototypeOn.__proto__ = typeOfObjectToSetPrototypeOn.prototype;
	
			for (var childPropertyName in objectToSetPrototypeOn)
			{
				var childProperty = objectToSetPrototypeOn[childPropertyName];
				this.setPrototypeRecursively(childProperty);
			}
		}
		else if (objectToSetPrototypeOn.constructor.name == "Array")
		{
			for (var i = 0; i < objectToSetPrototypeOn.length; i++)
			{
				var element = objectToSetPrototypeOn[i];
				this.setPrototypeRecursively(element);
			}
		}
	}

	// functions
	
	Serializer.prototype.unwrapFunctionsRecursively = function(objectToUnwrapFunctionsOn)
	{
		if (objectToUnwrapFunctionsOn == null)
		{
			return;
		}
				
		var className = objectToUnwrapFunctionsOn.className;
				
		if (this.knownTypes[className] != null)
		{
			for (var childPropertyName in objectToUnwrapFunctionsOn)
			{
				var childProperty = objectToUnwrapFunctionsOn[childPropertyName];
				if (childProperty != null)
				{
					var childPropertyIsFunctionWrapped = 
						(childProperty.className == "_FunctionWrapper");
						
					if (childPropertyIsFunctionWrapped == true)
					{
						var functionBodyAsString = "(" + childProperty.functionBody + ")";
						delete childProperty.functionBody;
						var functionBodyAsFunction = eval(functionBodyAsString);
						functionBodyAsFunction.__proto__ = Function.prototype;

						objectToUnwrapFunctionsOn[childPropertyName] = functionBodyAsFunction;
					}
					else
					{
						this.unwrapFunctionsRecursively(childProperty);
					}
				}
			}
		}
		else if (objectToUnwrapFunctionsOn.constructor.name == "Array")
		{
			for (var i = 0; i < objectToUnwrapFunctionsOn.length; i++)
			{
				var element = objectToUnwrapFunctionsOn[i];
				this.unwrapFunctionsRecursively(element);
			}
		}
	}
	
	Serializer.prototype.wrapFunctionsRecursively = function(objectToWrapFunctionsOn)
	{
		var className = objectToWrapFunctionsOn.constructor.name;
		if (this.knownTypes[className] != null)
		{
			for (var childPropertyName in objectToWrapFunctionsOn)
			{
				var childProperty = objectToWrapFunctionsOn[childPropertyName];
				if (childProperty != null)
				{
					if (childProperty.constructor.name == "Function")
					{
						if (objectToWrapFunctionsOn.__proto__[childPropertyName] == null)
						{
							childProperty = new _FunctionWrapper
							(
								childProperty.toString()
							);
							objectToWrapFunctionsOn[childPropertyName] = childProperty;
						}
					}
					else
					{
						this.wrapFunctionsRecursively(childProperty);
					}
				}
			}
		}
		else if (className == "Array")
		{
			for (var i = 0; i < objectToWrapFunctionsOn.length; i++)
			{
				var element = objectToWrapFunctionsOn[i];
				this.wrapFunctionsRecursively(element);
			}
		}
	}
	
	// arrays
	
	Serializer.prototype.unwrapArraysRecursively = function(objectToUnwrapArraysOn)
	{
		if (objectToUnwrapArraysOn == null)
		{
			return;
		}
				
		var className = objectToUnwrapArraysOn.className;
				
		if (this.knownTypes[className] != null)
		{
			for (var childPropertyName in objectToUnwrapArraysOn)
			{
				var childProperty = objectToUnwrapArraysOn[childPropertyName];
				if (childProperty != null)
				{
					var childPropertyIsArrayWrapped = 
						(childProperty.className == "_ArrayWrapper");
						
					if (childPropertyIsArrayWrapped == true)
					{
						var wrapper = childProperty;
						var arrayWrapped = wrapper.arrayWrapped;
						delete wrapper.arrayWrapped;
						for (var grandchildPropertyName in wrapper)
						{
							var indexOfPropertyWithinArray = wrapper[grandchildPropertyName];
							var grandchildProperty = arrayWrapped[indexOfPropertyWithinArray];
							arrayWrapped[grandchildPropertyName] = grandchildProperty; 		
						}
						
						childProperty = arrayWrapped;
						objectToUnwrapArraysOn[childPropertyName] = childProperty;
					}

					this.unwrapArraysRecursively(childProperty);
				}
			}
		}
		else if (objectToUnwrapArraysOn.constructor.name == "Array")
		{
			for (var i = 0; i < objectToUnwrapArraysOn.length; i++)
			{
				var element = objectToUnwrapArraysOn[i];
				this.unwrapArraysRecursively(element);
			}
		}
	}
		
	Serializer.prototype.wrapArraysRecursively = function(objectToWrapArraysOn)
	{
		var className = objectToWrapArraysOn.constructor.name;
		if (this.knownTypes[className] != null)
		{
			for (var childPropertyName in objectToWrapArraysOn)
			{
				var childProperty = objectToWrapArraysOn[childPropertyName];
				if (childProperty != null)
				{
					if (childProperty.constructor.name == "Array")
					{
						var arrayWrapped = childProperty;

						var wrapper = new _ArrayWrapper(arrayWrapped);
						
						objectToWrapArraysOn[childPropertyName] = wrapper;						
						
						for (var grandchildPropertyName in arrayWrapped)
						{
							if (arrayWrapped.__proto__[grandchildPropertyName] == null)
							{
								var grandchildProperty = arrayWrapped[grandchildPropertyName];
								var indexOfPropertyWithinArray = arrayWrapped.indexOf(grandchildProperty);
								if (indexOfPropertyWithinArray >= 0)
								{
									wrapper[grandchildPropertyName] = indexOfPropertyWithinArray;
								}
							}
						}
						
						this.wrapArraysRecursively(arrayWrapped);
					}
					else
					{
						this.wrapArraysRecursively(childProperty);
					}
				}
			}
		}
		else if (className == "Array")
		{
			for (var i = 0; i < objectToWrapArraysOn.length; i++)
			{
				var element = objectToWrapArraysOn[i];
				this.wrapArraysRecursively(element);
			}
		}
	}
} 

function Session(scene)
{
	this.scene = scene;
}
{
	Session.prototype.initialize = function()
	{
		// todo
	}
		
	Session.prototype.update = function()
	{
		var display = Globals.Instance.display;
		display.clear();
		this.scene.update();
	}
	
	// tools
	
	Session.prototype.deselectAtCursor = function()
	{
		this.selectOrDeselectAtCursor(true);
	}
	
	Session.prototype.sceneLoadFromFile = function(fileToLoad)
	{
		var fileHelper = new FileHelper();
		fileHelper.loadFileAsText
		(
			fileToLoad,
			this.sceneLoadFromFile_2, // callback
			this // contextForCallback
		);
	}
	
	Session.prototype.sceneLoadFromFile_2 = function(fileAsText)
	{
		var sceneSerialized = fileAsText;
		var serializer = this.serializerBuild();
		var sceneDeserialized = serializer.deserialize(sceneSerialized);

		// hack - Serializer currently breaks referential integrity.		
		sceneDeserialized.camera.reinitialize();
		
		this.scene = sceneDeserialized;
		this.update();
	}
		
	Session.prototype.sceneSaveToFile = function()
	{
		var serializer = this.serializerBuild();
		var sceneSerialized = serializer.serialize(this.scene);
		var fileHelper = new FileHelper();
		fileHelper.saveTextStringToFileWithName(sceneSerialized, "Scene.json");
	}

	Session.prototype.selectAll = function()
	{
		var vertexIndicesSelected = this.scene.selection.vertexIndices;
		vertexIndicesSelected.length = 0;
		
		var numberOfVertices = this.scene.mesh.vertices.length;
				
		for (var i = 0; i < numberOfVertices; i++)
		{
			vertexIndicesSelected.push(i);
		}
		this.update();
	}
	
	Session.prototype.selectAtCursor = function()
	{
		this.selectOrDeselectAtCursor(false);
	}
	
	Session.prototype.selectOrDeselectAtCursor = function(deselectRatherThanSelect)
	{
		var inputHelper = Globals.Instance.inputHelper;
		var camera = this.scene.camera;
		var mesh = this.scene.mesh;
		var selection = this.scene.selection;
		var vertices = (deselectRatherThanSelect == true ? selection.verticesForMesh(mesh) : mesh.vertices);
		
		var vertexPosApparent = new Coords();
		var displacement = vertexPosApparent;
		var vertexHandleRadius = 5;
		
		var vertexZClosestSoFar = Number.POSITIVE_INFINITY;
		var vertexIndexClosestSoFar = null;

		for (var i = 0; i < vertices.length; i++)
		{
			var vertex = vertices[i];
			displacement = camera.transformCoordsWorldToView
			(
				vertexPosApparent.overwriteWith
				(
					vertex
				)
			).subtract
			(
				inputHelper.mouseClickPos
			);
			
			var vertexZ = displacement.z;
			displacement.z = 0;
			
			var distanceOfVertexFromClick = displacement.magnitude();
			
			if (distanceOfVertexFromClick <= vertexHandleRadius)
			{
				if (vertexZ < vertexZClosestSoFar)
				{
					vertexZClosestSoFar = vertexZ;
					vertexIndexClosestSoFar = i;
				}
			}
			
		} // end for each vertex
		
		if (vertexIndexClosestSoFar != null)
		{
			if (deselectRatherThanSelect == true)
			{
				selection.vertexIndices.remove(vertexIndexClosestSoFar);
			}
			else
			{
				if (selection.vertexIndices.contains(vertexIndexClosestSoFar) == false)
				{
					selection.vertexIndices.push(vertexIndexClosestSoFar);
				}
			}
		}
		this.update();
	}
	
	Session.prototype.selectNone = function()
	{
		var vertexIndicesSelected = this.scene.selection.vertexIndices;
		vertexIndicesSelected.length = 0;
		this.update();
	}
	
	Session.prototype.transformWithCenterApplyToSelected = function(transform, center)
	{
		// todo - Use center.
	
		var scene = this.scene;
		var selection = scene.selection;
		var selectionVertices = selection.verticesForMesh(scene.mesh);
		for (var i = 0; i < selectionVertices.length; i++)
		{
			var vertex = selectionVertices[i];
			transform.transformCoords(vertex);
		}
	}		
	
	Session.prototype.viewMove = function(direction)
	{
		var distanceToMove = .05;
		var camera = this.scene.camera;
		var cameraPos = camera.loc.pos;
		var offset = direction.clone().multiplyScalar(distanceToMove);
		cameraPos.add(offset);
	}
	
	Session.prototype.viewMoveDown = function() 
	{	
		this.viewMove(this.scene.camera.loc.orientation.down); 
	}
	
	Session.prototype.viewMoveIn = function() 
	{
		var distanceToMove = .1;
		var camera = this.scene.camera;
		camera.constraints[2].distanceToMaintain -= distanceToMove; // hack
	}	
	
	Session.prototype.viewMoveLeft = function() 
	{	
		this.viewMove(this.scene.camera.loc.orientation.right.clone().invert()); 
	}
	
	Session.prototype.viewMoveOut = function() 
	{	
		var distanceToMove = .1;
		var camera = this.scene.camera;
		camera.constraints[2].distanceToMaintain += distanceToMove; // hack
	}		

	Session.prototype.viewMoveRight = function() 
	{	
		this.viewMove(this.scene.camera.loc.orientation.right); 
	}

	Session.prototype.viewMoveUp = function() 
	{	
		this.viewMove(this.scene.camera.loc.orientation.down.clone().invert()); 
	}
	
	Session.prototype.viewSet = function(cameraOrientationNew)
	{
		var scene = this.scene;
		var camera = scene.camera;
		var cameraLoc = camera.loc;
		
		var distanceOfCameraFromOrigin = cameraLoc.pos.magnitude();

		cameraLoc.pos.overwriteWith
		(
			cameraOrientationNew.forward
		).invert().multiplyScalar
		(
			distanceOfCameraFromOrigin
		);
		
		cameraLoc.orientation.overwriteWith(cameraOrientationNew);
	}
	
	Session.prototype.viewSetFront = function()
	{
		this.viewSet
		(
			new Orientation
			(
				new Coords(0, 1, 0), // forward
				new Coords(1, 0, 0), // right
				new Coords(0, 0, 1) // down
			)
		);
	}
	
	Session.prototype.viewSetSelected = function()
	{
		var scene = this.scene;
		var camera = scene.camera;
		var mesh = scene.mesh;
		var selection = scene.selection;
		
		var selectionMedian = selection.medianForMesh(mesh);
		if (selectionMedian != null)
		{
			var constraintLookAt = this.scene.camera.constraints[1]; // hack
			constraintLookAt.targetPos.overwriteWith(selectionMedian);
		}
	}
		
	Session.prototype.viewSetSide = function()
	{
		this.viewSet
		(
			new Orientation
			(
				new Coords(-1, 0, 0), // forward
				new Coords(0, 1, 0), // right
				new Coords(0, 0, 1) // down
			)
		);
	}
			
	Session.prototype.viewSetTop = function()
	{
		this.viewSet
		(
			new Orientation
			(
				new Coords(0, 0, 1), // forward
				new Coords(1, 0, 0), // right
				new Coords(0, -1, 0) // down
			)
		);
	}
	
	// serializer
	
	Session.prototype.serializerBuild = function()
	{
		return new Serializer
		([
			Camera,
			Constraint_KeepDistance,
			Constraint_LookAt,
			Constraint_Upright,
			Coords,
			Cursor,
			Edge,
			Face,
			Location,
			Mesh,
			Orientation,
			Scene,
			Selection,
			Transform_Multiple,
			Transform_Orient,
			Transform_OrientInverse,
			Transform_Perspective,
			Transform_PerspectiveInverse,
			Transform_Rotate,
			Transform_Scale,
			Transform_Translate,
			Transform_TranslateInverse,
		]);
	}
}

function Transform_Multiple(children)
{
	this.children = children;
}
{
	Transform_Multiple.prototype.transformCoords = function(coordsToTransform)
	{
		for (var i = 0; i < this.children.length; i++)
		{
			var child = this.children[i];
			child.transformCoords(coordsToTransform);
		}
		
		return coordsToTransform;
	}
}

function Transform_Orient(orientation)
{
	this.orientation = orientation;
}
{
	Transform_Orient.prototype.transformCoords = function(coordsToTransform)
	{
		coordsToTransform.overwriteWithXYZ
		(
			this.orientation.right.dotProduct(coordsToTransform),
			this.orientation.down.dotProduct(coordsToTransform),
			this.orientation.forward.dotProduct(coordsToTransform)
		);
	
		return coordsToTransform;
	}
}

function Transform_OrientInverse(orientation)
{
	this.orientation = orientation;
}
{
	Transform_OrientInverse.prototype.transformCoords = function(coordsToTransform)
	{
		var original = coordsToTransform.clone();
		
		coordsToTransform.overwriteWith
		(
			this.orientation.right.clone().multiplyScalar(original.x)
		).add
		(
			this.orientation.down.clone().multiplyScalar(original.y)
		).add
		(
			this.orientation.forward.clone().multiplyScalar(original.z)
		);
	
		return coordsToTransform;
	}
}

function Transform_Perspective(focalLength)
{
	this.focalLength = focalLength;
}
{
	Transform_Perspective.prototype.transformCoords = function(coordsToTransform)
	{
		if (coordsToTransform.z != 0)
		{
			var multiplier = this.focalLength / coordsToTransform.z;
			coordsToTransform.x *= multiplier;
			coordsToTransform.y *= multiplier;
			coordsToTransform.z *= this.focalLength;
		}
		return coordsToTransform;
	}
}

function Transform_PerspectiveInverse(focalLength)
{
	this.focalLength = focalLength;
}
{
	Transform_PerspectiveInverse.prototype.transformCoords = function(coordsToTransform)
	{
		if (coordsToTransform.z != 0)
		{
			var multiplier = coordsToTransform.z / (this.focalLength * this.focalLength); // hack
			coordsToTransform.x *= multiplier;
			coordsToTransform.y *= multiplier;
			coordsToTransform.z /= this.focalLength; // fix
		}
		return coordsToTransform;
	}
}

function Transform_Rotate(axis, angleInCycles)
{
	this.axis = axis;
	this.angleInCycles = angleInCycles;
	
	var orientation = Orientation.fromForwardAndDown
	(
		this.axis.clone().right(), 
		this.axis
	);
	this.transformOrient = new Transform_Orient(orientation);
}
{
	Transform_Rotate.prototype.transformCoords = function(coordsToTransform)
	{
		this.transformOrient.transformCoords
		(
			coordsToTransform
		);
		
		// todo
		var one = 1;
		
		return coordsToTransform;
	}
}

function Transform_Scale(scaleFactors)
{
	this.scaleFactors = scaleFactors;
}
{
	Transform_Scale.prototype.transformCoords = function(coordsToTransform)
	{
		return coordsToTransform.multiply(this.scaleFactors);
	}
}

function Transform_Translate(offset)
{
	this.offset = offset;
}
{
	Transform_Translate.prototype.transformCoords = function(coordsToTransform)
	{
		return coordsToTransform.add(this.offset);
	}
}

function Transform_TranslateInverse(offset)
{
	this.offset = offset;
}
{
	Transform_TranslateInverse.prototype.transformCoords = function(coordsToTransform)
	{
		return coordsToTransform.subtract(this.offset);
	}
}

// run

main();

</script>

</body>
</html>

Posted in Uncategorized | Tagged , , , , | Leave a comment

Drawing a Rotated Image to an HTML5 Canvas

The JavaScript code below, when run, prompts the user to specify an image file to be uploaded, and then displays that image, scaled to 100 x 100 pixels and rotated 45 degrees clockwise in the center of an HTML5 canvas.

ImageRotatedOnHTML5Canvas.png


<html>
<body>

<!-- ui -->

<input type="file" onchange="inputFileImageToDraw_Changed(this);" />

<!-- ui ends -->

<script type="text/javascript">

// ui event handlers

function inputFileImageToDraw_Changed(inputFileImageToDraw)
{
	var fileToLoad = inputFileImageToDraw.files[0];
	if (fileToLoad != null)
	{
		if (fileToLoad.type.match("image.*") != null)
		{
			var fileReader = new FileReader();
			fileReader.onload =
				inputFileImageToDraw_Changed_FileLoaded;
			fileReader.readAsDataURL(fileToLoad);
		}
	}
}

function inputFileImageToDraw_Changed_FileLoaded(fileLoadedEvent)
{
	var imageToDraw = document.createElement("img");
	imageToDraw.src = fileLoadedEvent.target.result;

	var display = new Display
	(
		new Coords(200, 200), "Gray", "Black"
	);
	display.initialize();

	display.clear();

	display.drawImageAtPosWithSizeAndRotation
	(
		imageToDraw,
		new Coords(100, 100), // pos
		new Coords(100, 100), // size
		.125 // rotationInCycles
	);

}
// classes

function Coords(x, y)
{
	this.x = x;
	this.y = y;
}

function Display(sizeInPixels, colorFore, colorBack)
{
	this.sizeInPixels = sizeInPixels;
	this.colorFore = colorFore;
	this.colorBack = colorBack;
}
{
	// constants

	Display.RadiansPerCycle = Math.PI * 2;

	// methods

	Display.prototype.clear = function()
	{
		this.graphics.fillStyle = this.colorBack;
		this.graphics.fillRect
		(
			0, 0, this.sizeInPixels.x, this.sizeInPixels.y
		);

		this.graphics.strokeStyle = this.colorFore;
		this.graphics.strokeRect
		(
			0, 0, this.sizeInPixels.x, this.sizeInPixels.y
		);
	}

	Display.prototype.drawImageAtPosWithSizeAndRotation = function
	(
		imageToDraw, pos, size, rotationInCycles
	)
	{
		this.graphics.save();
		this.graphics.translate(pos.x, pos.y);
		var rotationInRadians =
			rotationInCycles * Display.RadiansPerCycle;
		this.graphics.rotate(rotationInRadians);
		this.graphics.drawImage
		(
			imageToDraw,
			0 - size.x / 2, 0 - size.y / 2, // pos
			size.x, size.y // size
		);
		this.graphics.restore();
	}

	Display.prototype.initialize = function()
	{
		this.canvas = document.createElement("canvas");
		this.canvas.width = this.sizeInPixels.x;
		this.canvas.height = this.sizeInPixels.y;
		document.body.appendChild(this.canvas);

		this.graphics = this.canvas.getContext("2d");
	}
}

</script>

</body>
</html>

Posted in Uncategorized | Tagged , , , , | Leave a comment

A Simple Flashcard System in JavaScript

The JavaScript program below, when run, prompts for a lesson file, and, when one is provided, quizzes the user with the questions from it. When the user answers all questions correctly three times in a row, the lesson is counted as complete.

The lesson file should be a text file. Each line in the file represents a question and its correct response, delimited by a semicolon.

To see the code in action, copy it into an .html file and open that file in a web browser that runs JavaScript. Or, for an online version, visit https://thiscouldbebetter.neocities.org/flashcards.html.

UPDATE 2017/01/13 – The program has been modified to allow the user to change the number of times each question must be answered correctly, as well as to reverse the question and answer. Also, some very basic input validation has been added.

flashcards


<html>
<body>

	<!-- user interface -->
	<div style="border:1px solid">
		<label>Lesson File:</label>
		<input id="inputFileLesson" type="file"></input>
		
		<div>
			<label>Reverse Question and Answer:</label>
			<input id="checkboxReverse" type="checkbox"></input>
		</div>

		<div>
			<label>Times Each Question Must Be Answered Correctly:</label>		
			<input id="inputTimesCorrectPerQuestion" type="number" value="3"></input>
		</div>

		<button id="buttonStart" onclick="buttonStart_Clicked();">Start Lesson</button>
	</div>

	<div style="border:1px solid">
		<div>
			<div><label>Question:</label></div>
			<textarea id="textareaPresentation" readonly="readonly"></textarea>
		</div>

		<div>
			<div><label>Answer:</label></div>
			<input id="inputResponse"></input>
		</div>

		<p id="pStatusMessage">Upload a valid lesson file and click the Start button to begin.</p>
	</div>
	
<script type="text/javascript">

// ui event handlers

function buttonStart_Clicked()
{
	var inputFileLesson = document.getElementById("inputFileLesson");
	var file = inputFileLesson.files[0];
	
	if (file == null)
	{
		alert("A valid lesson file must be specified by clicking the Lesson File button.");
	}
	else
	{
		var fileReader = new FileReader();
		fileReader.onload = inputFileLesson_Changed_FileLoaded;
		fileReader.readAsText(file);
	}
}

function inputFileLesson_Changed_FileLoaded(event)
{
	var fileContents = event.target.result;

	var questionsAsStrings = fileContents.split("\n");

	var questions = [];

	for (var i = 0; i < questionsAsStrings.length; i++)
	{
		var questionAsString = questionsAsStrings[i];

		var presentationAndResponse = questionAsString.split(";");

		var presentation = presentationAndResponse[0];
		var response = presentationAndResponse[1].trim();

		var question = new Question(presentation, response);

		questions.push(question);
	}
	
	var checkboxReverse = document.getElementById("checkboxReverse");
	var arePresentationAndResponseReversed = checkboxReverse.checked;
	
	var inputTimesCorrectPerQuestion = document.getElementById
	(
		"inputTimesCorrectPerQuestion"
	);
	var timesCorrectPerQuestion = parseInt(inputTimesCorrectPerQuestion.value);
	if (isNaN(timesCorrectPerQuestion) == true || timesCorrectPerQuestion <= 0)
	{
		alert("A positive number must be entered in the Times Each Question Must Be Answered Correctly box.");
	}
	else
	{
		var lessonDefn = new LessonDefn
		(
			questions, 
			arePresentationAndResponseReversed,
			timesCorrectPerQuestion
		);

		Globals.Instance.initialize(lessonDefn);
	}
}

// classes

function DisplayHelper()
{
	// do nothing
}
{
	DisplayHelper.prototype.displayLessonRun = function(lessonRun)
	{
		var textareaPresentation = document.getElementById
		(
			"textareaPresentation"
		);

		var questionCurrent = lessonRun.questionCurrent();
		var presentation = 
		(
			lessonRun.defn.arePresentationAndResponseReversed == true 
			? questionCurrent.responseCorrect 
			: questionCurrent.presentation
		);
		textareaPresentation.value = presentation;

		var inputResponse = document.getElementById("inputResponse");
		inputResponse.value = "";
		inputResponse.focus();

		var pStatusMessage = document.getElementById("pStatusMessage");
		pStatusMessage.innerHTML = lessonRun.statusMessage;
	}
}

function Globals()
{
	// do nothing
}
{
	// instance

	Globals.Instance = new Globals();

	// methods

	Globals.prototype.initialize = function(lessonDefn)
	{
		this.lessonDefn = lessonDefn;

		this.lessonRun = new LessonRun(this.lessonDefn);

		this.displayHelper = new DisplayHelper();
		this.inputHelper = new InputHelper();

		this.lessonRun.initialize();

		this.inputHelper.initialize();

		this.displayHelper.displayLessonRun(this.lessonRun);
	}
}


function InputHelper()
{
	// do nothing
}
{
	InputHelper.prototype.initialize = function()
	{
		document.body.onkeydown = this.handleEventKeydown.bind(this);
	}

	// event handlers

	InputHelper.prototype.handleEventKeydown = function(event)
	{
		if (event.key == "Enter")
		{
			var inputResponse = document.getElementById("inputResponse");
			var responseActual = inputResponse.value.trim();
			
			var lessonRun = Globals.Instance.lessonRun;
			var questionCurrent = lessonRun.questionCurrent();
			var responseExpected = 
			(
				lessonRun.defn.arePresentationAndResponseReversed == true 
				? questionCurrent.presentation 
				: questionCurrent.responseCorrect
			);
	
			var responseRecordCurrent = lessonRun.responseRecordCurrent();

			if (responseActual == responseExpected)
			{
				responseRecordCurrent.timesCorrect++;
				lessonRun.statusMessage = 
					"Correct!  The previous question has been answered correctly "
					+ responseRecordCurrent.timesCorrect
					+ " times in a row.";
			}
			else
			{
				responseRecordCurrent.timesCorrect = 0;
				lessonRun.statusMessage = 
					"Incorrect!  The correct answer was "
					+ responseExpected
					+ ".  You answered "
					+ responseActual
					+ ".";	
			}

			if (lessonRun.isComplete() == true)
			{
				lessonRun.statusMessage = 
					"Lesson complete!  Each question was answered correctly " 
					+ lessonRun.defn.timesCorrectPerQuestion
					+ " times in a row.";
				document.body.onkeydown = null;
			}
			else
			{
				lessonRun.questionAdvance();
			}

			Globals.Instance.displayHelper.displayLessonRun
			(
				lessonRun
			);
		}
		
	}
}

function LessonDefn(questions, arePresentationAndResponseReversed, timesCorrectPerQuestion)
{
	this.questions = questions;
	this.arePresentationAndResponseReversed = arePresentationAndResponseReversed;
	this.timesCorrectPerQuestion = timesCorrectPerQuestion;
}

function LessonRun(defn)
{
	this.defn = defn;
	this.statusMessage = 
		"Each question must be correctly answered " 
		+ this.defn.timesCorrectPerQuestion
		+ " times in a row.";
}
{
	LessonRun.prototype.initialize = function()
	{
		this.questionIndexCurrent = 0;
		this.responseRecords = [];

		var questions = this.defn.questions;

		for (var i = 0; i < questions.length; i++)
		{
			var question = questions[i];
			var responseRecord = new ResponseRecord();
			this.responseRecords.push(responseRecord);
		}
	}

	LessonRun.prototype.isComplete = function()
	{
		var returnValue = true;

		var timesRequired = this.defn.timesCorrectPerQuestion;

		for (var i = 0; i < this.responseRecords.length; i++)
		{
			var responseRecord = this.responseRecords[i];
			if (responseRecord.timesCorrect < timesRequired)
			{
				returnValue = false;
				break;
			}
		}	

		return returnValue;
	}

	LessonRun.prototype.questionAdvance = function()
	{
		var isFirstTime = true;
		var timesRequired = this.defn.timesCorrectPerQuestion;

		while 
		(
			isFirstTime == true 
			|| this.responseRecordCurrent().timesCorrect >= timesRequired
		)
		{
			isFirstTime = false;

			this.questionIndexCurrent++;

			if (this.questionIndexCurrent >= this.defn.questions.length)
			{
				this.questionIndexCurrent = 0;
			}
		}
	}

	LessonRun.prototype.questionCurrent = function()
	{
		return this.defn.questions[this.questionIndexCurrent];
	}

	LessonRun.prototype.responseRecordCurrent = function()
	{
		return this.responseRecords[this.questionIndexCurrent];
	}

}

function Question(presentation, responseCorrect)
{
	this.presentation = presentation;
	this.responseCorrect = responseCorrect;
}

function ResponseRecord()
{
	this.timesCorrect = 0;
}

</script>

</body>
</html>

Posted in Uncategorized | Tagged , , , | Leave a comment

Accessing a Google API using OAuth2 in JavaScript

The instructions below describe how to create a JavaScript web application that accesses one of Google’s APIs using OAuth2.

1. If you have not already done so, install and activate a web server. If using Windows, the XAMPP package is recommended. Make a note of the location of the root document folder for the web server, perhaps at “C:\xampp\htdocs”.

2. Open a web browser.

3. If you have not already done so, create a Google account and then log in to that account.

4. In the web browser, navigate to the URL “https://console.developers.google.com“.

5. Click the “Create a project” button. In the dialog that appears, enter the value “OAuth2Test” in the Project name box, then click the “Create” button. Wait for the new project to be created.

6. A new dialog box titled “Credentials” will appear. (If it doesn’t, click on the “Credentials” item in the left-hand menu pane.) Click the “Create credentials” button, then select the entry titled “API key” from the list that appears.

7. A new dialog titled “API key created” will appear. Record the value in the “Your API key” box for later use, then click the “Close” button.

8. Click the “Create credentials” button again, and this time select the entry titled “OAuth client ID” from the list that appears. A warning message will appear stating that, “To create an OAuth client ID, you must first set a product name on the consent screen”.

9. Click the “Configure consent screen” button. On the “OAuth consent screen” tab that appears, enter the value “OAuth2Test” in the “Product name shown to users” box, then click the “Save” button.

10. Back on the “Create client ID” screen, click the “Web application” radio button. Enter the value “Oauth2Test” in the Name box that appears, and the value “http://localhost” in both the “Authorized JavaScript origins” and “Authorized redirect URIs” boxes beneath.

11. Click the “Create” button. Record the client ID and client secret that are displayed in the dialog that appears.

12. Enable the Google People API for the newly created project by navigating to the URL “https://console.developers.google.com/apis/api/people.googleapis.com/overview” and clicking the “Enable” button.

13. In the web server’s document folder (as determined in a previous step), create a new text file named “OAuth2.html”, containing the text from the code listing below. Substitute the API key and client ID obtained in the previous step at the end of the program, where indicated. This code is adapted from a sample found at the URL “https://developers.google.com/api-client-library/javascript/samples/samples“, under the heading “Authorizing and Making Authorized Requests”.


<html>
	
<body>

<button id="authorize-button" onclick="buttonAuthorize_Clicked()">Authorize</button>
<button id="signout-button" onclick="buttonSignout_Clicked()">Sign Out</button>

<script src="https://apis.google.com/js/api.js"></script>

<script type="text/javascript">

// ui event handlers

function buttonAuthorize_Clicked(event) 
{
	googleAPIClient.signIn();
}

function buttonSignout_Clicked(event) 
{
	googleAPIClient.signOut();
}

function GoogleAPIClient(googleAPI, apiKey, clientID)
{
	this.googleAPI = googleAPI;
	this.apiKey = apiKey;
	this.clientID = clientID;

	this.initialize();
}
{
	// public methods

	GoogleAPIClient.prototype.getUserGivenName = function() 
	{
		this.googleAPI.client.people.people.get
		(
			{
				resourceName: "people/me"
			}
		).then
		(
 			this.getUserGivenName_Success,
			this.getUserGivenName_Error
		);
	}

	GoogleAPIClient.prototype.getUserGivenName_Success = function(response) 
	{
		var givenName = 
			response.result.names[0].givenName;

		console.log
		(
			"Hello, " + givenName + "!"
		);
	}

	GoogleAPIClient.prototype.getUserGivenName_Error = function(reason) 
	{
		console.log
		(
			"Error: " + reason.result.error.message
		);
	}

	GoogleAPIClient.prototype.signIn = function()
	{
		this.googleAPI.auth2.getAuthInstance().signIn();
	}

	GoogleAPIClient.prototype.signOut = function()
	{
		this.googleAPI.auth2.getAuthInstance().signOut();
	}

	// initialization

	GoogleAPIClient.prototype.initialize = function() 
	{
		// Load the API client and auth2 library

		this.googleAPI.load
		(
			"client:auth2", 
			this.initialize_2.bind(this)
		);
	}

	GoogleAPIClient.prototype.initialize_2 = function() 
	{
		var initializerObject = 			
		{
			apiKey: this.apiKey,
			discoveryDocs: 
			[
				"https://people.googleapis.com"
					+ "/$discovery/rest?version=v1"
			],
			clientId: this.clientID,
			scope: "profile"
		}

		this.googleAPI.client.init
		(
			initializerObject
		).then
		(
			this.initialize_3.bind(this)
		);
	}

	GoogleAPIClient.prototype.initialize_3 = function() 
	{
		var authInstance = this.googleAPI.auth2.getAuthInstance();

		// Listen for sign-in state changes.
		authInstance.isSignedIn.listen
		(
			this.updateSigninStatus.bind(this)
		);

		// Handle the initial sign-in state.
		this.updateSigninStatus
		(
			authInstance.isSignedIn.get()
		);
	}

	// helper methods

	GoogleAPIClient.prototype.updateSigninStatus = function(isSignedIn) 
	{
		if (isSignedIn == true) 
		{
			this.getUserGivenName();
		}
	}

} // end class GoogleAPIClient

// run 

var googleAPIClient = new GoogleAPIClient
(
	gapi,
	"INSERT_API_KEY_HERE",
	"CLIENT_ID_HERE.apps.googleusercontent.com"
);

</script>

</body>

</html>

14. In the web browser, open the debugging pane (perhaps by pressing the F12 key) and make sure the Console sub-pane is visible.

15. In the web browser, navigate to the URL “http://localhost/OAuth2.html“. A page with two buttons, named “Authorize” and “Sign Out” will appear.

16. In another tab of the web browser, sign out of your Google account.

17. Click the “Authorize” button. On the dialog that appears, enter the username and password of a valid Google account (such as the one you just singed out of). If a dialog appears prompting for permissions, click the “Allow” button.

18. Verify that a customized greeting message is displayed in the debugging console pane.

Posted in Uncategorized | Tagged , , , | Leave a comment

A Network Routing Simulation in JavaScript with the Bellman-Ford Algorithm

The JavaScript code below, when run, simulates routing of packets within a simple computer network using the Bellman-Ford routing algorithm. To see it in action, copy it into an .html file and open that file in a web browser that runs JavaScript.

The simulation will be updated once per second, as packets are routed towards their destination nodes. New packets can be created by entering the appropriate text in the box and clicking the “Do” button. The nodes will share routing tables among themselves every 5 seconds.

networkroutingsimulator



<html>
<body>

<script type="text/javascript">

// main

function main()
{
	var display = new Display
	(
		new Coords(400, 300), // sizeInPixels
		10, // fontHeightInPixels
		"White", // colorBackground
		"Gray" // colorForeground
	);

	var network = new Network
	(
		new Coords(20, 20), // nodeSizeInPixels
		5, // timerTicksPerRouteShare
		// nodes
		[
			new Node("Node0", new Coords(30, 30)),
			new Node("Node1", new Coords(90, 90)),
			new Node("Node2", new Coords(150, 150)),
			new Node("Node3", new Coords(90, 210)),

		],
		// links
		[
			new Link( ["Node0", "Node1"], 8),
			new Link( ["Node1", "Node2"], 8),
			new Link( ["Node2", "Node3"], 8),
		], 
		// packets
		[
			// todo
		]
	);

	Globals.Instance.initialize
	(
		display,
		1, // timerTicksPerSecond
		network
	);
}

// extensions

function ArrayExtensions()
{
	// extension class
}
{
	Array.prototype.addLookups = function(keyName)
	{
		for (var i = 0; i < this.length; i++)
		{
			var element = this[i];
			var key = element[keyName];
			this[key] = element;
		}

		return this;
	}

	Array.prototype.remove = function(element)
	{
		var indexOfElement = this.indexOf(element);
		if (indexOfElement >= 0)
		{
			this.splice(indexOfElement, 1);
		}
		return this;
	}
}

// classes

function Coords(x, y)
{
	this.x = x;
	this.y = y;
}
{
	// instances

	Coords.Instances = new Coords_Instances();

	function Coords_Instances()
	{
		this.Zeroes = new Coords(0, 0);
	}

	// methods

	Coords.prototype.add = function(other)
	{
		this.x += other.x;
		this.y += other.y;
		return this;
	}

	Coords.prototype.clone = function()
	{
		return new Coords(this.x, this.y);
	}

	Coords.prototype.divideScalar = function(scalar)
	{
		this.x /= scalar;
		this.y /= scalar;
		return this;
	}

	Coords.prototype.multiplyScalar = function(scalar)
	{
		this.x *= scalar;
		this.y *= scalar;
		return this;
	}

}

function Display(sizeInPixels, fontHeightInPixels, colorBackground, colorBorder)
{
	this.sizeInPixels = sizeInPixels;
	this.fontHeightInPixels = fontHeightInPixels;
	this.colorBackground = colorBackground;
	this.colorBorder = colorBorder;
}
{
	Display.prototype.clear = function()
	{
		this.drawRectangle
		(
			Coords.Instances.Zeroes, 
			this.sizeInPixels, 
			this.colorBackground,
			this.colorBorder
		);
	}

	Display.prototype.drawLine = function(posFrom, posTo, color)
	{
		this.graphics.strokeStyle = color;
		this.graphics.beginPath();
		this.graphics.moveTo(posFrom.x, posFrom.y);
		this.graphics.lineTo(posTo.x, posTo.y);
		this.graphics.stroke();
	}

	Display.prototype.drawRectangle = function(pos, size, colorFill, colorBorder)
	{
		this.graphics.fillStyle = colorFill;
		this.graphics.fillRect(pos.x, pos.y, size.x, size.y);

		this.graphics.strokeStyle = colorBorder;
		this.graphics.strokeRect(pos.x, pos.y, size.x, size.y);	
	}

	Display.prototype.drawText = function(text, pos, color)
	{
		this.graphics.fillStyle = color;
		this.graphics.fillText
		(
			text, pos.x, pos.y
		);
	}

	Display.prototype.initialize = function()
	{
		var canvas = document.createElement("canvas");
		canvas.width = this.sizeInPixels.x;
		canvas.height = this.sizeInPixels.y;

		this.graphics = canvas.getContext("2d");

		this.graphics.font = 
			this.fontHeightInPixels + "px sans-serif";

		document.body.appendChild(canvas);
	}

}

function Globals()
{
	// do nothing
}
{
	// instance

	Globals.Instance = new Globals();
	
	// methods

	Globals.prototype.initialize = function
	(
		display, timerTicksPerSecond, network
	)
	{
		this.display = display;
		this.display.initialize();

		this.network = network;
		this.network.initialize();

		var millisecondsPerTimerTick = 
			1000 / timerTicksPerSecond;

		this.timerTicksSoFar = 0;

		this.handleEventTimerTick();

		this.timer = setInterval
		(
			this.handleEventTimerTick.bind(this),
			millisecondsPerTimerTick
		);
	}

	Globals.prototype.handleEventTimerTick = function()
	{
		this.network.updateForTimerTick();
		this.network.drawToDisplay(this.display);

		this.timerTicksSoFar++;
	}
}

function Link(namesOfNodesLinked, costToTraverse)
{
	this.namesOfNodesLinked = namesOfNodesLinked;
	this.costToTraverse = costToTraverse;
}
{
	Link.prototype.node0 = function()
	{
		return Globals.Instance.network.nodes[this.namesOfNodesLinked[0]];
	}

	Link.prototype.node1 = function()
	{
		return Globals.Instance.network.nodes[this.namesOfNodesLinked[1]];
	}

	Link.prototype.nodes = function()
	{
		return [ this.node0(), this.node1() ];
	}

	// drawable

	Link.prototype.drawToDisplay = function(display)
	{
		var node0Center = this.node0().center(); 
		var node1Center = this.node1().center();

		display.drawLine
		(
			node0Center, node1Center, "Gray"
		);

		var midpoint = node0Center.add
		(
			node1Center
		).divideScalar(2);

		display.drawText
		(
			"" + this.costToTraverse, 
			midpoint,
			"Gray"
		);
	}	
}

function Network(nodeSizeInPixels, timerTicksPerRouteShare, nodes, links, packets)
{
	this.nodeSizeInPixels = nodeSizeInPixels;
	this.timerTicksPerRouteShare = timerTicksPerRouteShare;
	this.nodes = nodes;
	this.links = links;
	this.packets = packets;

	this.nodes.addLookups("name");
	this.packetsToRemove = [];
}
{
	Network.prototype.initialize = function()
	{
		for (var i = 0; i < this.links.length; i++)
		{
			var link = this.links[i];
			var nodesLinked = link.nodes();
			for (var n = 0; n < nodesLinked.length; n++)
			{
				var nodeSource = nodesLinked[n];
				var nodeTarget = nodesLinked[1 - n]; 

				var linkToNeighbor = new Link
				(
					[
						nodeSource.name, 
						nodeTarget.name,
					],
					link.costToTraverse
				);

				nodeSource.linksToNeighbors.push(linkToNeighbor);				
			}
		}

		for (var i = 0; i < this.nodes.length; i++)
		{
			var node = this.nodes[i];
			node.initialize();
		}

		this.domElementUpdate();
	}

	Network.prototype.routesShareAmongNodes = function()
	{
		for (var i = 0; i < this.nodes.length; i++)
		{
			var node = this.nodes[i];
			node.routesShareWithNeighbors();
		}
	}

	Network.prototype.updateForTimerTick = function()
	{
		var timerTicksSoFar = Globals.Instance.timerTicksSoFar;
		if 
		(
			timerTicksSoFar != 0 
			&& timerTicksSoFar % this.timerTicksPerRouteShare == 0
		)
		{
			this.routesShareAmongNodes();	
		}

		this.packetsToRemove.length = 0;

		for (var i = 0; i < this.packets.length; i++)
		{
			var packet = this.packets[i];
			if (packet.isDelivered() == true)
			{
				this.packetsToRemove.push(packet);
				packet.nodeCurrent().packetsDelivered.push(packet);
			}
			else
			{
				packet.updateForTimerTick();
			}
		}

		for (var i = 0; i < this.packetsToRemove.length; i++)
		{
			var packet = this.packetsToRemove[i];
			this.packets.remove(packet);
		}		
	}

	// dom

	Network.prototype.domElementUpdate = function()
	{
		if (this.domElement == null)
		{
			var divNetwork = document.createElement("div");

			var divControls = document.createElement("div");

			var labelCommand = document.createElement("label");
			labelCommand.innerHTML = "Command:";
			divControls.appendChild(labelCommand);

			var inputCommandText = document.createElement("input");
			inputCommandText.id = "inputCommandText";
			inputCommandText.value = "packet Node0 Node3 data"
			divControls.appendChild(inputCommandText);
			
			var buttonCommandPerform = document.createElement("button");
			buttonCommandPerform.innerHTML = "Do";
			buttonCommandPerform.onclick = this.buttonCommandPerform_Clicked.bind(this);
			divControls.appendChild(buttonCommandPerform);

			var buttonCommandHelp = document.createElement("button");
			buttonCommandHelp.innerHTML = "Help";
			buttonCommandHelp.onclick = this.buttonCommandHelp_Clicked.bind(this);
			divControls.appendChild(buttonCommandHelp);

			divNetwork.appendChild(divControls);

			document.body.appendChild(divNetwork);

			this.domElement = divNetwork;
		}
	}

	Network.prototype.buttonCommandHelp_Clicked = function()
	{
		var message = "Valid command format: 'packet [from] [to] [data]'";
		alert(message);
	}

	Network.prototype.buttonCommandPerform_Clicked = function()
	{
		var inputCommandText = document.getElementById("inputCommandText");
		var commandText = inputCommandText.value;
		var commandArguments = commandText.split(" ");
		var operationName = commandArguments[0];
		
		if (operationName == "packet")
		{
			if (commandArguments.length != 4)
			{
				alert("Wrong number of arguments!");
			}

			var nodeNameSource = commandArguments[1]; 
			var nodeTargetName = commandArguments[2];
			var payload = commandArguments[3];

			var nodeSource = this.nodes[nodeNameSource];
			var nodeTarget = this.nodes[nodeTargetName];

			if (nodeSource == null)
			{
				alert("Invalid source node name: " + nodeNameSource);
			}
			else if (nodeSource == null)
			{
				alert("Invalid target node name: " + nodeNameSource);
			}
			else
			{
				var packet = new Packet
				(
					nodeNameSource, nodeTargetName, payload
				);
				this.packets.push(packet);

			}
		}
		else
		{
			alert("Unrecognized command!");
		}
	}

	// drawable

	Network.prototype.drawToDisplay = function(display)
	{
		display.clear();

		for (var i = 0; i < this.links.length; i++)
		{
			var link = this.links[i];
			link.drawToDisplay(display);
		}

		for (var i = 0; i < this.nodes.length; i++)
		{
			var node = this.nodes[i];
			node.drawToDisplay(display);
		}

		for (var i = 0; i < this.packets.length; i++)
		{
			var packet = this.packets[i];
			packet.drawToDisplay(display);
		}

		display.drawText
		(
			"Time:" + Globals.Instance.timerTicksSoFar, 
			new Coords(10, 10), 
			"Gray"
		);

		display.drawText
		(
			"Routes shared every " 
				+ this.timerTicksPerRouteShare 
				+ " ticks.", 
			new Coords(10, 20), 
			"Gray"
		);
	}
}

function Node(name, pos)
{
	this.name = name;
	this.pos = pos;
	
	this.linksToNeighbors = [];
	this.routes = [];
	this.packetsDelivered = [];
}
{
	Node.prototype.center = function()
	{
		return this.size().clone().divideScalar(2).add(this.pos);
	}

	Node.prototype.initialize = function()
	{
		for (var i = 0; i < this.linksToNeighbors.length; i++)
		{
			var link = this.linksToNeighbors[i];

			var neighborName = link.namesOfNodesLinked[1];

			this.linksToNeighbors[neighborName] = link;

			var route = new Route
			(
				neighborName, // nodeTargetName, 
				link.costToTraverse, // totalCostToTarget, 
				neighborName // nodeNextName
			);
			this.routes.push(route);
		}

		this.routes.addLookups("nodeTargetName");
	}

	Node.prototype.routesShareWithNeighbors = function()
	{
		for (var n = 0; n < this.linksToNeighbors.length; n++)
		{
			var link = this.linksToNeighbors[n];
			var nodeNeighbor = link.node1();
			nodeNeighbor.routesUpdateFromNeighbor
			(
				this.name, 
				link.costToTraverse, 
				this.routes
			);
		}
	}

	Node.prototype.routesUpdateFromNeighbor = function
	(
		neighborName, costToNeighbor, routesFromNeighbor
	)
	{
		for (var r = 0; r < routesFromNeighbor.length; r++)
		{
			var routeFromNeighbor = routesFromNeighbor[r];

			var totalCostToTargetThroughNeighbor = 
				costToNeighbor + routeFromNeighbor.totalCostToTarget;

			var nodeTargetName = routeFromNeighbor.nodeTargetName;

			if (nodeTargetName == this.name)
			{
				// do nothing
			}
			else 
			{
				var routeExisting = this.routes[nodeTargetName];
				if (routeExisting == null)
				{
					var routeNew = new Route
					(
						nodeTargetName, // target
						totalCostToTargetThroughNeighbor, 
						neighborName // nodeNextName
					);
	
					this.routes.push(routeNew);
					this.routes[nodeTargetName] = routeNew;
				}
				else if (routeExisting.totalCostToTarget > totalCostToTargetThroughNeighbor)
				{
					routeExisting.totalCostToTarget = totalCostToTargetThroughNeighbor;
					routeExisting.nodeNextName = neighborName;
				}
			}
		}
	}

	Node.prototype.size = function()
	{
		return Globals.Instance.network.nodeSizeInPixels;
	}

	// drawable

	Node.prototype.drawToDisplay = function(display)
	{
		var network = Globals.Instance.network;

		display.drawRectangle
		(
			this.pos, network.nodeSizeInPixels, "White", "Gray"
		);
		display.drawText(this.name, this.pos, "Gray"); 


		var textPos = this.center();
		textPos.y += display.fontHeightInPixels;

		for (var i = 0; i < this.routes.length; i++)
		{
			var route = this.routes[i];
			display.drawText(route.toString(), textPos, "Blue");
			textPos.y += display.fontHeightInPixels;
		}

		for (var i = 0; i < this.packetsDelivered.length; i++)
		{
			var packet = this.packetsDelivered[i];
			display.drawText(packet.toString(), textPos, "DarkGreen");
			textPos.y += display.fontHeightInPixels;
		}

	}
}

function Packet(nodeSourceName, nodeTargetName, payload)
{
	this.nodeSourceName = nodeSourceName;
	this.nodeCurrentName = this.nodeSourceName;
	this.nodeTargetName = nodeTargetName;
	this.payload = payload;

	this.nodeNextName = null;
	this.ticksTowardNodeNext = 0;
}
{
	Packet.prototype.fractionTowardNodeNext = function()
	{
		var returnValue = 
			this.ticksTowardNodeNext 
			/ this.linkCurrent().costToTraverse;

		return returnValue;		
	}

	Packet.prototype.isDelivered = function()
	{
		return (this.nodeCurrentName == this.nodeTargetName);
	}

	Packet.prototype.linkCurrent = function()
	{
		return this.nodeCurrent().linksToNeighbors[this.nodeNextName];
	}

	Packet.prototype.nodeCurrent = function()
	{
		return Globals.Instance.network.nodes[this.nodeCurrentName];
	}

	Packet.prototype.nodeNext = function()
	{
		return Globals.Instance.network.nodes[this.nodeNextName];
	}

	Packet.prototype.updateForTimerTick = function()
	{
		if (this.nodeNextName == null)
		{
			var nodeCurrent = this.nodeCurrent();
			var route = nodeCurrent.routes[this.nodeTargetName];
			if (route == null)
			{
				// Drop the packet?
			}
			else
			{
				this.nodeNextName = route.nodeNextName;
			}
		}
		else
		{
			var linkCurrent = this.linkCurrent();
			if (linkCurrent != null)
			{
				this.ticksTowardNodeNext++;

				if (this.ticksTowardNodeNext < linkCurrent.costToTraverse)
				{
					// todo
				}
				else
				{
					this.nodeCurrentName = this.nodeNextName;	
					this.nodeNextName = null;
					this.ticksTowardNodeNext = 0;
				}
			}

		}
	}

	// drawable

	Packet.prototype.drawToDisplay = function(display, network)
	{
		var pos = this.nodeCurrent().center().clone();

		if (this.nodeNextName != null)
		{
			var fractionTowardNodeNext = 
				this.fractionTowardNodeNext();

			pos.multiplyScalar
			(
				1 - fractionTowardNodeNext
			).add
			(
				this.nodeNext().center().clone().multiplyScalar
				(
					fractionTowardNodeNext
				)
			);
		}


		display.drawText(this.toString(), pos, "Red"); 
	}

	// string

	Packet.prototype.toString = function()
	{
		var returnValue = 
			"[packet"
			+ " from:" + this.nodeSourceName 
			+ " to:" + this.nodeTargetName
			+ " data:" + this.payload 
			+ "]";

		return returnValue;
	}

}

function Route(nodeTargetName, totalCostToTarget, nodeNextName)
{
	this.nodeTargetName = nodeTargetName;
	this.totalCostToTarget = totalCostToTarget;
	this.nodeNextName = nodeNextName;
}
{
	// string

	Route.prototype.toString = function()
	{
		var returnValue = 
			"[route"
			+ " to:" + this.nodeTargetName
			+ " cost:" + this.totalCostToTarget
			+ "]";

		return returnValue;
	}
}

// run 

main();

</script>

</body>
</html>

Posted in Uncategorized | Tagged , , , , , | 1 Comment

An Entropy Generator in JavaScript

The JavaScript code below, when run, attempts to gather “entropy” (randomness) from the time between the user’s keypresses, and displays the gathered entropy as bits, hexadecimal digits, and Base64 digits. To see it in action, copy it into an .html file and open that file in a web browser that runs JavaScript.

I’m not sure how well it works. With rapid typing, long runs of 1’s or 0’s tend to appear, which doesn’t seem very random. I’m not sure if the problem is just the nature of typing, the limitations of the way JavaScript runs, or just my own misunderstanding of how entropy works.

entropy


<html>
<body>

<div id="divMain"></div>

<script type="text/javascript">

// main
	
function main()
{
	var session = new Session();
	Globals.Instance.initialize(session);	
}

// classes

function Globals()
{
	// do nothing
}
{
	Globals.Instance = new Globals();

	Globals.prototype.initialize = function(session)
	{
		this.session = session;
		this.session.initialize();
	}
}

function Session()
{
	this.entropyBitsGatheredAsString = "";
	this.timeOfPreviousKeyEvent = new Date();
}
{
	// methods

	Session.prototype.entropyGatherFromEventKey = function(event)
	{
		var now = new Date();

		var millisecondsSincePreviousKeyEvent = 
			now.getTime() 
			- this.timeOfPreviousKeyEvent.getTime();

		var entropyBitGathered = 
			millisecondsSincePreviousKeyEvent % 2;

		this.entropyBitsGatheredAsString += "" + entropyBitGathered; 

		this.domElementUpdate();

		this.timeOfPreviousKeyEvent = now;
	}

	Session.prototype.entropyGatheringReset = function()
	{
		this.entropyBitsGatheredAsString = "";	

		this.domElementUpdate();
	}

	Session.prototype.initialize = function()
	{
		this.domElementUpdate();
		document.body.onkeyup = this.entropyGatherFromEventKey.bind(this);
	}

	// dom

	Session.prototype.domElementUpdate = function()
	{
		if (this.domElement == null)
		{
			var divSession = document.createElement("div");

			var divBits = document.createElement("div");

			var labelBitsGatheredSoFar = document.createElement("label");
			labelBitsGatheredSoFar.innerHTML = "Bits Gathered So Far:";
			divBits.appendChild(labelBitsGatheredSoFar);
			
			var inputBitsGatheredSoFar = document.createElement("textarea");
			inputBitsGatheredSoFar.cols = 40;
			inputBitsGatheredSoFar.rows = 10;
			inputBitsGatheredSoFar.disabled = true;
			this.inputBitsGatheredSoFar = inputBitsGatheredSoFar;
			divBits.appendChild(inputBitsGatheredSoFar);
		
			divSession.appendChild(divBits);

			var divHexadecimal = document.createElement("div");

			var labelAsHexadecimal = document.createElement("label");
			labelAsHexadecimal.innerHTML = "As Hexadecimal:";
			divHexadecimal.appendChild(labelAsHexadecimal);

			var inputAsHexadecimal = document.createElement("textarea");
			inputAsHexadecimal.cols = 40;
			inputAsHexadecimal.rows = 10;
			inputAsHexadecimal.readOnly = true;
			this.inputAsHexadecimal = inputAsHexadecimal;
			divHexadecimal.appendChild(inputAsHexadecimal);

			divSession.appendChild(divHexadecimal);

			var divBase64 = document.createElement("div");

			var labelAsBase64 = document.createElement("label");
			labelAsBase64.innerHTML = "As Base64:";
			divBase64.appendChild(labelAsBase64);

			var inputAsBase64 = document.createElement("textarea");
			inputAsBase64.cols = 40;
			inputAsBase64.rows = 10;
			inputAsBase64.readOnly = true;
			this.inputAsBase64 = inputAsBase64;
			divBase64.appendChild(inputAsBase64);

			divSession.appendChild(divBase64);

			var divControls = document.createElement("div");

			var buttonReset = document.createElement("button");
			buttonReset.innerHTML = "Reset";
			buttonReset.onclick = this.entropyGatheringReset.bind(this);
			divControls.appendChild(buttonReset);

			divSession.appendChild(divControls);

			var divMain = document.getElementById("divMain");
			divMain.appendChild(divSession);

			this.domElement = divSession;

		}

		this.inputBitsGatheredSoFar.value = this.entropyBitsGatheredAsString;

		var numberOfBits = this.entropyBitsGatheredAsString.length;

		var bitsPerNibble = 4;
		var numberOfNibbles = Math.floor(numberOfBits / bitsPerNibble);
		var entropyGatheredAsHexadecimal = "";

		for (var i = 0; i < numberOfNibbles; i++)
		{
			var nibbleAsBitString = this.entropyBitsGatheredAsString.substr
			(
				i * bitsPerNibble, bitsPerNibble
			);

			var nibbleValue = parseInt(nibbleAsBitString, 2);

			var nibbleValueAsHexadecimal = nibbleValue.toString(16);

			entropyGatheredAsHexadecimal += nibbleValueAsHexadecimal;
		}

		this.inputAsHexadecimal.value = entropyGatheredAsHexadecimal;

		var base64DigitsAll = 
			"0123456789"
			+ "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
			+ "abcdefghijklmnopqrstuvwxyz"
			+ "+/";

		var bitsPerSextet = 6;
		var numberOfSextets = Math.floor(numberOfBits / bitsPerSextet);
		var entropyGatheredAsBase64 = "";

		for (var i = 0; i < numberOfSextets; i++)
		{
			var sextetAsBitString = this.entropyBitsGatheredAsString.substr
			(
				i * bitsPerSextet, bitsPerSextet
			);

			var sextetValue = parseInt(sextetAsBitString, 2);

			var sextetValueAsBase64 = base64DigitsAll[sextetValue];

			entropyGatheredAsBase64 += sextetValueAsBase64;
		}

		this.inputAsBase64.value = entropyGatheredAsBase64;

		return this.domElement;
	}

}

// run

main();

</script>
</body>
</html>

Posted in Uncategorized | Tagged , , , | Leave a comment