<commit_before>/*
	Coding Soccer

	Copyright (c) 2017 Cédric Ronvel

	The MIT License (MIT)

	Permission is hereby granted, free of charge, to any person obtaining a copy
	of this software and associated documentation files (the "Software"), to deal
	in the Software without restriction, including without limitation the rights
	to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
	copies of the Software, and to permit persons to whom the Software is
	furnished to do so, subject to the following conditions:

	The above copyright notice and this permission notice shall be included in all
	copies or substantial portions of the Software.

	THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
	IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
	FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
	AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
	LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
	OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
	SOFTWARE.
*/

/* jshint -W072 */

"use strict" ;

var Logfella = require( 'logfella' ) ;
var log = Logfella.global.use( 'physic' ) ;

/*
	Shapes are like “Brushes”: they are convex CSG union of basic primitives:
	* plane: separate the space in two side: the positive side (outside) and the negative side (inside)
	* cylinder: infinite tube, separate into the inner (inside) and the outer (outside)
	* sphere: separate into the inner (inside) and the outer (outside) of the sphere
*/

function Shape( primitives , boundingBox , omni ) { return Shape.create( primitives , boundingBox , omni ) ; }
module.exports = Shape ;

var physic = require( './physic.js' ) ;

/*
	* primitives: array of object, where:
		* type: the type of the primitive
		* ... parameters for this type
	* boundingBox: object having properties: xMin, xMax, yMin, yMax, zMin, zMax
	* omni: omni-present, non-solid and static

	For intance only 'sphere' and 'plane' primitives exists.
*/
Shape.create = function create( primitives , boundingBox , omni )
{
	var self = Object.create( Shape.prototype , {
		omni: { value: !! omni , writable: true , enumerable: true } ,
		primitives: { value: primitives , writable: true , enumerable: true } ,
		boundingBox: { value: boundingBox , writable: true , enumerable: true } ,
	} ) ;

	return self ;
} ;

Shape.prototype.isIntersectingWith = function isIntersectingWith(
	boundVector , oldBoundVector ,
	withShape , withBoundVector , withOldBoundVector ,
	solid )
{
	var i , iMax , j , jMax , p1 , p2 , result ;

	// Omni shapes always intersect, but are forced non-solid
	if ( this.omni || withShape.omni ) { return true ; }

	// Filtering out by bounding boxes
	if ( ! this.boundingBox.translatedBoundingBoxesIntersection( boundVector.position , withShape.boundingBox , withBoundVector.position ) )
	{
		//log.info( 'filtered out by bbox' ) ;
		return false ;
	}

	for ( i = 0 , iMax = this.primitives.length ; i < iMax ; i ++ )
	{
		p1 = this.primitives[ i ] ;

		for ( j = 0 , jMax = withShape.primitives.length ; j < jMax ; j ++ )
		{
			p2 = withShape.primitives[ j ] ;
			result = lqCollision[ p1.type ][ p2.type ](
				p1 , boundVector ,
				p2 , withBoundVector ,
				solid ) ;

			if ( result ) { return result ; }
		}
	}

	return false ;
} ;

Shape.prototype.isHqIntersectingWith = function isHqIntersectingWith(
	boundVector , oldBoundVector ,
	withShape , withBoundVector , withOldBoundVector ,
	solid )
{
	var i , iMax , j , jMax , p1 , p2 , result ;

	// Omni shapes always intersect, but are forced non-solid
	if ( this.omni || withShape.omni ) { return true ; }

	// Filtering out by moving bounding boxes
	if (
		// create bbox from both position and merges it with its bbox
		! physic.BoundingBox3D.fromTo( boundVector.position , oldBoundVector.position ).add( this.boundingBox )
			.boundingBoxIntersection(
				// create bbox from both position and merges it with its bbox
				physic.BoundingBox3D.fromTo( withBoundVector.position , withOldBoundVector.position ).add( withShape.boundingBox )
			)
	)
	{
		//log.info( 'filtered out by HQ bbox' ) ;
		return false ;
	}

	for ( i = 0 , iMax = this.primitives.length ; i < iMax ; i ++ )
	{
		p1 = this.primitives[ i ] ;

		for ( j = 0 , jMax = withShape.primitives.length ; j < jMax ; j ++ )
		{
			p2 = withShape.primitives[ j ] ;
			result = hqCollision[ p1.type ][ p2.type ](
				p1 , boundVector , oldBoundVector ,
				p2 , withBoundVector , withOldBoundVector ,
				solid ) ;

			if ( result ) { return result ; }
		}
	}

	return false ;
} ;

var lqCollision = {} ;
lqCollision.sphere = {} ;
lqCollision.plane = {} ;

var hqCollision = {} ;
hqCollision.sphere = {} ;
hqCollision.plane = {} ;

/*
	Return values:
	* false: no collision and no contact between the entities
	* true:
		* if solid: this is a tangential contact, i.e. entity are touching each others but not colliding,
		  friction and other environmental effect applies but not bounces
		* if non-solid: entity are touching or inside each others
	* array: the two entities collide each others.
	  The array contains 2 objects, one for each entities, where:
		* at: it contains the position where the collision would had happend if against a static entity.
		  If two dynamic entities collide, the result should mix its current position and its 'at' value.
		* normal: the normal for that collision
		* decomposed: the speed vector of the entity decomposed along the normal and the tangential plane
*/

lqCollision.sphere.plane = function(
	spherePrimitive , sphereBoundVector ,
	planePrimitive , planeBoundVector ,
	solid )
{
	// Create the plane geometry
	var plane = physic.Plane3D.fromNormalVectors( planeBoundVector.position , planePrimitive.normal ) ;

	// Create the point where the collision may happen
	var sphereContact = sphereBoundVector.position.dup().moveAlong( planePrimitive.normal , - spherePrimitive.radius ) ;

	// If the test is positive, the sphere is on the outside of the plane
	var test = physic.epsilonZero( plane.testVector( sphereContact ) ) ;

	// Exit now if there is no collision or if one entity is non-solid
	if ( test >= 0 || ! solid ) { return test <= 0 ; }

	// Get the plane contact point
	var planeContact ;
	if ( test === 0 ) { planeContact = sphereContact ; }
	else { planeContact = plane.pointProjection( sphereContact ) ; }
	//else { planeContact = plane.intersection( physic.BoundVector3D.fromVectors( sphereContact , planePrimitive.normal ) ) ; }

	return [
		// For the plane
		{
			at: planeContact.moveAlong( planePrimitive.normal , spherePrimitive.radius ) ,
			normal: planePrimitive.normal ,
			decomposed: sphereBoundVector.vector.decompose( planePrimitive.normal )
		} ,
		// For the sphere
		{
			at: sphereContact ,
			normal: planePrimitive.normal.dup().inv() ,
			decomposed: planeBoundVector.vector.decompose( planePrimitive.normal )
		}
	] ;
} ;

lqCollision.plane.sphere = function( e1p , e1b , e2p , e2b , s )
{
	var array = lqCollision.sphere.plane( e2p , e2b , e1p , e1b , s ) ;
	var tmp = array[ 1 ] ;
	array[ 1 ] = array[ 0 ] ;
	array[ 0 ] = tmp ;
	return array ;
} ;

hqCollision.sphere.plane = function(
	spherePrimitive , sphereBoundVector , sphereOldBoundVector ,
	planePrimitive , planeBoundVector , planeOldBoundVector ,
	solid )
{
	// Create the plane geometry
	var plane = physic.Plane3D.fromNormalVectors( planeBoundVector.position , planePrimitive.normal ) ;

	// Create the point where the collision may happen
	var sphereContact = sphereBoundVector.position.dup().moveAlong( planePrimitive.normal , - spherePrimitive.radius ) ;

	// It's a plane, so only the final point should be tested
	// If the test is positive, the sphere is on the outside of the plane
	var test = physic.epsilonZero( plane.testVector( sphereContact ) ) ;

	// Exit now if there is no collision or if one entity is non-solid
	if ( test >= 0 || ! solid ) { return test <= 0 ; }

	var planeContact ;

	// Compute it like if the plane was static: mix its movement with the sphere one
	var vector = physic.Vector3D.fromTo( sphereBoundVector.position , sphereOldBoundVector.position )
		.add( planeBoundVector.position ).sub( planeOldBoundVector.position ) ;

	// Fallback to the plane normal if the vector is null or perpendicular
	vector = vector.isNull() || vector.dot( planePrimitive.normal ) === 0 ? planePrimitive.normal : vector ;

	// Get the contact point
	planeContact = plane.intersection( physic.BoundVector3D.fromVectors( sphereContact , vector ) ) ;

	return [
		// For the plane
		{
			at: planeContact.moveAlong( planePrimitive.normal , spherePrimitive.radius ) ,
			normal: planePrimitive.normal ,
			decomposed: sphereBoundVector.vector.decompose( planePrimitive.normal )
		} ,
		// For the sphere
		{
			at: sphereContact ,
			normal: planePrimitive.normal.dup().inv() ,
			decomposed: planeBoundVector.vector.decompose( planePrimitive.normal )
		}
	] ;
} ;

hqCollision.plane.sphere = function( e1p , e1b , e1o , e2p , e2b , e2o , s )
{
	var array = hqCollision.sphere.plane( e2p , e2b , e2o , e1p , e1b , e1o , s ) ;
	var tmp = array[ 1 ] ;
	array[ 1 ] = array[ 0 ] ;
	array[ 0 ] = tmp ;
	return array ;
} ;

lqCollision.sphere.sphere = function(
	s1Primitive , s1BoundVector ,
	s2Primitive , s2BoundVector ,
	solid )
{
	var radius = s1Primitive.radius + s2Primitive.radius ;

	// Squared distance
	var squaredDistance = s1BoundVector.position.pointSquaredDistance( s2BoundVector.position ) ;

	var diff = physic.Vector3D.fromTo( s1BoundVector.position , s2BoundVector.position ) ;

	var test = physic.epsilonZero( squaredDistance - radius * radius ) ;

	// Exit now if there is no collision or if one entity is non-solid
	if ( test >= 0 || ! solid ) { return test <= 0 ; }

	// Displacement value
	var displacement = radius - Math.sqrt( squaredDistance ) ;

	return [
		// For S1
		{
			at: s1BoundVector.position.dup().moveAlong( diff , - displacement ) ,
			normal: diff.dup().inv() ,
			decomposed: s1BoundVector.vector.decompose( diff )
		} ,
		// For S2
		{
			at: s2BoundVector.position.dup().moveAlong( diff , displacement ) ,
			normal: diff ,
			decomposed: s2BoundVector.vector.decompose( diff )
		}
	] ;
} ;

hqCollision.sphere.sphere = function(
	s1Primitive , s1BoundVector , s1OldBoundVector ,
	s2Primitive , s2BoundVector , s2OldBoundVector ,
	solid )
{
	var wasInside = false ;

	// We will make it as if it was a big static sphere and a moving point, i.e. a line
	var radius = s1Primitive.radius + s2Primitive.radius ;
	var sphere = physic.Sphere3D.fromVectorRadius( s1BoundVector.position , radius ) ;

	// Fake old position
	var segmentOldPos = s2OldBoundVector.position.dup().add( s1BoundVector.position ).sub( s1OldBoundVector.position ) ;
	//log.error( 'Fake old pos: %Y %Y %Y %Y' , segmentOldPos , s2OldBoundVector.position , s1BoundVector.position , s1OldBoundVector.position ) ;

	if ( physic.epsilonLt( sphere.testVector( segmentOldPos ) , 0 ) )
	{
		// The old position was already inside, move it on the sphere's surface
		segmentOldPos = sphere.pointProjection( segmentOldPos ) ;
		wasInside = true ;
	}

	// We use the fake old position as the target point
	var segment = physic.BoundVector3D.fromTo( s2BoundVector.position , segmentOldPos ) ;
	//log.error( 'Fake segment: %Y' , segment ) ;

	var intersectionArray = sphere.intersection( segment ) ;
	//log.error( 'Intersection array: %Y' , intersectionArray ) ;

	// No intersection
	if ( ! intersectionArray ) { return false ; }

	// Check if the intersection is on the segment (faster than performing .isOnLineSegment(),
	// since it would check for the line too: a fact already known).
	// Also, if the entity is moving outward, we check for exclusive bounds.
	if ( wasInside || physic.epsilonGt( segment.vector.dot( physic.Vector3D.fromTo( sphere , segmentOldPos ) ) , 0 ) )
	{
		intersectionArray = intersectionArray.filter( e => segment.isInBounds( e ) ) ;
	}
	else
	{
		//log.fatal( "Exclusive bounds filtering, segment: %Y %Y %Y" , segment.position , segment.getEndPoint() , segmentOldPos ) ;
		intersectionArray = intersectionArray.filter( e => segment.isInExclusiveBounds( e ) ) ;
	}

	//log.error( 'Filtered intersection array: %Y' , intersectionArray ) ;

	var intersection ;

	if ( ! intersectionArray.length )
	{
		return false ;
	}
	else if ( intersectionArray.length === 1 ||
		segmentOldPos.pointSquaredDistance( intersectionArray[ 0 ] ) <=
		segmentOldPos.pointSquaredDistance( intersectionArray[ 1 ] ) )
	{
		intersection = intersectionArray[ 0 ] ;
	}
	else
	{
		intersection = intersectionArray[ 1 ] ;
	}

	// Exit now if non-solid
	if ( ! solid ) { return true ; }

	var normal = physic.Vector3D.fromTo( s1BoundVector.position , intersection ) ;

	return [
		// For S1
		{
			at: s1BoundVector.position.dup().add( s1BoundVector.position ).sub( intersection ) ,
			normal: normal.dup().inv() ,
			decomposed: s1BoundVector.vector.decompose( normal )
		} ,
		// For S2
		{
			at: intersection ,
			normal: normal ,
			decomposed: s2BoundVector.vector.decompose( normal )
		}
	] ;
} ;

Shape.Omni = require( './shapes/Omni.js' ) ;
Shape.Sphere = require( './shapes/Sphere.js' ) ;
Shape.Plane = require( './shapes/Plane.js' ) ;

<commit_msg>before branching<commit_after>/*
	Coding Soccer

	Copyright (c) 2017 Cédric Ronvel

	The MIT License (MIT)

	Permission is hereby granted, free of charge, to any person obtaining a copy
	of this software and associated documentation files (the "Software"), to deal
	in the Software without restriction, including without limitation the rights
	to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
	copies of the Software, and to permit persons to whom the Software is
	furnished to do so, subject to the following conditions:

	The above copyright notice and this permission notice shall be included in all
	copies or substantial portions of the Software.

	THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
	IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
	FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
	AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
	LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
	OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
	SOFTWARE.
*/

/* jshint -W072 */

"use strict" ;

var Logfella = require( 'logfella' ) ;
var log = Logfella.global.use( 'physic' ) ;

/*
	Shapes are like “Brushes”: they are convex CSG intersection of basic primitives:
	* plane: separate the space in two side: the positive side (outside) and the negative side (inside)
	* cylinder: infinite tube, separate into the inner (inside) and the outer (outside)
	* sphere: separate into the inner (inside) and the outer (outside) of the sphere
*/

function Shape( primitives , boundingBox , omni ) { return Shape.create( primitives , boundingBox , omni ) ; }
module.exports = Shape ;

var physic = require( './physic.js' ) ;

/*
	* primitives: array of object, where:
		* type: the type of the primitive
		* ... parameters for this type
	* boundingBox: object having properties: xMin, xMax, yMin, yMax, zMin, zMax
	* omni: omni-present, non-solid and static

	For intance only 'sphere' and 'plane' primitives exists.
*/
Shape.create = function create( primitives , boundingBox , omni )
{
	var self = Object.create( Shape.prototype , {
		omni: { value: !! omni , writable: true , enumerable: true } ,
		primitives: { value: primitives , writable: true , enumerable: true } ,
		boundingBox: { value: boundingBox , writable: true , enumerable: true } ,
	} ) ;

	return self ;
} ;

Shape.prototype.isIntersectingWith = function isIntersectingWith(
	boundVector , oldBoundVector ,
	withShape , withBoundVector , withOldBoundVector ,
	solid )
{
	var i , iMax , j , jMax , p1 , p2 , result ;

	// Omni shapes always intersect, but are forced non-solid
	if ( this.omni || withShape.omni ) { return true ; }

	// Filtering out by bounding boxes
	if ( ! this.boundingBox.translatedBoundingBoxesIntersection( boundVector.position , withShape.boundingBox , withBoundVector.position ) )
	{
		//log.info( 'filtered out by bbox' ) ;
		return false ;
	}

	for ( i = 0 , iMax = this.primitives.length ; i < iMax ; i ++ )
	{
		p1 = this.primitives[ i ] ;

		for ( j = 0 , jMax = withShape.primitives.length ; j < jMax ; j ++ )
		{
			p2 = withShape.primitives[ j ] ;
			result = lqCollision[ p1.type ][ p2.type ](
				p1 , boundVector ,
				p2 , withBoundVector ,
				solid ) ;

			if ( result ) { return result ; }
		}
	}

	return false ;
} ;

Shape.prototype.isHqIntersectingWith = function isHqIntersectingWith(
	boundVector , oldBoundVector ,
	withShape , withBoundVector , withOldBoundVector ,
	solid )
{
	var i , iMax , j , jMax , p1 , p2 , result ;

	// Omni shapes always intersect, but are forced non-solid
	if ( this.omni || withShape.omni ) { return true ; }

	// Filtering out by moving bounding boxes
	if (
		// create bbox from both position and merges it with its bbox
		! physic.BoundingBox3D.fromTo( boundVector.position , oldBoundVector.position ).add( this.boundingBox )
			.boundingBoxIntersection(
				// create bbox from both position and merges it with its bbox
				physic.BoundingBox3D.fromTo( withBoundVector.position , withOldBoundVector.position ).add( withShape.boundingBox )
			)
	)
	{
		//log.info( 'filtered out by HQ bbox' ) ;
		return false ;
	}

	for ( i = 0 , iMax = this.primitives.length ; i < iMax ; i ++ )
	{
		p1 = this.primitives[ i ] ;

		for ( j = 0 , jMax = withShape.primitives.length ; j < jMax ; j ++ )
		{
			p2 = withShape.primitives[ j ] ;
			result = hqCollision[ p1.type ][ p2.type ](
				p1 , boundVector , oldBoundVector ,
				p2 , withBoundVector , withOldBoundVector ,
				solid ) ;

			if ( result ) { return result ; }
		}
	}

	return false ;
} ;

var lqCollision = {} ;
lqCollision.sphere = {} ;
lqCollision.plane = {} ;

var hqCollision = {} ;
hqCollision.sphere = {} ;
hqCollision.plane = {} ;

/*
	Return values:
	* false: no collision and no contact between the entities
	* true:
		* if solid: this is a tangential contact, i.e. entity are touching each others but not colliding,
		  friction and other environmental effect applies but not bounces
		* if non-solid: entity are touching or inside each others
	* array: the two entities collide each others.
	  The array contains 2 objects, one for each entities, where:
		* at: it contains the position where the collision would had happend if against a static entity.
		  If two dynamic entities collide, the result should mix its current position and its 'at' value.
		* normal: the normal for that collision
		* decomposed: the speed vector of the entity decomposed along the normal and the tangential plane
*/

lqCollision.sphere.plane = function(
	spherePrimitive , sphereBoundVector ,
	planePrimitive , planeBoundVector ,
	solid )
{
	// Create the plane geometry
	var plane = physic.Plane3D.fromNormalVectors( planeBoundVector.position , planePrimitive.normal ) ;

	// Create the point where the collision may happen
	var sphereContact = sphereBoundVector.position.dup().moveAlong( planePrimitive.normal , - spherePrimitive.radius ) ;

	// If the test is positive, the sphere is on the outside of the plane
	var test = physic.epsilonZero( plane.testVector( sphereContact ) ) ;

	// Exit now if there is no collision or if one entity is non-solid
	if ( test >= 0 || ! solid ) { return test <= 0 ; }

	// Get the plane contact point
	var planeContact ;
	if ( test === 0 ) { planeContact = sphereContact ; }
	else { planeContact = plane.pointProjection( sphereContact ) ; }
	//else { planeContact = plane.intersection( physic.BoundVector3D.fromVectors( sphereContact , planePrimitive.normal ) ) ; }

	return [
		// For the plane
		{
			at: planeContact.moveAlong( planePrimitive.normal , spherePrimitive.radius ) ,
			normal: planePrimitive.normal ,
			decomposed: sphereBoundVector.vector.decompose( planePrimitive.normal )
		} ,
		// For the sphere
		{
			at: sphereContact ,
			normal: planePrimitive.normal.dup().inv() ,
			decomposed: planeBoundVector.vector.decompose( planePrimitive.normal )
		}
	] ;
} ;

lqCollision.plane.sphere = function( e1p , e1b , e2p , e2b , s )
{
	var array = lqCollision.sphere.plane( e2p , e2b , e1p , e1b , s ) ;
	var tmp = array[ 1 ] ;
	array[ 1 ] = array[ 0 ] ;
	array[ 0 ] = tmp ;
	return array ;
} ;

hqCollision.sphere.plane = function(
	spherePrimitive , sphereBoundVector , sphereOldBoundVector ,
	planePrimitive , planeBoundVector , planeOldBoundVector ,
	solid )
{
	// Create the plane geometry
	var plane = physic.Plane3D.fromNormalVectors( planeBoundVector.position , planePrimitive.normal ) ;

	// Create the point where the collision may happen
	var sphereContact = sphereBoundVector.position.dup().moveAlong( planePrimitive.normal , - spherePrimitive.radius ) ;

	// It's a plane, so only the final point should be tested
	// If the test is positive, the sphere is on the outside of the plane
	var test = physic.epsilonZero( plane.testVector( sphereContact ) ) ;

	// Exit now if there is no collision or if one entity is non-solid
	if ( test >= 0 || ! solid ) { return test <= 0 ; }

	var planeContact ;

	// Compute it like if the plane was static: mix its movement with the sphere one
	var vector = physic.Vector3D.fromTo( sphereBoundVector.position , sphereOldBoundVector.position )
		.add( planeBoundVector.position ).sub( planeOldBoundVector.position ) ;

	// Fallback to the plane normal if the vector is null or perpendicular
	vector = vector.isNull() || vector.dot( planePrimitive.normal ) === 0 ? planePrimitive.normal : vector ;

	// Get the contact point
	planeContact = plane.intersection( physic.BoundVector3D.fromVectors( sphereContact , vector ) ) ;

	return [
		// For the plane
		{
			at: planeContact.moveAlong( planePrimitive.normal , spherePrimitive.radius ) ,
			normal: planePrimitive.normal ,
			decomposed: sphereBoundVector.vector.decompose( planePrimitive.normal )
		} ,
		// For the sphere
		{
			at: sphereContact ,
			normal: planePrimitive.normal.dup().inv() ,
			decomposed: planeBoundVector.vector.decompose( planePrimitive.normal )
		}
	] ;
} ;

hqCollision.plane.sphere = function( e1p , e1b , e1o , e2p , e2b , e2o , s )
{
	var array = hqCollision.sphere.plane( e2p , e2b , e2o , e1p , e1b , e1o , s ) ;
	var tmp = array[ 1 ] ;
	array[ 1 ] = array[ 0 ] ;
	array[ 0 ] = tmp ;
	return array ;
} ;

lqCollision.sphere.sphere = function(
	s1Primitive , s1BoundVector ,
	s2Primitive , s2BoundVector ,
	solid )
{
	var radius = s1Primitive.radius + s2Primitive.radius ;

	// Squared distance
	var squaredDistance = s1BoundVector.position.pointSquaredDistance( s2BoundVector.position ) ;

	var diff = physic.Vector3D.fromTo( s1BoundVector.position , s2BoundVector.position ) ;

	var test = physic.epsilonZero( squaredDistance - radius * radius ) ;

	// Exit now if there is no collision or if one entity is non-solid
	if ( test >= 0 || ! solid ) { return test <= 0 ; }

	// Displacement value
	var displacement = radius - Math.sqrt( squaredDistance ) ;

	return [
		// For S1
		{
			at: s1BoundVector.position.dup().moveAlong( diff , - displacement ) ,
			normal: diff.dup().inv() ,
			decomposed: s1BoundVector.vector.decompose( diff )
		} ,
		// For S2
		{
			at: s2BoundVector.position.dup().moveAlong( diff , displacement ) ,
			normal: diff ,
			decomposed: s2BoundVector.vector.decompose( diff )
		}
	] ;
} ;

hqCollision.sphere.sphere = function(
	s1Primitive , s1BoundVector , s1OldBoundVector ,
	s2Primitive , s2BoundVector , s2OldBoundVector ,
	solid )
{
	var wasInside = false ;

	// We will make it as if it was a big static sphere and a moving point, i.e. a line
	var radius = s1Primitive.radius + s2Primitive.radius ;
	var sphere = physic.Sphere3D.fromVectorRadius( s1BoundVector.position , radius ) ;

	// Fake old position
	var segmentOldPos = s2OldBoundVector.position.dup().add( s1BoundVector.position ).sub( s1OldBoundVector.position ) ;
	//log.error( 'Fake old pos: %Y %Y %Y %Y' , segmentOldPos , s2OldBoundVector.position , s1BoundVector.position , s1OldBoundVector.position ) ;

	if ( physic.epsilonLt( sphere.testVector( segmentOldPos ) , 0 ) )
	{
		// The old position was already inside, move it on the sphere's surface
		segmentOldPos = sphere.pointProjection( segmentOldPos ) ;
		wasInside = true ;
	}

	// We use the fake old position as the target point
	var segment = physic.BoundVector3D.fromTo( s2BoundVector.position , segmentOldPos ) ;
	//log.error( 'Fake segment: %Y' , segment ) ;

	var intersectionArray = sphere.intersection( segment ) ;
	//log.error( 'Intersection array: %Y' , intersectionArray ) ;

	// No intersection
	if ( ! intersectionArray ) { return false ; }

	// Check if the intersection is on the segment (faster than performing .isOnLineSegment(),
	// since it would check for the line too: a fact already known).
	// Also, if the entity is moving outward, we check for exclusive bounds.
	if ( wasInside || physic.epsilonGt( segment.vector.dot( physic.Vector3D.fromTo( sphere , segmentOldPos ) ) , 0 ) )
	{
		intersectionArray = intersectionArray.filter( e => segment.isInBounds( e ) ) ;
	}
	else
	{
		//log.fatal( "Exclusive bounds filtering, segment: %Y %Y %Y" , segment.position , segment.getEndPoint() , segmentOldPos ) ;
		intersectionArray = intersectionArray.filter( e => segment.isInExclusiveBounds( e ) ) ;
	}

	//log.error( 'Filtered intersection array: %Y' , intersectionArray ) ;

	var intersection ;

	if ( ! intersectionArray.length )
	{
		return false ;
	}
	else if ( intersectionArray.length === 1 ||
		segmentOldPos.pointSquaredDistance( intersectionArray[ 0 ] ) <=
		segmentOldPos.pointSquaredDistance( intersectionArray[ 1 ] ) )
	{
		intersection = intersectionArray[ 0 ] ;
	}
	else
	{
		intersection = intersectionArray[ 1 ] ;
	}

	// Exit now if non-solid
	if ( ! solid ) { return true ; }

	var normal = physic.Vector3D.fromTo( s1BoundVector.position , intersection ) ;

	return [
		// For S1
		{
			at: s1BoundVector.position.dup().add( s1BoundVector.position ).sub( intersection ) ,
			normal: normal.dup().inv() ,
			decomposed: s1BoundVector.vector.decompose( normal )
		} ,
		// For S2
		{
			at: intersection ,
			normal: normal ,
			decomposed: s2BoundVector.vector.decompose( normal )
		}
	] ;
} ;

Shape.Omni = require( './shapes/Omni.js' ) ;
Shape.Sphere = require( './shapes/Sphere.js' ) ;
Shape.Plane = require( './shapes/Plane.js' ) ;

<|endoftext|>


<commit_before>/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this file,
 * You can obtain one at http://mozilla.org/MPL/2.0/. */

const electron = global.require('electron')
const remote = electron.remote
const Menu = remote.require('menu')
const Immutable = require('immutable')
const clipboard = electron.clipboard
const messages = require('./constants/messages')
const WindowStore = require('./stores/windowStore')
const WindowActions = require('./actions/windowActions')
const AppActions = require('./actions/appActions')
const siteTags = require('./constants/siteTags')
const siteUtil = require('./state/siteUtil')
const CommonMenu = require('./commonMenu')
const ipc = global.require('electron').ipcRenderer

/**
 * Obtains an add bookmark menu item
 * @param {object} Detail of the bookmark to initialize with
 */
const addBookmarkMenuItem = (siteDetail) => {
  return {
    label: 'Add Bookmark...',
    click: () => {
      WindowActions.setBookmarkDetail(siteDetail)
    }
  }
}

const addFolderMenuItem = {
  label: 'Add Folder...',
  click: () => {
    const emptyFolder = Immutable.fromJS({tags: [siteTags.BOOKMARK_FOLDER]})
    WindowActions.setBookmarkDetail(emptyFolder)
  }
}

function tabPageTemplateInit (framePropsList) {
  const muteAll = (framePropsList, mute) => {
    framePropsList.forEach(frameProps => {
      if (mute && frameProps.get('audioPlaybackActive') && !frameProps.get('audioMuted')) {
        WindowActions.setAudioMuted(frameProps, true)
      } else if (!mute && frameProps.get('audioMuted')) {
        WindowActions.setAudioMuted(frameProps, false)
      }
    })
  }
  return [{
    label: 'Unmute tabs',
    click: (item, focusedWindow) => {
      muteAll(framePropsList, false)
    }
  }, {
    label: 'Mute tabs',
    click: (item, focusedWindow) => {
      muteAll(framePropsList, true)
    }
  }]
}

function inputTemplateInit (e) {
  const hasSelection = e.target.selectionStart !== undefined &&
      e.target.selectionEnd !== undefined &&
      e.target.selectionStart !== e.target.selectionEnd
  return getEditableItems(hasSelection)
}

function tabsToolbarTemplateInit (settingsState, activeFrame) {
  return [
    CommonMenu.bookmarksMenuItem,
    CommonMenu.bookmarksToolbarMenuItem(settingsState),
    CommonMenu.separatorMenuItem,
    addBookmarkMenuItem(siteUtil.getDetailFromFrame(activeFrame, siteTags.BOOKMARK)),
    addFolderMenuItem
  ]
}

function moreBookmarksTemplateInit (activeFrame, bookmarks) {
  return bookmarks.map(bookmark => {
    return {
      label: bookmark.get('title'),
      click: () => {
        WindowActions.loadUrl(activeFrame, bookmark.get('location'))
      }
    }
  }).toJS()
}

function bookmarkTemplateInit (siteDetail, activeFrame) {
  const location = siteDetail.get('location')
  const isFolder = siteDetail.get('tags').includes(siteTags.BOOKMARK_FOLDER)
  const template = []
  if (!isFolder) {
    template.push(openInNewTabMenuItem(location),
      openInNewPrivateTabMenuItem(location),
      openInNewSessionTabMenuItem(location),
      copyLinkLocationMenuItem(location),
      CommonMenu.separatorMenuItem)
  }

  template.push(
    {
      label: isFolder ? 'Edit Folder...' : 'Edit Bookmark...',
      click: () => {
        // originalLocation is undefined signifies add mode
        WindowActions.setBookmarkDetail(siteDetail, siteDetail)
      }
    },
    CommonMenu.separatorMenuItem, {
      label: isFolder ? 'Delete Folder' : 'Delete Bookmark',
      click: () => {
        AppActions.removeSite(siteDetail, siteDetail.get('tags').includes(siteTags.BOOKMARK_FOLDER) ? siteTags.BOOKMARK_FOLDER : siteTags.BOOKMARK)
      }
    },
    CommonMenu.separatorMenuItem,
    addBookmarkMenuItem(siteUtil.getDetailFromFrame(activeFrame, siteTags.BOOKMARK)),
    addFolderMenuItem)
  return template
}

function showBookmarkFolderInit (bookmarks, bookmark, activeFrame) {
  const items = siteUtil.filterSitesRelativeTo(bookmarks, bookmark)
  if (items.size === 0) {
    return [{
      label: '(empty)',
      enabled: false
    }]
  }

  return items.map(site => {
    const templateItem = {
      label: site.get('title'),
      click: function () {
        WindowActions.loadUrl(activeFrame, site.get('location'))
      }
    }
    const isFolder = siteUtil.isFolder(site)
    if (isFolder) {
      templateItem.submenu = showBookmarkFolderInit(bookmarks, site, activeFrame)
    }
    return templateItem
  }).toJS()
}

function tabTemplateInit (frameProps) {
  const tabKey = frameProps.get('key')
  const items = []
  items.push({
    label: 'Reload tab',
    click: (item, focusedWindow) => {
      if (focusedWindow) {
        focusedWindow.webContents.send(messages.SHORTCUT_FRAME_RELOAD, tabKey)
      }
    }
  })

  if (!frameProps.get('isPrivate')) {
    if (frameProps.get('pinnedLocation')) {
      items.push({
        label: 'Unpin tab',
        click: (item) => {
          // Handle converting the current tab window into a pinned site
          WindowActions.setPinned(frameProps, false)
          // Handle setting it in app storage for the other windows
          AppActions.removeSite(siteUtil.getDetailFromFrame(frameProps), siteTags.PINNED)
        }
      })
    } else {
      items.push({
        label: 'Pin tab',
        click: (item) => {
          // Handle converting the current tab window into a pinned site
          WindowActions.setPinned(frameProps, true)
          // Handle setting it in app storage for the other windows
          AppActions.addSite(siteUtil.getDetailFromFrame(frameProps), siteTags.PINNED)
        }
      })
    }
  }

  if (frameProps.get('audioPlaybackActive')) {
    if (frameProps.get('audioMuted')) {
      items.push({
        label: 'Unmute tab',
        click: item => {
          WindowActions.setAudioMuted(frameProps, false)
        }
      })
    } else {
      items.push({
        label: 'Mute tab',
        click: item => {
          WindowActions.setAudioMuted(frameProps, true)
        }
      })
    }
  }

  Array.prototype.push.apply(items, [{
    label: 'Disable tracking protection',
    enabled: false
  }, {
    label: 'Disable ad block',
    enabled: false
  }])

  if (!frameProps.get('pinnedLocation')) {
    items.push({
      label: 'Close tab',
      click: (item, focusedWindow) => {
        if (focusedWindow) {
          // TODO: Don't switch active tabs when this is called
          focusedWindow.webContents.send(messages.SHORTCUT_CLOSE_FRAME, tabKey)
        }
      }
    })
  }

  items.push(Object.assign({},
    CommonMenu.reopenLastClosedTabItem,
    { enabled: WindowStore.getState().get('closedFrames').size > 0 }
  ))

  return items
}

function getEditableItems (hasSelection) {
  return [{
    label: 'Cut',
    enabled: hasSelection,
    accelerator: 'CmdOrCtrl+X',
    // Enabled doesn't work when a role is used
    role: hasSelection && 'cut' || undefined
  }, {
    label: 'Copy',
    enabled: hasSelection,
    accelerator: 'CmdOrCtrl+C',
    // Enabled doesn't work when a role is used
    role: hasSelection && 'copy' || undefined
  }, {
    label: 'Paste',
    accelerator: 'CmdOrCtrl+V',
    role: 'paste'
  }]
}

function hamburgerTemplateInit (braverySettings, settingsState) {
  const template = [
    CommonMenu.newTabMenuItem,
    CommonMenu.newPrivateTabMenuItem,
    CommonMenu.newPartitionedTabMenuItem,
    CommonMenu.newWindowMenuItem,
    CommonMenu.separatorMenuItem,
    CommonMenu.findOnPageMenuItem,
    CommonMenu.printMenuItem,
    CommonMenu.separatorMenuItem,
    CommonMenu.buildBraveryMenu(braverySettings, function () {
      ipc.send(messages.UPDATE_APP_MENU, {bookmarked: braverySettings.bookmarked})
    }),
    CommonMenu.separatorMenuItem,
    CommonMenu.preferencesMenuItem,
    {
      label: 'Bookmarks',
      submenu: [
        CommonMenu.bookmarksMenuItem,
        CommonMenu.bookmarksToolbarMenuItem(settingsState)
      ]
    },
    CommonMenu.separatorMenuItem,
    CommonMenu.quitMenuItem
  ]
  return template
}

const openInNewTabMenuItem = location => {
  return {
    label: 'Open in new tab',
    click: () => {
      WindowActions.newFrame({ location }, false)
    }
  }
}

const openInNewPrivateTabMenuItem = location => {
  return {
    label: 'Open in new private tab',
    click: () => {
      WindowActions.newFrame({
        location,
        isPrivate: true
      }, false)
    }
  }
}

const openInNewSessionTabMenuItem = location => {
  return {
    label: 'Open in new session tab',
    click: (item, focusedWindow) => {
      WindowActions.newFrame({
        location,
        isPartitioned: true
      }, false)
    }
  }
}

const copyLinkLocationMenuItem = location => {
  return {
    label: 'Copy link address',
    click: () => {
      clipboard.writeText(location)
    }
  }
}

function mainTemplateInit (nodeProps, frame) {
  const template = []
  const nodeName = nodeProps.name

  if (nodeProps.href) {
    template.push(openInNewTabMenuItem(nodeProps.href),
      openInNewPrivateTabMenuItem(nodeProps.href),
      openInNewSessionTabMenuItem(nodeProps.href),
      copyLinkLocationMenuItem(nodeProps.href),
      CommonMenu.separatorMenuItem)
  }

  if (nodeName === 'IMG') {
    template.push({
      label: 'Save image...',
      click: (item, focusedWindow) => {
        if (focusedWindow && nodeProps.src) {
          focusedWindow.webContents.downloadURL(nodeProps.src)
        }
      }
    })
    template.push({
      label: 'Open image in new tab',
      click: (item, focusedWindow) => {
        if (focusedWindow && nodeProps.src) {
          // TODO: open this in the next tab instead of last tab
          focusedWindow.webContents.send(messages.SHORTCUT_NEW_FRAME, nodeProps.src)
        }
      }
    })
    template.push({
      label: 'Copy image address',
      click: (item, focusedWindow) => {
        if (focusedWindow && nodeProps.src) {
          clipboard.writeText(nodeProps.src)
        }
      }
    })
    template.push(CommonMenu.separatorMenuItem)
  }

  if (nodeName === 'TEXTAREA' || nodeName === 'INPUT' || nodeProps.isContentEditable) {
    const editableItems = getEditableItems(nodeProps.hasSelection)
    template.push({
      label: 'Undo',
      accelerator: 'CmdOrCtrl+Z',
      role: 'undo'
    }, {
      label: 'Redo',
      accelerator: 'Shift+CmdOrCtrl+Z',
      role: 'redo'
    }, CommonMenu.separatorMenuItem, ...editableItems)
  } else if (nodeProps.hasSelection) {
    template.push({
      label: 'Copy',
      accelerator: 'CmdOrCtrl+C',
      role: 'copy'
    })
  }

  if (template.length > 0) {
    template.push(CommonMenu.separatorMenuItem)
  }

  template.push({
    label: 'Reload',
    click: (item, focusedWindow) => {
      if (focusedWindow) {
        focusedWindow.webContents.send(messages.SHORTCUT_ACTIVE_FRAME_RELOAD)
      }
    }
  }, {
    label: 'View Page Source',
    click: (item, focusedWindow) => {
      if (focusedWindow) {
        focusedWindow.webContents.send(messages.SHORTCUT_ACTIVE_FRAME_VIEW_SOURCE)
      }
    }
  },
  addBookmarkMenuItem(siteUtil.getDetailFromFrame(frame, siteTags.BOOKMARK)),
    {
      label: 'Add to reading list',
      enabled: false
    })
  return template
}

export function onHamburgerMenu (braverySettings, settingsState, e) {
  const hamburgerMenu = Menu.buildFromTemplate(hamburgerTemplateInit(braverySettings, settingsState))
  const rect = e.target.getBoundingClientRect()
  hamburgerMenu.popup(remote.getCurrentWindow(), rect.left, rect.bottom)
}

export function onMainContextMenu (nodeProps, frame, contextMenuType) {
  if (contextMenuType === 'bookmark') {
    onBookmarkContextMenu(Immutable.fromJS(nodeProps), Immutable.fromJS({ location: '', title: '', partitionNumber: frame.get('partitionNumber') }))
  } else {
    const mainMenu = Menu.buildFromTemplate(mainTemplateInit(nodeProps, frame))
    mainMenu.popup(remote.getCurrentWindow())
  }
}

export function onTabContextMenu (frameProps, e) {
  e.stopPropagation()
  const tabMenu = Menu.buildFromTemplate(tabTemplateInit(frameProps))
  tabMenu.popup(remote.getCurrentWindow())
}

export function onTabsToolbarContextMenu (settings, activeFrame, e) {
  e.stopPropagation()
  const tabsToolbarMenu = Menu.buildFromTemplate(tabsToolbarTemplateInit(settings, activeFrame))
  tabsToolbarMenu.popup(remote.getCurrentWindow())
}

export function onTabPageContextMenu (framePropsList, e) {
  e.stopPropagation()
  const tabPageMenu = Menu.buildFromTemplate(tabPageTemplateInit(framePropsList))
  tabPageMenu.popup(remote.getCurrentWindow())
}

export function onUrlBarContextMenu (e) {
  e.stopPropagation()
  const inputMenu = Menu.buildFromTemplate(inputTemplateInit(e))
  inputMenu.popup(remote.getCurrentWindow())
}

export function onBookmarkContextMenu (siteDetail, activeFrame, e) {
  if (e) {
    e.stopPropagation()
  }
  const menu = Menu.buildFromTemplate(bookmarkTemplateInit(siteDetail, activeFrame))
  menu.popup(remote.getCurrentWindow())
}

export function onShowBookmarkFolderMenu (bookmarks, bookmark, activeFrame, e) {
  const menu = Menu.buildFromTemplate(showBookmarkFolderInit(bookmarks, bookmark, activeFrame))
  const rectLeft = e.target.getBoundingClientRect()
  const rectBottom = e.target.parentNode.getBoundingClientRect()
  menu.popup(remote.getCurrentWindow(), rectLeft.left | 0, rectBottom.bottom | 0)
}

export function onMoreBookmarksMenu (activeFrame, bookmarks) {
  const menu = Menu.buildFromTemplate(moreBookmarksTemplateInit(activeFrame, bookmarks))
  menu.popup(remote.getCurrentWindow())
}
<commit_msg>Disable folders for 0.7.16 release<commit_after>/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this file,
 * You can obtain one at http://mozilla.org/MPL/2.0/. */

const electron = global.require('electron')
const remote = electron.remote
const Menu = remote.require('menu')
const Immutable = require('immutable')
const clipboard = electron.clipboard
const messages = require('./constants/messages')
const WindowStore = require('./stores/windowStore')
const WindowActions = require('./actions/windowActions')
const AppActions = require('./actions/appActions')
const siteTags = require('./constants/siteTags')
const siteUtil = require('./state/siteUtil')
const CommonMenu = require('./commonMenu')
const ipc = global.require('electron').ipcRenderer

/**
 * Obtains an add bookmark menu item
 * @param {object} Detail of the bookmark to initialize with
 */
const addBookmarkMenuItem = (siteDetail) => {
  return {
    label: 'Add Bookmark...',
    click: () => {
      WindowActions.setBookmarkDetail(siteDetail)
    }
  }
}

const addFolderMenuItem = {
  label: 'Add Folder...',
  enabled: false,
  click: () => {
    const emptyFolder = Immutable.fromJS({tags: [siteTags.BOOKMARK_FOLDER]})
    WindowActions.setBookmarkDetail(emptyFolder)
  }
}

function tabPageTemplateInit (framePropsList) {
  const muteAll = (framePropsList, mute) => {
    framePropsList.forEach(frameProps => {
      if (mute && frameProps.get('audioPlaybackActive') && !frameProps.get('audioMuted')) {
        WindowActions.setAudioMuted(frameProps, true)
      } else if (!mute && frameProps.get('audioMuted')) {
        WindowActions.setAudioMuted(frameProps, false)
      }
    })
  }
  return [{
    label: 'Unmute tabs',
    click: (item, focusedWindow) => {
      muteAll(framePropsList, false)
    }
  }, {
    label: 'Mute tabs',
    click: (item, focusedWindow) => {
      muteAll(framePropsList, true)
    }
  }]
}

function inputTemplateInit (e) {
  const hasSelection = e.target.selectionStart !== undefined &&
      e.target.selectionEnd !== undefined &&
      e.target.selectionStart !== e.target.selectionEnd
  return getEditableItems(hasSelection)
}

function tabsToolbarTemplateInit (settingsState, activeFrame) {
  return [
    CommonMenu.bookmarksMenuItem,
    CommonMenu.bookmarksToolbarMenuItem(settingsState),
    CommonMenu.separatorMenuItem,
    addBookmarkMenuItem(siteUtil.getDetailFromFrame(activeFrame, siteTags.BOOKMARK)),
    addFolderMenuItem
  ]
}

function moreBookmarksTemplateInit (activeFrame, bookmarks) {
  return bookmarks.map(bookmark => {
    return {
      label: bookmark.get('title'),
      click: () => {
        WindowActions.loadUrl(activeFrame, bookmark.get('location'))
      }
    }
  }).toJS()
}

function bookmarkTemplateInit (siteDetail, activeFrame) {
  const location = siteDetail.get('location')
  const isFolder = siteDetail.get('tags').includes(siteTags.BOOKMARK_FOLDER)
  const template = []
  if (!isFolder) {
    template.push(openInNewTabMenuItem(location),
      openInNewPrivateTabMenuItem(location),
      openInNewSessionTabMenuItem(location),
      copyLinkLocationMenuItem(location),
      CommonMenu.separatorMenuItem)
  }

  template.push(
    {
      label: isFolder ? 'Edit Folder...' : 'Edit Bookmark...',
      click: () => {
        // originalLocation is undefined signifies add mode
        WindowActions.setBookmarkDetail(siteDetail, siteDetail)
      }
    },
    CommonMenu.separatorMenuItem, {
      label: isFolder ? 'Delete Folder' : 'Delete Bookmark',
      click: () => {
        AppActions.removeSite(siteDetail, siteDetail.get('tags').includes(siteTags.BOOKMARK_FOLDER) ? siteTags.BOOKMARK_FOLDER : siteTags.BOOKMARK)
      }
    },
    CommonMenu.separatorMenuItem,
    addBookmarkMenuItem(siteUtil.getDetailFromFrame(activeFrame, siteTags.BOOKMARK)),
    addFolderMenuItem)
  return template
}

function showBookmarkFolderInit (bookmarks, bookmark, activeFrame) {
  const items = siteUtil.filterSitesRelativeTo(bookmarks, bookmark)
  if (items.size === 0) {
    return [{
      label: '(empty)',
      enabled: false
    }]
  }

  return items.map(site => {
    const templateItem = {
      label: site.get('title'),
      click: function () {
        WindowActions.loadUrl(activeFrame, site.get('location'))
      }
    }
    const isFolder = siteUtil.isFolder(site)
    if (isFolder) {
      templateItem.submenu = showBookmarkFolderInit(bookmarks, site, activeFrame)
    }
    return templateItem
  }).toJS()
}

function tabTemplateInit (frameProps) {
  const tabKey = frameProps.get('key')
  const items = []
  items.push({
    label: 'Reload tab',
    click: (item, focusedWindow) => {
      if (focusedWindow) {
        focusedWindow.webContents.send(messages.SHORTCUT_FRAME_RELOAD, tabKey)
      }
    }
  })

  if (!frameProps.get('isPrivate')) {
    if (frameProps.get('pinnedLocation')) {
      items.push({
        label: 'Unpin tab',
        click: (item) => {
          // Handle converting the current tab window into a pinned site
          WindowActions.setPinned(frameProps, false)
          // Handle setting it in app storage for the other windows
          AppActions.removeSite(siteUtil.getDetailFromFrame(frameProps), siteTags.PINNED)
        }
      })
    } else {
      items.push({
        label: 'Pin tab',
        click: (item) => {
          // Handle converting the current tab window into a pinned site
          WindowActions.setPinned(frameProps, true)
          // Handle setting it in app storage for the other windows
          AppActions.addSite(siteUtil.getDetailFromFrame(frameProps), siteTags.PINNED)
        }
      })
    }
  }

  if (frameProps.get('audioPlaybackActive')) {
    if (frameProps.get('audioMuted')) {
      items.push({
        label: 'Unmute tab',
        click: item => {
          WindowActions.setAudioMuted(frameProps, false)
        }
      })
    } else {
      items.push({
        label: 'Mute tab',
        click: item => {
          WindowActions.setAudioMuted(frameProps, true)
        }
      })
    }
  }

  Array.prototype.push.apply(items, [{
    label: 'Disable tracking protection',
    enabled: false
  }, {
    label: 'Disable ad block',
    enabled: false
  }])

  if (!frameProps.get('pinnedLocation')) {
    items.push({
      label: 'Close tab',
      click: (item, focusedWindow) => {
        if (focusedWindow) {
          // TODO: Don't switch active tabs when this is called
          focusedWindow.webContents.send(messages.SHORTCUT_CLOSE_FRAME, tabKey)
        }
      }
    })
  }

  items.push(Object.assign({},
    CommonMenu.reopenLastClosedTabItem,
    { enabled: WindowStore.getState().get('closedFrames').size > 0 }
  ))

  return items
}

function getEditableItems (hasSelection) {
  return [{
    label: 'Cut',
    enabled: hasSelection,
    accelerator: 'CmdOrCtrl+X',
    // Enabled doesn't work when a role is used
    role: hasSelection && 'cut' || undefined
  }, {
    label: 'Copy',
    enabled: hasSelection,
    accelerator: 'CmdOrCtrl+C',
    // Enabled doesn't work when a role is used
    role: hasSelection && 'copy' || undefined
  }, {
    label: 'Paste',
    accelerator: 'CmdOrCtrl+V',
    role: 'paste'
  }]
}

function hamburgerTemplateInit (braverySettings, settingsState) {
  const template = [
    CommonMenu.newTabMenuItem,
    CommonMenu.newPrivateTabMenuItem,
    CommonMenu.newPartitionedTabMenuItem,
    CommonMenu.newWindowMenuItem,
    CommonMenu.separatorMenuItem,
    CommonMenu.findOnPageMenuItem,
    CommonMenu.printMenuItem,
    CommonMenu.separatorMenuItem,
    CommonMenu.buildBraveryMenu(braverySettings, function () {
      ipc.send(messages.UPDATE_APP_MENU, {bookmarked: braverySettings.bookmarked})
    }),
    CommonMenu.separatorMenuItem,
    CommonMenu.preferencesMenuItem,
    {
      label: 'Bookmarks',
      submenu: [
        CommonMenu.bookmarksMenuItem,
        CommonMenu.bookmarksToolbarMenuItem(settingsState)
      ]
    },
    CommonMenu.separatorMenuItem,
    CommonMenu.quitMenuItem
  ]
  return template
}

const openInNewTabMenuItem = location => {
  return {
    label: 'Open in new tab',
    click: () => {
      WindowActions.newFrame({ location }, false)
    }
  }
}

const openInNewPrivateTabMenuItem = location => {
  return {
    label: 'Open in new private tab',
    click: () => {
      WindowActions.newFrame({
        location,
        isPrivate: true
      }, false)
    }
  }
}

const openInNewSessionTabMenuItem = location => {
  return {
    label: 'Open in new session tab',
    click: (item, focusedWindow) => {
      WindowActions.newFrame({
        location,
        isPartitioned: true
      }, false)
    }
  }
}

const copyLinkLocationMenuItem = location => {
  return {
    label: 'Copy link address',
    click: () => {
      clipboard.writeText(location)
    }
  }
}

function mainTemplateInit (nodeProps, frame) {
  const template = []
  const nodeName = nodeProps.name

  if (nodeProps.href) {
    template.push(openInNewTabMenuItem(nodeProps.href),
      openInNewPrivateTabMenuItem(nodeProps.href),
      openInNewSessionTabMenuItem(nodeProps.href),
      copyLinkLocationMenuItem(nodeProps.href),
      CommonMenu.separatorMenuItem)
  }

  if (nodeName === 'IMG') {
    template.push({
      label: 'Save image...',
      click: (item, focusedWindow) => {
        if (focusedWindow && nodeProps.src) {
          focusedWindow.webContents.downloadURL(nodeProps.src)
        }
      }
    })
    template.push({
      label: 'Open image in new tab',
      click: (item, focusedWindow) => {
        if (focusedWindow && nodeProps.src) {
          // TODO: open this in the next tab instead of last tab
          focusedWindow.webContents.send(messages.SHORTCUT_NEW_FRAME, nodeProps.src)
        }
      }
    })
    template.push({
      label: 'Copy image address',
      click: (item, focusedWindow) => {
        if (focusedWindow && nodeProps.src) {
          clipboard.writeText(nodeProps.src)
        }
      }
    })
    template.push(CommonMenu.separatorMenuItem)
  }

  if (nodeName === 'TEXTAREA' || nodeName === 'INPUT' || nodeProps.isContentEditable) {
    const editableItems = getEditableItems(nodeProps.hasSelection)
    template.push({
      label: 'Undo',
      accelerator: 'CmdOrCtrl+Z',
      role: 'undo'
    }, {
      label: 'Redo',
      accelerator: 'Shift+CmdOrCtrl+Z',
      role: 'redo'
    }, CommonMenu.separatorMenuItem, ...editableItems)
  } else if (nodeProps.hasSelection) {
    template.push({
      label: 'Copy',
      accelerator: 'CmdOrCtrl+C',
      role: 'copy'
    })
  }

  if (template.length > 0) {
    template.push(CommonMenu.separatorMenuItem)
  }

  template.push({
    label: 'Reload',
    click: (item, focusedWindow) => {
      if (focusedWindow) {
        focusedWindow.webContents.send(messages.SHORTCUT_ACTIVE_FRAME_RELOAD)
      }
    }
  }, {
    label: 'View Page Source',
    click: (item, focusedWindow) => {
      if (focusedWindow) {
        focusedWindow.webContents.send(messages.SHORTCUT_ACTIVE_FRAME_VIEW_SOURCE)
      }
    }
  },
  addBookmarkMenuItem(siteUtil.getDetailFromFrame(frame, siteTags.BOOKMARK)),
    {
      label: 'Add to reading list',
      enabled: false
    })
  return template
}

export function onHamburgerMenu (braverySettings, settingsState, e) {
  const hamburgerMenu = Menu.buildFromTemplate(hamburgerTemplateInit(braverySettings, settingsState))
  const rect = e.target.getBoundingClientRect()
  hamburgerMenu.popup(remote.getCurrentWindow(), rect.left, rect.bottom)
}

export function onMainContextMenu (nodeProps, frame, contextMenuType) {
  if (contextMenuType === 'bookmark') {
    onBookmarkContextMenu(Immutable.fromJS(nodeProps), Immutable.fromJS({ location: '', title: '', partitionNumber: frame.get('partitionNumber') }))
  } else {
    const mainMenu = Menu.buildFromTemplate(mainTemplateInit(nodeProps, frame))
    mainMenu.popup(remote.getCurrentWindow())
  }
}

export function onTabContextMenu (frameProps, e) {
  e.stopPropagation()
  const tabMenu = Menu.buildFromTemplate(tabTemplateInit(frameProps))
  tabMenu.popup(remote.getCurrentWindow())
}

export function onTabsToolbarContextMenu (settings, activeFrame, e) {
  e.stopPropagation()
  const tabsToolbarMenu = Menu.buildFromTemplate(tabsToolbarTemplateInit(settings, activeFrame))
  tabsToolbarMenu.popup(remote.getCurrentWindow())
}

export function onTabPageContextMenu (framePropsList, e) {
  e.stopPropagation()
  const tabPageMenu = Menu.buildFromTemplate(tabPageTemplateInit(framePropsList))
  tabPageMenu.popup(remote.getCurrentWindow())
}

export function onUrlBarContextMenu (e) {
  e.stopPropagation()
  const inputMenu = Menu.buildFromTemplate(inputTemplateInit(e))
  inputMenu.popup(remote.getCurrentWindow())
}

export function onBookmarkContextMenu (siteDetail, activeFrame, e) {
  if (e) {
    e.stopPropagation()
  }
  const menu = Menu.buildFromTemplate(bookmarkTemplateInit(siteDetail, activeFrame))
  menu.popup(remote.getCurrentWindow())
}

export function onShowBookmarkFolderMenu (bookmarks, bookmark, activeFrame, e) {
  const menu = Menu.buildFromTemplate(showBookmarkFolderInit(bookmarks, bookmark, activeFrame))
  const rectLeft = e.target.getBoundingClientRect()
  const rectBottom = e.target.parentNode.getBoundingClientRect()
  menu.popup(remote.getCurrentWindow(), rectLeft.left | 0, rectBottom.bottom | 0)
}

export function onMoreBookmarksMenu (activeFrame, bookmarks) {
  const menu = Menu.buildFromTemplate(moreBookmarksTemplateInit(activeFrame, bookmarks))
  menu.popup(remote.getCurrentWindow())
}
<|endoftext|>


<commit_before>"use strict";

var spawn = require('child-process-promise').spawn;
var path = require('path');
var pipe = require('../index.js');
var file = require('../file');

function getLaTeXML() {
    return path.resolve(__dirname + '/../../' + pipe.config.conf.latexml_path);
}

module.exports = function (getFolder, tex) {
    return getFolder.then(function (folder) {
        var args = [
            tex || folder.tex,
            '-pmml',
            folder.path + '/latexml.mml'
        ];
        var latexml = getLaTeXML();
        return file.exists(latexml)
            .then(function () {
                return spawn(latexml, args).then(function () {
                    return folder.path + '/latexml.mml';
                });
            }, function () {
                return 'latexml not present';
            });
    });
};<commit_msg>Use LaTeXML's texvc plugin<commit_after>"use strict";

var spawn = require('child-process-promise').spawn;
var path = require('path');
var pipe = require('../index.js');
var file = require('../file');

function getLaTeXML() {
    return path.resolve(__dirname + '/../../' + pipe.config.conf.latexml_path);
}

module.exports = function (getFolder, tex) {
    return getFolder.then(function (folder) {
        var args = [
            tex || folder.tex,
            '--preload',
            'texvc',
            '-pmml',
            folder.path + '/latexml.mml'
        ];
        var latexml = getLaTeXML();
        return file.exists(latexml)
            .then(function () {
                return spawn(latexml, args).then(function () {
                    return folder.path + '/latexml.mml';
                });
            }, function () {
                return 'latexml not present';
            });
    });
};<|endoftext|>


<commit_before>/*global rg2:false */
(function () {
  function CourseParser(evt, worldfile, localWorldfile) {
    this.courses = [];
    this.courseClassMapping = [];
    this.newcontrols = new rg2.Controls();
    this.courses.length = 0;
    this.courseClassMapping.length = 0;
    this.fromCondes = false;
    this.coursesGeoreferenced = false;
    this.newcontrols.deleteAllControls();
    // holding copies of worldfiles: not ideal but it works
    this.localWorldfile = localWorldfile;
    this.worldfile = worldfile;
    this.processCoursesXML(evt.target.result);
    return {courses: this.courses, newcontrols: this.newcontrols, mapping: this.courseClassMapping, georeferenced: this.coursesGeoreferenced};
  }

  CourseParser.prototype = {

    Constructor : CourseParser,

    processCoursesXML : function (rawXML) {
      var xml, version, nodelist;
      try {
        xml = $.parseXML(rawXML);
      } catch (err) {
        rg2.utils.showWarningDialog("XML file error", "File is not a valid XML course file.");
        return;
      }
      nodelist = xml.getElementsByTagName('CourseData');
      if (nodelist.length === 0) {
        if (xml.documentElement.nodeName === "kml") {
          this.processKMLCourses(xml);
        } else {
          rg2.utils.showWarningDialog("XML file error", "File is not a valid XML course file. CourseData element missing.");
        }
        return;
      }
      version = this.getVersion(xml);
      switch (version) {
      case "2.0.3":
        this.processIOFV2XMLCourses(xml);
        break;
      case "3.0":
        this.processIOFV3XMLCourses(xml);
        break;
      default:
        rg2.utils.showWarningDialog("XML file error", 'Invalid IOF file format. Version ' + version + ' not supported.');
      }
    },

    getVersion : function (xml) {
      var nodelist, version;
      version = "";
      // test for IOF Version 2
      nodelist = xml.getElementsByTagName('IOFVersion');
      if (nodelist.length > 0) {
        version = nodelist[0].getAttribute('version');
      }
      if (version === "") {
        // test for IOF Version 3
        nodelist = xml.getElementsByTagName('CourseData');
        if (nodelist.length > 0) {
          version = nodelist[0].getAttribute('iofVersion').trim();
          this.setCreator(nodelist[0].getAttribute('creator').trim());
        }
      }
      return version;
    },

    setCreator : function (text) {
      // allow handling of files from Condes which use original worldfile rather than WGS-84 as expected by IOF scheme
      if (text.indexOf('Condes') > -1) {
        this.fromCondes = true;
      }
    },

    processIOFV3XMLCourses : function (xml) {
      // extract all controls
      var nodelist, i, code, pt, latlng, controls, control;
      nodelist = xml.getElementsByTagName('Control');
      // only need first-level Controls
      pt = {x: 0, y: 0};
      controls = [];
      for (i = 0; i < nodelist.length; i += 1) {
        if (nodelist[i].parentNode.nodeName === 'RaceCourseData') {
          code = nodelist[i].getElementsByTagName("Id")[0].textContent;
          latlng = nodelist[i].getElementsByTagName("Position");
          if ((this.localWorldfile.valid) && (latlng.length > 0)) {
            pt = this.getXYFromLatLng(latlng);
            this.coursesGeoreferenced = true;
          } else {
            // only works if all controls have lat/lon or none do: surely a safe assumption...
            pt = this.getXYFromMapPosition(nodelist[i].getElementsByTagName("MapPosition"));
          }
          // don't want to save crossing points
          if (nodelist[i].getAttribute('type') !== 'CrossingPoint') {
            this.newcontrols.addControl(code.trim(), pt.x, pt.y);
            control = {};
            control.id = i;
            control.lat = parseFloat(latlng[0].getAttribute('lat'));
            control.lng = parseFloat(latlng[0].getAttribute('lng'));
            control.x = pt.x;
            control.y = pt.y;
            controls.push(control);
          }
        }
      }
      // extract all courses
      nodelist = xml.getElementsByTagName('Course');
      this.extractV3Courses(nodelist);
      // extract all course/class mappings
      nodelist = xml.getElementsByTagName('ClassCourseAssignment');
      this.extractV3CourseClassMapping(nodelist);
    },

    processKMLCourses : function (xml) {
      // reads OOM KML files: assumes we have a WGS-84 georeferenced map...
      // extract all controls
      var places, point, coords, code, pt, controls, control, i, codes, className, courseName, lat, lng;
      controls = [];
      this.coursesGeoreferenced = true;
      places = xml.getElementsByTagName('Placemark');
      for (i = 0; i < places.length; i = i + 1 ) {
        pt = {};
        code = places[i].getElementsByTagName('name')[0].childNodes[0].nodeValue.trim();
        point = places[i].getElementsByTagName('Point')[0];
        coords = point.getElementsByTagName('coordinates')[0].childNodes[0].nodeValue.trim().split(",");
        lat = +coords[1];
        lng = +coords[0];
        pt.x = this.worldfile.getX(lng, lat);
        pt.y = this.worldfile.getY(lng, lat);
        this.newcontrols.addControl(code.trim(), pt.x, pt.y);
        control = {};
        control.id = i + 1;
        control.lat = lat;
        control.lng = lng;
        control.x = pt.x;
        control.y = pt.y;
        controls.push(control);
      }
      // set up dummy score course
      codes = [];
      courseName = "Course 1";
      className = "Course 1";
      codes = this.newcontrols.controls.map(function (control) { return control.code;});
      // courseid 0 for now: set when result mapping is known
      this.courses.push({courseid: 0, x: [], y: [], codes: codes, name: courseName});
      this.courseClassMapping.push({'course': courseName, 'className': className});
      $("#rg2-select-course-file").addClass('valid');
    },

    getXYFromLatLng : function (latLng) {
      var lat, lng, pt;
      pt = {x: 0, y: 0};
      lat = parseFloat(latLng[0].getAttribute('lat'));
      lng = parseFloat(latLng[0].getAttribute('lng'));
      // handle Condes-specific georeferencing
      if (this.fromCondes) {
        // use original map worldfile
        pt.x = this.localWorldfile.getX(lng, lat);
        pt.y = this.localWorldfile.getY(lng, lat);
      } else {
        // use WGS-84 worldfile as expected (?) by IOF V3 schema
        pt.x = this.worldfile.getX(lng, lat);
        pt.y = this.worldfile.getY(lng, lat);
      }
      return pt;
    },

    processIOFV2XMLCourses : function (xml) {
      var i, x, y;
      // extract all start controls
      this.extractV2Controls(xml.getElementsByTagName('StartPoint'), 'StartPointCode');
      // extract all normal controls
      this.extractV2Controls(xml.getElementsByTagName('Control'), 'ControlCode');
      // extract all finish controls
      this.coursesGeoreferenced = this.extractV2Controls(xml.getElementsByTagName('FinishPoint'), 'FinishPointCode');
      if (this.coursesGeoreferenced) {
        if (this.localWorldfile.valid) {
          for (i = 0; i < this.newcontrols.controls.length; i += 1) {
            x = this.newcontrols.controls[i].x;
            y = this.newcontrols.controls[i].y;
            this.newcontrols.controls[i].x = this.localWorldfile.getX(x, y);
            this.newcontrols.controls[i].y = this.localWorldfile.getY(x, y);
          }
        }
      }
      // extract all courses
      this.extractV2Courses(xml.getElementsByTagName('Course'));
    },

    extractV2Courses : function (nodelist) {
      var i, name, codes, x, y;
      for (i = 0; i < nodelist.length; i += 1) {
        codes = [];
        x = [];
        y = [];
        name = nodelist[i].getElementsByTagName('CourseName')[0].textContent.trim();
        codes = this.extractCodesFromControlList(nodelist[i], "ControlCode");
        // add start code at beginning of array
        codes.unshift(nodelist[i].getElementsByTagName('StartPointCode')[0].textContent.trim());
        codes.push(nodelist[i].getElementsByTagName('FinishPointCode')[0].textContent.trim());
        // courseid 0 for now: set when result mapping is known
        this.courses.push({courseid: 0, x: x, y: y, codes: codes, name: name});
      }
      $("#rg2-select-course-file").addClass('valid');
    },

    extractV3Courses : function (nodelist) {
      var i, name, codes, x, y;
      for (i = 0; i < nodelist.length; i += 1) {
        codes = [];
        x = [];
        y = [];
        name = nodelist[i].getElementsByTagName('Name')[0].textContent.trim();
        codes = this.extractCodesFromControlList(nodelist[i], "Control");
        // courseid 0 for now: set when result mapping is known
        this.courses.push({courseid: 0, x: x, y: y, codes: codes, name: name});
      }
      $("#rg2-select-course-file").addClass('valid');
    },

    extractV3CourseClassMapping : function (nodelist) {
      var i, course, className;
      for (i = 0; i < nodelist.length; i += 1) {
        course = nodelist[i].getElementsByTagName('CourseName')[0].textContent.trim();
        className = nodelist[i].getElementsByTagName('ClassName')[0].textContent.trim();
        this.courseClassMapping.push({'course': course, 'className': className});
      }
      $("#rg2-select-course-file").addClass('valid');
    },

    extractCodesFromControlList : function (nodeList, tagName) {
      // tagName depends on IOF version being parsed
      var i, code, codes, controlList;
      controlList = nodeList.getElementsByTagName('CourseControl');
      codes = [];
      for (i = 0; i < controlList.length; i += 1) {
        code = controlList[i].getElementsByTagName(tagName)[0].textContent.trim();
        // if control code doesn't exist it was a crossing point so we don't need it
        if (this.validControlCode(code)) {
          codes.push(code);
        }
      }
      return codes;
    },

    // returns true if controls are georeferenced
    extractV2Controls : function (nodelist, type) {
      var i, pt, code, geopos, isGeoref;
      isGeoref = false;
      pt = {x: 0, y: 0};
      for (i = 0; i < nodelist.length; i += 1) {
        code = nodelist[i].getElementsByTagName(type)[0].textContent;
        geopos = nodelist[i].getElementsByTagName("ControlPosition");
        // subtle bug and a half #190
        // if you have an IOF XML V2 file which has georeferenced controls AND
        // the map file itself isn't georeferenced
        // then you need to use X, Y and not the georeferenced co-ordinates
        if ((geopos.length > 0) && (this.localWorldfile.valid)) {
          pt.x = parseFloat(geopos[0].getAttribute('x'));
          pt.y = parseFloat(geopos[0].getAttribute('y'));
          isGeoref = true;
        } else {
          pt = this.getXYFromMapPosition(nodelist[i].getElementsByTagName("MapPosition"));
        }
        this.newcontrols.addControl(code.trim(), pt.x, pt.y);
      }
      return isGeoref;
    },

    getXYFromMapPosition : function (mapPosition) {
      // #269 allow for getting a comma instead of a decimal point
      return {x: mapPosition[0].getAttribute('x').replace(",", "."), y: mapPosition[0].getAttribute('y').replace(",", ".")};
    },

    // check if a given control code is in the list of known controls
    validControlCode : function (code) {
      var i, controls;
      controls = this.newcontrols.controls;
      for (i = 0; i < controls.length; i += 1) {
        if (controls[i].code === code) {
          return true;
        }
      }
      return false;
    }
  };
  rg2.CourseParser = CourseParser;
}());<commit_msg>Handle Condes 10.1+ georeferencing<commit_after>/*global rg2:false */
(function () {
  function CourseParser(evt, worldfile, localWorldfile) {
    this.courses = [];
    this.courseClassMapping = [];
    this.newcontrols = new rg2.Controls();
    this.courses.length = 0;
    this.courseClassMapping.length = 0;
    this.fromOldCondes = false;
    this.coursesGeoreferenced = false;
    this.newcontrols.deleteAllControls();
    // holding copies of worldfiles: not ideal but it works
    this.localWorldfile = localWorldfile;
    this.worldfile = worldfile;
    this.processCoursesXML(evt.target.result);
    return {courses: this.courses, newcontrols: this.newcontrols, mapping: this.courseClassMapping, georeferenced: this.coursesGeoreferenced};
  }

  CourseParser.prototype = {

    Constructor : CourseParser,

    processCoursesXML : function (rawXML) {
      var xml, version, nodelist;
      try {
        xml = $.parseXML(rawXML);
      } catch (err) {
        rg2.utils.showWarningDialog("XML file error", "File is not a valid XML course file.");
        return;
      }
      nodelist = xml.getElementsByTagName('CourseData');
      if (nodelist.length === 0) {
        if (xml.documentElement.nodeName === "kml") {
          this.processKMLCourses(xml);
        } else {
          rg2.utils.showWarningDialog("XML file error", "File is not a valid XML course file. CourseData element missing.");
        }
        return;
      }
      version = this.getVersion(xml);
      switch (version) {
      case "2.0.3":
        this.processIOFV2XMLCourses(xml);
        break;
      case "3.0":
        this.processIOFV3XMLCourses(xml);
        break;
      default:
        rg2.utils.showWarningDialog("XML file error", 'Invalid IOF file format. Version ' + version + ' not supported.');
      }
    },

    getVersion : function (xml) {
      var nodelist, version;
      version = "";
      // test for IOF Version 2
      nodelist = xml.getElementsByTagName('IOFVersion');
      if (nodelist.length > 0) {
        version = nodelist[0].getAttribute('version');
      }
      if (version === "") {
        // test for IOF Version 3
        nodelist = xml.getElementsByTagName('CourseData');
        if (nodelist.length > 0) {
          version = nodelist[0].getAttribute('iofVersion').trim();
          this.setCreator(nodelist[0].getAttribute('creator').trim());
        }
      }
      return version;
    },

    setCreator : function (text) {
      var version;
      // creator looks like "Condes version 10.1.1": need to extract version number
      var introLength = 15;
      // allow handling of files from Condes before v10.1 which use original worldfile rather than WGS-84 as expected by IOF scheme
      if (text.indexOf('Condes') > -1) {
        if (text.length > introLength) {
          // parseFloat ignores everything from second decimal poin if it finds one
          // so we will get something like 10.1 from 10.1.1
          version = parseFloat(text.substring(introLength));
          if (version < 10.1) {
            this.fromOldCondes = true;
          }
        }
      }
    },

    processIOFV3XMLCourses : function (xml) {
      // extract all controls
      var nodelist, i, code, pt, latlng, controls, control;
      nodelist = xml.getElementsByTagName('Control');
      // only need first-level Controls
      pt = {x: 0, y: 0};
      controls = [];
      for (i = 0; i < nodelist.length; i += 1) {
        if (nodelist[i].parentNode.nodeName === 'RaceCourseData') {
          code = nodelist[i].getElementsByTagName("Id")[0].textContent;
          latlng = nodelist[i].getElementsByTagName("Position");
          if ((this.localWorldfile.valid) && (latlng.length > 0)) {
            pt = this.getXYFromLatLng(latlng);
            this.coursesGeoreferenced = true;
          } else {
            // only works if all controls have lat/lon or none do: surely a safe assumption...
            pt = this.getXYFromMapPosition(nodelist[i].getElementsByTagName("MapPosition"));
          }
          // don't want to save crossing points
          if (nodelist[i].getAttribute('type') !== 'CrossingPoint') {
            this.newcontrols.addControl(code.trim(), pt.x, pt.y);
            control = {};
            control.id = i;
            control.lat = parseFloat(latlng[0].getAttribute('lat'));
            control.lng = parseFloat(latlng[0].getAttribute('lng'));
            control.x = pt.x;
            control.y = pt.y;
            controls.push(control);
          }
        }
      }
      // extract all courses
      nodelist = xml.getElementsByTagName('Course');
      this.extractV3Courses(nodelist);
      // extract all course/class mappings
      nodelist = xml.getElementsByTagName('ClassCourseAssignment');
      this.extractV3CourseClassMapping(nodelist);
    },

    processKMLCourses : function (xml) {
      // reads OOM KML files: assumes we have a WGS-84 georeferenced map...
      // extract all controls
      var places, point, coords, code, pt, controls, control, i, codes, className, courseName, lat, lng;
      controls = [];
      this.coursesGeoreferenced = true;
      places = xml.getElementsByTagName('Placemark');
      for (i = 0; i < places.length; i = i + 1 ) {
        pt = {};
        code = places[i].getElementsByTagName('name')[0].childNodes[0].nodeValue.trim();
        point = places[i].getElementsByTagName('Point')[0];
        coords = point.getElementsByTagName('coordinates')[0].childNodes[0].nodeValue.trim().split(",");
        lat = +coords[1];
        lng = +coords[0];
        pt.x = this.worldfile.getX(lng, lat);
        pt.y = this.worldfile.getY(lng, lat);
        this.newcontrols.addControl(code.trim(), pt.x, pt.y);
        control = {};
        control.id = i + 1;
        control.lat = lat;
        control.lng = lng;
        control.x = pt.x;
        control.y = pt.y;
        controls.push(control);
      }
      // set up dummy score course
      codes = [];
      courseName = "Course 1";
      className = "Course 1";
      codes = this.newcontrols.controls.map(function (control) { return control.code;});
      // courseid 0 for now: set when result mapping is known
      this.courses.push({courseid: 0, x: [], y: [], codes: codes, name: courseName});
      this.courseClassMapping.push({'course': courseName, 'className': className});
      $("#rg2-select-course-file").addClass('valid');
    },

    getXYFromLatLng : function (latLng) {
      var lat, lng, pt;
      pt = {x: 0, y: 0};
      lat = parseFloat(latLng[0].getAttribute('lat'));
      lng = parseFloat(latLng[0].getAttribute('lng'));
      // handle old Condes-specific georeferencing
      if (this.fromOldCondes) {
        // use original map worldfile
        pt.x = this.localWorldfile.getX(lng, lat);
        pt.y = this.localWorldfile.getY(lng, lat);
      } else {
        // use WGS-84 worldfile as expected (?) by IOF V3 schema
        pt.x = this.worldfile.getX(lng, lat);
        pt.y = this.worldfile.getY(lng, lat);
      }
      return pt;
    },

    processIOFV2XMLCourses : function (xml) {
      var i, x, y;
      // extract all start controls
      this.extractV2Controls(xml.getElementsByTagName('StartPoint'), 'StartPointCode');
      // extract all normal controls
      this.extractV2Controls(xml.getElementsByTagName('Control'), 'ControlCode');
      // extract all finish controls
      this.coursesGeoreferenced = this.extractV2Controls(xml.getElementsByTagName('FinishPoint'), 'FinishPointCode');
      if (this.coursesGeoreferenced) {
        if (this.localWorldfile.valid) {
          for (i = 0; i < this.newcontrols.controls.length; i += 1) {
            x = this.newcontrols.controls[i].x;
            y = this.newcontrols.controls[i].y;
            this.newcontrols.controls[i].x = this.localWorldfile.getX(x, y);
            this.newcontrols.controls[i].y = this.localWorldfile.getY(x, y);
          }
        }
      }
      // extract all courses
      this.extractV2Courses(xml.getElementsByTagName('Course'));
    },

    extractV2Courses : function (nodelist) {
      var i, name, codes, x, y;
      for (i = 0; i < nodelist.length; i += 1) {
        codes = [];
        x = [];
        y = [];
        name = nodelist[i].getElementsByTagName('CourseName')[0].textContent.trim();
        codes = this.extractCodesFromControlList(nodelist[i], "ControlCode");
        // add start code at beginning of array
        codes.unshift(nodelist[i].getElementsByTagName('StartPointCode')[0].textContent.trim());
        codes.push(nodelist[i].getElementsByTagName('FinishPointCode')[0].textContent.trim());
        // courseid 0 for now: set when result mapping is known
        this.courses.push({courseid: 0, x: x, y: y, codes: codes, name: name});
      }
      $("#rg2-select-course-file").addClass('valid');
    },

    extractV3Courses : function (nodelist) {
      var i, name, codes, x, y;
      for (i = 0; i < nodelist.length; i += 1) {
        codes = [];
        x = [];
        y = [];
        name = nodelist[i].getElementsByTagName('Name')[0].textContent.trim();
        codes = this.extractCodesFromControlList(nodelist[i], "Control");
        // courseid 0 for now: set when result mapping is known
        this.courses.push({courseid: 0, x: x, y: y, codes: codes, name: name});
      }
      $("#rg2-select-course-file").addClass('valid');
    },

    extractV3CourseClassMapping : function (nodelist) {
      var i, course, className;
      for (i = 0; i < nodelist.length; i += 1) {
        course = nodelist[i].getElementsByTagName('CourseName')[0].textContent.trim();
        className = nodelist[i].getElementsByTagName('ClassName')[0].textContent.trim();
        this.courseClassMapping.push({'course': course, 'className': className});
      }
      $("#rg2-select-course-file").addClass('valid');
    },

    extractCodesFromControlList : function (nodeList, tagName) {
      // tagName depends on IOF version being parsed
      var i, code, codes, controlList;
      controlList = nodeList.getElementsByTagName('CourseControl');
      codes = [];
      for (i = 0; i < controlList.length; i += 1) {
        code = controlList[i].getElementsByTagName(tagName)[0].textContent.trim();
        // if control code doesn't exist it was a crossing point so we don't need it
        if (this.validControlCode(code)) {
          codes.push(code);
        }
      }
      return codes;
    },

    // returns true if controls are georeferenced
    extractV2Controls : function (nodelist, type) {
      var i, pt, code, geopos, isGeoref;
      isGeoref = false;
      pt = {x: 0, y: 0};
      for (i = 0; i < nodelist.length; i += 1) {
        code = nodelist[i].getElementsByTagName(type)[0].textContent;
        geopos = nodelist[i].getElementsByTagName("ControlPosition");
        // subtle bug and a half #190
        // if you have an IOF XML V2 file which has georeferenced controls AND
        // the map file itself isn't georeferenced
        // then you need to use X, Y and not the georeferenced co-ordinates
        if ((geopos.length > 0) && (this.localWorldfile.valid)) {
          pt.x = parseFloat(geopos[0].getAttribute('x'));
          pt.y = parseFloat(geopos[0].getAttribute('y'));
          isGeoref = true;
        } else {
          pt = this.getXYFromMapPosition(nodelist[i].getElementsByTagName("MapPosition"));
        }
        this.newcontrols.addControl(code.trim(), pt.x, pt.y);
      }
      return isGeoref;
    },

    getXYFromMapPosition : function (mapPosition) {
      // #269 allow for getting a comma instead of a decimal point
      return {x: mapPosition[0].getAttribute('x').replace(",", "."), y: mapPosition[0].getAttribute('y').replace(",", ".")};
    },

    // check if a given control code is in the list of known controls
    validControlCode : function (code) {
      var i, controls;
      controls = this.newcontrols.controls;
      for (i = 0; i < controls.length; i += 1) {
        if (controls[i].code === code) {
          return true;
        }
      }
      return false;
    }
  };
  rg2.CourseParser = CourseParser;
}());<|endoftext|>


<commit_before>'use strict';

var expectFile = require('gulp-expect-file');
var streamSync = require('./stream-sync');
var through = require('through2').obj;

module.exports = function (gulp, gutil) {

  function allDirTreePaths(paths) {
    var newPaths = [];
    for (var i = 0; i < paths.length; i++) {
      var dirs = paths[i].split('/');
      var path = '';
      for (var j = 0; j < dirs.length; j++) {
        path = path + (path.length === 0 ? '' : '/') + dirs[j];
        if (newPaths.indexOf(path) === -1) {
          newPaths.push(path);
        }
      }
    }
    return newPaths;
  }

  var RULES = [
    // expected files
    {
      pattern: ['**/*'],
      options: {
        reportMissing: false,
        reportUnexpected: true,
        errorOnFailure: true,
        testFiles: true
      },
      expectations: [
        'res/lang/text.{en,en-gb}.properties',
        'res/lang/text.{ar,bg,ca,cs,da,de,el,es,es-mx,fi,fr,hr,hu,in,it,iw,ja,kk,ko,ms,nl,no,pl,pt,pt-br,ro,ru,sk,sq,' +
          'sv,th,tr,uk,vi,zh-cn,zh-tw}.(UTF-8).properties',
        'res/lang/text.{en,en-gb}/*.text',
        'res/lang/text.{ar,bg,ca,cs,da,de,el,es,es-mx,fi,fr,hr,hu,in,it,iw,ja,kk,ko,ms,nl,no,pl,pt,pt-br,ro,ru,sk,sq,' +
          'sv,th,tr,uk,vi,zh-cn,zh-tw}.(UTF-8)/*.text',
        'res/lang/feature/*/text.*.properties',
        'res/lang/feature/*/text.*.properties.json',
        'res/quilts/*.quilt.xml',
        'res/quilts/custom/*.quilt.xml',
        'res/layouts/*.layout.xml',
        'res/orders/*.order.xml',
        'res/components/*.ftl',
        'res/endpoints/*.endpoint.xml',
        'res/controllers/*.ftl',
        'res/macros/*.ftl',
        'res/init/Common.init.ftl',
        'res/skins/**/css/*.css',
        'res/skins/**/sass/**/*.{scss,sass}',
        'res/skins/**/sass/**/*.{scss,sass}.ftl',
        'res/skins/**/components/*.ftl',
        'res/skins/**/images/*.{png,gif,jpg,jpeg,svg}',
        'res/skins/**/skin.properties',
        'res/js/angularjs/**/*.js',
        'res/js/angularjs/metadata/dependencies.json',
        'res/editors/tinymce/**/*.js',
        'res/editors/tinymce/instances/**/*.json',
        'res/editors/tinymce/plugins/**/img/*.{png,gif,jpg,jpeg,svg}',
        'res/feature/*/*/res/lang/text.{en,en-gb}.properties',
        'res/feature/*/*/res/lang/text.{ar,bg,ca,cs,da,de,el,es,es-mx,fi,fr,hr,hu,in,it,iw,ja,kk,ko,ms,nl,no,pl,pt,pt-br,ro,ru,sk,sq,' +
          'sv,th,tr,uk,vi,zh-cn,zh-tw}.(UTF-8).properties',
        'res/feature/*/*/res/lang/text.{en,en-gb}/*.text',
        'res/feature/*/*/res/lang/text.{ar,bg,ca,cs,da,de,el,es,es-mx,fi,fr,hr,hu,in,it,iw,ja,kk,ko,ms,nl,no,pl,pt,pt-br,ro,ru,sk,sq,' +
          'sv,th,tr,uk,vi,zh-cn,zh-tw}.(UTF-8)/*.text',
        'res/feature/*/*/res/quilts/*.quilt.xml',
        'res/feature/*/*/res/layouts/*.layout.xml',
        'res/feature/*/*/res/orders/*.order.xml',
        'res/feature/*/*/res/components/*.ftl',
        'res/feature/*/*/res/init/Common.init.ftl',
        'res/feature/*/*/res/skins/**/css/*.css',
        'res/feature/*/*/res/skins/**/sass/**/*.{scss,sass}',
        'res/feature/*/*/res/skins/**/sass/**/*.{scss,sass}.ftl',
        'res/feature/*/*/res/skins/**/components/*.ftl',
        'res/feature/*/*/res/skins/**/images/*.{png,gif,jpg,jpeg,svg}',
        'res/feature/*/*/res/skins/**/skin.properties',
        'res/feature/*/*/res/editors/tinymce/**/*.js',
        'res/feature/*/*/res/editors/tinymce/instances/**/*.json',
        'res/feature/*/*/res/editors/tinymce/plugins/**/img/*.{png,gif,jpg,jpeg,svg}',
        'res/feature/feature-definition.xml',
        '{res,web}/**/README.md',
        '{res,web}/**/*.example',
        'web/html/**/*.*',
        'web/html/apps/**/*.*',
        'web/html/**/version'
      ]
    },
    // expected directories
    {
      pattern: ['**/*'],
      options: {
        reportMissing: false,
        reportUnexpected: true,
        errorOnFailure: true,
        testDirectories: true
      },
      expectations: allDirTreePaths([
        'res/editors/tinymce/**',
        'res/lang',
        'res/lang/text.{en,en-gb}',
        'res/lang/text.{ar,bg,ca,cs,da,de,el,es,es-mx,fi,fr,hr,hu,in,it,iw,ja,kk,ko,ms,nl,no,pl,pt,pt-br,ro,ru,sk,sq,' +
          'sv,th,tr,uk,vi,zh-cn,zh-tw}.(UTF-8)',
        'res/lang/feature/*',
        'res/quilts',
        'res/quilts/custom',
        'res/layouts',
        'res/orders',
        'res/components',
        'res/init',
        'res/controllers',
        'res/endpoints',
        'res/macros',
        'res/skins/**',
        'res/js/angularjs/**',
        'res/js/angularjs/metadata',
        'res/feature/*/*/res/lang',
        'res/feature/*/*/res/lang/text.{en,en-gb}',
        'res/feature/*/*/res/lang/text.{ar,bg,ca,cs,da,de,el,es,es-mx,fi,fr,hr,hu,in,it,iw,ja,kk,ko,ms,nl,no,pl,pt,pt-br,ro,ru,sk,sq,' +
          'sv,th,tr,uk,vi,zh-cn,zh-tw}.(UTF-8)',
        'res/feature/*/*/res/quilts',
        'res/feature/*/*/res/layouts',
        'res/feature/*/*/res/orders',
        'res/feature/*/*/res/components',
        'res/feature/*/*/res/init',
        'res/feature/*/*/res/editors/tinymce/**',
        'res/feature/*/*/res/skins/**',
        'web/html/ng-app/status/version',
        'web/html/apps/**',
        'web/html/assets/**',
        'web/html/badge_icons/**',
        'web/html/rank_icons/**',
        'web/html/survey_icons/**',
        'web/html/**'
      ])
    },
    {
      pattern: '**/*',
      options: {
        reportMissing: true,
        reportUnexpected: false,
        errorOnFailure: true,
        testDirectories: true
      },
      expectations: allDirTreePaths([
        'res/**'
      ])
    }
  ];

  return {
    verify: function () {
      return streamSync(
        {
          collateErrors: true
        },
        RULES.map(function (rule) {
          return function () {
            return gulp.src(rule.pattern, { cwd: 'plugin' })
              .pipe(through(function (file, enc, cb) {
                if (file.isStream()) {
                  cb(new gutil.PluginError('plugin-verify', 'Streaming not supported'));
                  return;
                }
                if (file.isNull() && file.stat && file.stat.isDirectory && file.stat.isDirectory()) {
                  if (rule.options.testDirectories) {
                    this.push(file);
                  }
                } else {
                  if (rule.options.testFiles) {
                    this.push(file);
                  }
                }
                cb();
              }))
              .pipe(expectFile(rule.options, rule.expectations));
          };
        })
      );
    }
  };
};
<commit_msg>Allow grids in plugin<commit_after>'use strict';

var expectFile = require('gulp-expect-file');
var streamSync = require('./stream-sync');
var through = require('through2').obj;

module.exports = function (gulp, gutil) {

  function allDirTreePaths(paths) {
    var newPaths = [];
    for (var i = 0; i < paths.length; i++) {
      var dirs = paths[i].split('/');
      var path = '';
      for (var j = 0; j < dirs.length; j++) {
        path = path + (path.length === 0 ? '' : '/') + dirs[j];
        if (newPaths.indexOf(path) === -1) {
          newPaths.push(path);
        }
      }
    }
    return newPaths;
  }

  var RULES = [
    // expected files
    {
      pattern: ['**/*'],
      options: {
        reportMissing: false,
        reportUnexpected: true,
        errorOnFailure: true,
        testFiles: true
      },
      expectations: [
        'res/lang/text.{en,en-gb}.properties',
        'res/lang/text.{ar,bg,ca,cs,da,de,el,es,es-mx,fi,fr,hr,hu,in,it,iw,ja,kk,ko,ms,nl,no,pl,pt,pt-br,ro,ru,sk,sq,' +
          'sv,th,tr,uk,vi,zh-cn,zh-tw}.(UTF-8).properties',
        'res/lang/text.{en,en-gb}/*.text',
        'res/lang/text.{ar,bg,ca,cs,da,de,el,es,es-mx,fi,fr,hr,hu,in,it,iw,ja,kk,ko,ms,nl,no,pl,pt,pt-br,ro,ru,sk,sq,' +
          'sv,th,tr,uk,vi,zh-cn,zh-tw}.(UTF-8)/*.text',
        'res/lang/feature/*/text.*.properties',
        'res/lang/feature/*/text.*.properties.json',
        'res/quilts/*.quilt.xml',
        'res/quilts/custom/*.quilt.xml',
        'res/layouts/*.layout.xml',
        'res/grids/*.grid.xml',
        'res/orders/*.order.xml',
        'res/components/*.ftl',
        'res/endpoints/*.endpoint.xml',
        'res/controllers/*.ftl',
        'res/macros/*.ftl',
        'res/init/Common.init.ftl',
        'res/skins/**/css/*.css',
        'res/skins/**/sass/**/*.{scss,sass}',
        'res/skins/**/sass/**/*.{scss,sass}.ftl',
        'res/skins/**/components/*.ftl',
        'res/skins/**/images/*.{png,gif,jpg,jpeg,svg}',
        'res/skins/**/skin.properties',
        'res/js/angularjs/**/*.js',
        'res/js/angularjs/metadata/dependencies.json',
        'res/editors/tinymce/**/*.js',
        'res/editors/tinymce/instances/**/*.json',
        'res/editors/tinymce/plugins/**/img/*.{png,gif,jpg,jpeg,svg}',
        'res/feature/*/*/res/lang/text.{en,en-gb}.properties',
        'res/feature/*/*/res/lang/text.{ar,bg,ca,cs,da,de,el,es,es-mx,fi,fr,hr,hu,in,it,iw,ja,kk,ko,ms,nl,no,pl,pt,pt-br,ro,ru,sk,sq,' +
          'sv,th,tr,uk,vi,zh-cn,zh-tw}.(UTF-8).properties',
        'res/feature/*/*/res/lang/text.{en,en-gb}/*.text',
        'res/feature/*/*/res/lang/text.{ar,bg,ca,cs,da,de,el,es,es-mx,fi,fr,hr,hu,in,it,iw,ja,kk,ko,ms,nl,no,pl,pt,pt-br,ro,ru,sk,sq,' +
          'sv,th,tr,uk,vi,zh-cn,zh-tw}.(UTF-8)/*.text',
        'res/feature/*/*/res/quilts/*.quilt.xml',
        'res/feature/*/*/res/layouts/*.layout.xml',
        'res/feature/*/*/res/grids/*.grid.xml',
        'res/feature/*/*/res/orders/*.order.xml',
        'res/feature/*/*/res/components/*.ftl',
        'res/feature/*/*/res/init/Common.init.ftl',
        'res/feature/*/*/res/skins/**/css/*.css',
        'res/feature/*/*/res/skins/**/sass/**/*.{scss,sass}',
        'res/feature/*/*/res/skins/**/sass/**/*.{scss,sass}.ftl',
        'res/feature/*/*/res/skins/**/components/*.ftl',
        'res/feature/*/*/res/skins/**/images/*.{png,gif,jpg,jpeg,svg}',
        'res/feature/*/*/res/skins/**/skin.properties',
        'res/feature/*/*/res/editors/tinymce/**/*.js',
        'res/feature/*/*/res/editors/tinymce/instances/**/*.json',
        'res/feature/*/*/res/editors/tinymce/plugins/**/img/*.{png,gif,jpg,jpeg,svg}',
        'res/feature/feature-definition.xml',
        '{res,web}/**/README.md',
        '{res,web}/**/*.example',
        'web/html/**/*.*',
        'web/html/apps/**/*.*',
        'web/html/**/version'
      ]
    },
    // expected directories
    {
      pattern: ['**/*'],
      options: {
        reportMissing: false,
        reportUnexpected: true,
        errorOnFailure: true,
        testDirectories: true
      },
      expectations: allDirTreePaths([
        'res/editors/tinymce/**',
        'res/lang',
        'res/lang/text.{en,en-gb}',
        'res/lang/text.{ar,bg,ca,cs,da,de,el,es,es-mx,fi,fr,hr,hu,in,it,iw,ja,kk,ko,ms,nl,no,pl,pt,pt-br,ro,ru,sk,sq,' +
          'sv,th,tr,uk,vi,zh-cn,zh-tw}.(UTF-8)',
        'res/lang/feature/*',
        'res/quilts',
        'res/quilts/custom',
        'res/layouts',
        'res/grids',
        'res/orders',
        'res/components',
        'res/init',
        'res/controllers',
        'res/endpoints',
        'res/macros',
        'res/skins/**',
        'res/js/angularjs/**',
        'res/js/angularjs/metadata',
        'res/feature/*/*/res/lang',
        'res/feature/*/*/res/lang/text.{en,en-gb}',
        'res/feature/*/*/res/lang/text.{ar,bg,ca,cs,da,de,el,es,es-mx,fi,fr,hr,hu,in,it,iw,ja,kk,ko,ms,nl,no,pl,pt,pt-br,ro,ru,sk,sq,' +
          'sv,th,tr,uk,vi,zh-cn,zh-tw}.(UTF-8)',
        'res/feature/*/*/res/quilts',
        'res/feature/*/*/res/layouts',
        'res/feature/*/*/res/grids',
        'res/feature/*/*/res/orders',
        'res/feature/*/*/res/components',
        'res/feature/*/*/res/init',
        'res/feature/*/*/res/editors/tinymce/**',
        'res/feature/*/*/res/skins/**',
        'web/html/ng-app/status/version',
        'web/html/apps/**',
        'web/html/assets/**',
        'web/html/badge_icons/**',
        'web/html/rank_icons/**',
        'web/html/survey_icons/**',
        'web/html/**'
      ])
    },
    {
      pattern: '**/*',
      options: {
        reportMissing: true,
        reportUnexpected: false,
        errorOnFailure: true,
        testDirectories: true
      },
      expectations: allDirTreePaths([
        'res/**'
      ])
    }
  ];

  return {
    verify: function () {
      return streamSync(
        {
          collateErrors: true
        },
        RULES.map(function (rule) {
          return function () {
            return gulp.src(rule.pattern, { cwd: 'plugin' })
              .pipe(through(function (file, enc, cb) {
                if (file.isStream()) {
                  cb(new gutil.PluginError('plugin-verify', 'Streaming not supported'));
                  return;
                }
                if (file.isNull() && file.stat && file.stat.isDirectory && file.stat.isDirectory()) {
                  if (rule.options.testDirectories) {
                    this.push(file);
                  }
                } else {
                  if (rule.options.testFiles) {
                    this.push(file);
                  }
                }
                cb();
              }))
              .pipe(expectFile(rule.options, rule.expectations));
          };
        })
      );
    }
  };
};
<|endoftext|>